← back

Traits and implicits

1 June 2016

Question

When we have something like

trait Json[T] {
  def json(t: T): JVal
}

object Json {
  def toJson[T](t: T)(implicit e: Json[T]) = e.json(t)

// other Json code

  implicit def arrJson[A : Json] = new Json[Array[A]] {
    def json(t: Array[A]): JVal = JArr(for (j <- t.toList)
                                       yield(Json.toJson(j)))
  }
}

What exactly is happening when I call toJson(j)? I see inside the definition of toJson it has an implicit e. I'm not sure how the e.json(t) is resolved, what e represents, and how it all works with the earlier trait declaration to actually create the Json instances.

Answer

TL;DR: In Haskell, the plumbing is done automatically for you, in Scala you write it yourself. (See the near 1-1 correspondence between the Haskell/Scala code.)

The implicit says that "the type T is json-able, but the definition of serialization is separate from the definition of T".

So, when you want to serialize a value, you first have to find the function that implements that operation for the type, which is what the implicit defs handle. Remember a trait will be implemented by some concrete object, and this thing has to be looked up/called. Behind the scenes, the companion object is passed an object that implements the trait and it calls the json method.

Imo, the problem with Scala typeclasses is that it exposes too much implementation detail.

It might be clearer in the equivalent Haskell, where this idea came from:

data JVal = ...

-- similar to trait Json[T]
class Json t where
  json :: t -> JVal

-- think of this as implicit def
-- Ints are serializable
instance Json Int where
  json i = JNum i

-- this says "if type a is serializable then a list of them is serializable also"
instance Json a => Json [a] where
  json vs = JArr (map json vs)

then:

json [1,2,3,4] ==> JArr [JNum 1, JNum 2, JNum 3, JNum 4]

Haskell implements this via "dictionary passing"1, i.e. imagine the typeclass instances being compiled into an object where each interface function is a method (Haskell compiles it into a record/hashmap like thing). Each instance (for a concrete type A) is a concrete record. The compiler does this transformation:

json :: Json a => a -> JVal

becomes

json :: JsonImplA -> A -> JVal

i.e. the function just extracts the relevant implementation from the record. The "implicit record argument" for a type a is filled in automatically by the compiler based on the actual type parameter (assuming you defined an instance for it).

To translate this to the equivalent meanings in Scala, the trait (class ...) defines the signature of an object that the complier should implicitly look up. The implicit defs (instance ...) implement that trait and make objects visible only to the compiler, that it can plug into places that expect implicit values at compile time. The Object Json thing is just boilerplate.