Search code examples
scalaexistential-typeimplicits

How to express this type in Scala? Existential with type class (ie, implicit) restriction?


I'm using the Play framework's JSON library, which uses a type class to implement the Json.toJson function. (I may decide to use another technique with less static typing, like reflection; but for now I want to use this library because it's helping me learn the Scala type system.)

I have a bunch of simple case classes that need to be passed to toJson, so I have to implement an implicit Writes[T] object for each of them. A first cut might look like this, for each of the classes.

// An example class
case class Foo(title: String, lines: List[String])

// Make 'Foo' a member of the 'Writes' typeclass
implicit object FooWrites extends Writes[Foo] {
  def writes(f: Foo) : JsValue = {
    val fields = Seq("title" -> toJson(f.title), 
                     "lines" -> toJson(f.lines))                        
    JsObject(fields)
  }
}  

Each class will have a similar implicit value, so I could abstract the common part, as below. But this doesn't compile, because I'm not sure how to declare the type.

def makeSimpleWrites[C](fields: (String, C => T??)*) : Writes[C] = {
  new Writes[C] {
    def writes(c: C) : JsValue = {
      val jsFields = fields map { case (name, get) => (name, toJson(get(c)))}
      JsObject(jsFields)
    }
  }
}

implicit val fooWrites : Writes[Foo] = 
    makeSimpleWrites[Foo]("title" -> {_.title}, "lines" -> {_.lines})                                 
implicit val otherWrites ...

The issue is the type T that I want to pass to makeSimpleWrites. It can't be a normal type parameter because that T is different for each item in fields. Is this an existential type? I have yet to use one of these. Flailing at syntax...

def makeSimpleWrites[C](fields: (String, C=>T forSome { type T; implicit Writes[T] })*) 

Is this possible in Scala? If so, what is the syntax?


Solution

  • Because each field has a different type, you would need one type parameter per field. This is because to write these fields, you need to provide (implicitly) the Writes instances for the corresponding types (to method toJson), and those are resolved statically.

    One solution to work around this is to split the process in two parts: one method that you call for each field to extract the field accessor and pack it with the corresponding WriteS instance (this can even be maed an implicit conversion from the paairs that you are already passing), and one method that takes the whole and creates the final WriteS instance. Something like this (illustrative, untested):

    class WriteSFieldAccessor[C,T] private ( val title: String, val accessor: C => Any )( implicit val writes: Writes[T] )
    
    implicit def toWriteSFieldAccessor[C,T:Writes]( titleAndAccessor: (String, C => T) ): WriteSFieldAccessor = {
      new WriteSFieldAccessor[C,T]( titleAndAccessor._1, titleAndAccessor._2 )
    }
    def makeSimpleWrites[C](fields: WriteSFieldAccessor[C,_]*) : Writes[C] = {
      new Writes[C] {
        def writes(c: C) : JsValue = {
          val jsFields = fields map { f: WriteSFieldAccessor => 
            val jsField = toJson[Any](f.accessor(c))(f.writes.asInstanceOf[Writes[Any]])
            (f.title, jsField)
          }
          JsObject(jsFields)
        }
      }
    }
    
    // Each pair below is implicitly converted to a WriteSFieldAccessor  instance, capturing the required information and passing it to makeSimpleWrites
    implicit val fooWrites : Writes[Foo] = makeSimpleWrites[Foo]("title" -> {_.title}, "lines" -> {_.lines}) 
    

    The interesting part is toJson[Any](f.accessor(c))(f.writes..asInstanceOf[Writes[Any]]). You just pass Any as a the static type but explicitly pass the (normally implicit) Writes instance.