In this post I want to rant about how Scala’s implicits harm discoverability.
Let’s start with the following. Does it compile?
object example {
def foo(x: Int): String = x.toString
val x = foo(1) // We have no doubts that this will print "1"
}
What about this?
object example2 {
def foo(x: Int): String = x.toString
import my.magic.pony._
val x = foo("1") // Should this print "1"?
}
The answer is maybe. Scala searches the scope implicitly for suitable converters to convert String to Int so it depends on whether there exists a function in my.magic.pony
with the signature String => Int
. As a segue, this necessitates that such implicit functions are unambiguous.
object ambiguous {
implicit def foo(x: Int): String = x.toString
implicit def bar(x: Int): String = x.toString + "me"
def car(x: String) = x
def main(args: Array[String]): Unit = {
car(1)
}
}
Compiling the above will give the following error, which makes sense because how would the compiler know which implicit is preferred.
Error:(8, 9) type mismatch;
found : Int(1)
required: String
Note that implicit conversions are not applicable because they are ambiguous:
both method foo in object ambiguous of type (x: Int)String
and method bar in object ambiguous of type (x: Int)String
are possible conversion functions from Int(1) to String
car(1)
The main uses for implicits that I have seen have been about passing context arounds and for implimenting type classes.
For example, instead of always having to define a function that takes in a context, we can make the context implicit so that the caller just has to define the context once.
def getNameForId(id: Int, context: Context): Name = ???
def getNameForIdImplicit(id: Int)(implicit context: Context): Name = ???
In the latter case, the caller can focus on what’s important - providing the id.
What I dislike about this implicit paradigm is discoverability. If you have a dumb type system, then a function Int => Int
would only take Int. In the name of genericity, functions might be defined as Foo-able => Bar
. That would be fine if the caller knows how to create a Foo-able
. For example, if it is a class, then the caller has to just instantiate the class. If it is an interface, the caller can rely on the IDE to tell it possible implementations to instantiate. That’s all nice and dandy. Then one comes to type classes. This adds another layer of abstraction, and with it discoverability. Take the typical example of implementing JSON serialization in typical scala introduction books. How does one know if a FooJsWriter
exists? Do you just expect it to exist? What would you search to get a definite answer for whether it exists?
Case study: I was trying to use akka-http to do a get request and read the response. I just want to consume the body of the response as a string. However, the response is of type HttpResponse
and there are no obvious methods on that type that tells me how to extract the body.
After reading the docs, I realize I should be unmarshalling the HttpResponse
which makes sense.
val x: HttpResponse = ??? // The response I get
val y = Unmarshal(x).to[String] // Would this work?
Turns out the above does not compile. Because the signature of to
is:
def to[B](implicit um: Unmarshaller[A, B], ...): Future[B]
Now how do I create one of these Unmarshaller[A,B]
? I see that it is a trait. But Ctrl-H in intellij does not show its implementors (even after choosing scope to all)! Even after relying on Find usages
alt-7, I find implementations of String => X
but those are not useful. Also note that this exercise requires one to reify the types A and B to Unmarshaller[HttpResponse, String]
. Now we expect that this task of unmarshalling the response to string to be common. But which damn import is needed to bring that unmarshaller into scope? Eventually, after sinking hours trying to find a decent example to do a http request, I realized I should be unmarshalling the HttpEntity
inside the response rather than the response itself. Then, in the magical akka.http.scaladsl.unmarshalling
package I will find implicit def stringUnmarshaller: FromEntityUnmarshaller[String]
that does the job for me.
I hope my plight above illustrates the frustration implicits can bring to a library user.