Although I cannot use the Scala language at work because the productivity of our team would crater, I have still an interest in a language I would love to be able to use in a business world. Deciding that perhaps I was too harsh on the language, I have been playing with a couple of Scala libraries to try and give the language another shot in my off hours work. I just spent the better part of 7 hours playing with Slick 3.2.0 in Akka and Scala. I was massively frustrated and it turned out that yet again it is an implicit conversion that hosed me. Consider the following slick project.
Users.scala
package persistence.entities
import slick.driver.H2Driver.api._
case class User(id: Long, name: String)
class UserTable(tag: Tag) extends Table[User](tag, "user") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def * = (id, name) <> (User.tupled, User.unapply)
}
UsersDAL.scala
package persistence.dal
import persistence.entities.{Tables, User, UserTable}
import slick.jdbc.JdbcProfile
import slick.lifted.Query
import scala.concurrent.Future
trait UserDAL {
def findById(id: Long) : Future[Option[User]]
}
class UserDALImpl(implicit val db: JdbcProfile#Backend#Database) extends UserDAL {
override def findById(userId: Long) = null
def foo (userId : Long) = {
val filter: Query[UserTable, User, Seq] = Tables.users.filter(_.id == userId)
db.run(filter.result)
}
}
Should be simple right? No. The db.run(filter.result) line does not compile on the result call. OK, time to comb the Slick documentation. No dice, my code looks just like theirs does fundamentally. So why is the result method not available. Do I have the wrong type? Nope. What about the wrong library? Nope. Update from 3.1.1 to 3.2.0 ? Done … but … no. What about the wrong structure for my table? Am I missing something? Comb example code … and nope. OK, I am frustrated as all get out. In the meantime my irritation grows when I see example code like the following.
override def insert(rows: Seq[A]): Future[Seq[Long]] = {
db.run(tableQ returning tableQ.map(_.id) ++= rows.filter(_.isValid))
}
Good god, how to decode that mess. Obviously run is taking something as a parameter but exactly what is the order of operations here? Anyway, I digress.
Back to the subject at hand, I am at a complete loss for how to explain the problem here so, I decide to create a post on the Slick forums to figure out what I am doing wrong. I think, for simplicity sake lets combine all of our classes into one file.
package persistence.entities
import java.time.Instant
import slick.driver.H2Driver.api._
import slick.jdbc.JdbcProfile
import slick.lifted.{Query, TableQuery}
import scala.concurrent.Future
case class User(id: Long, name: String)
class UserTable(tag: Tag) extends Table[User](tag, "user") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def * = (id, name) <> (User.tupled, User.unapply)
}
object Users {
val users = TableQuery[UserTable]
trait UserDAL {
def findById(id: Long) : Future[Option[User]]
}
class UserDALImpl(implicit val db: JdbcProfile#Backend#Database) extends UserDAL {
override def findById(userId: Long) = null
def foo (userId : Long) = {
val filter: Query[UserTable, User, Seq] = Tables.users.filter(_.id == userId)
db.run(filter.result)
}
}
}
OK, now that we have combined them we are ready to post this … but wait … it compiles now. What The Fork? Oh wait. There was an implicit conversion going on in there somewhere. There was something in the new file converting our query type into something that had the result method predefined. Magically, behind the scenes and with the right import it doesn’t happen and the code doesn’t compile.After experimentally deleting imports i discover it’s import slick.driver.H2Driver.api._
that is the home of the implicit conversion. So I go look at the file and nope, no implicit. I drill and drill and finally find it. Wonderful. 7+ hours wasted for this. The fun part is the implicit conversion happened as a result of the implicit conversion of an implicit conversion of an implicit conversion. Do the designers of Scala think this is actually good? You are just supposed to “know” that the conversion of a conversion of a conversion is happening I guess. Or is the goal to make the code so cryptic and opaque that the source of the library is entirely unreadable?
Given my originalUsersDAL
file there was no way for me to know that to make the code compile I had to find some implicit conversion that could turn a Query
into a DBIOAction
. There is no means for you to know where this conversion is, what file has to be imported. All of the example code just assumes you must already know. Its fine copying the example code but if you modify, reorganize code into many files you break the delicately balanced Jenga tower and it doesn’t work.
So after playing around for a few days with this project, I can see that if I ever brought this code into a business environment my CEO would absolutely murder me as it would take 6 months to add the most basic of features, not to mention 1+ years to train a new developer to be even moderately productive. Once again, its the implicit and its use in the language that is at the core of the problem. It actually makes me sad, but it would be wholesale irresponsible of me as a Principal Software Architect to recommend these techs in a business that makes its money putting out product for users in a short time.
So why post this? Because otherwise i LIKE Scala and wish it could be different. But its becoming like the JDK, they have decided that is the direction they are going and no one on the planet can convince them otherwise. OK now that I am done stating my opinion, I am ready to get flamed again.
– Robert Simmons Jr.