Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 235
Rob N Avatar asked Dec 13 '12 18:12

Rob N


2 Answers

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.

like image 50
Régis Jean-Gilles Avatar answered Sep 28 '22 00:09

Régis Jean-Gilles


As of at least Jan 25 2015, play-json already has a built-in way of doing what you want:

import play.api.libs.json._
import play.api.libs.functional.syntax._

sealed case class Foo(title: String, lines: List[String])  // the `sealed` bit is not relevant but I always seal my ADTs

implicit val fooWrites = (
  (__ \ "title").write[String] ~
  (__ \ "lines").write[List[String]]
)(unlift(Foo.unapply))

in fact, this also works with Reads[T]

implicit val fooReads = (
  (__ \ "title").read[String] ~
  (-- \ "lines").read[List[String]]
)(Foo.apply _)

and Format[T]:

implicit val fooFormat = (
  (__ \ "title").format[String] ~
  (-- \ "lines").format[List[String]]
)(Foo.apply _, unlift(Foo.unapply))

you can also apply transforms, e.g.:

implicit val fooReads = (
  (__ \ "title").read[String].map(_.toLowerCase) ~
  (-- \ "lines").read[List[String]].map(_.filter(_.nonEmpty))
)(Foo.apply _)

or even 2-way transforms:

implicit val fooFormat = (
  (__ \ "title").format[String].inmap(_.toLowerCase, _.toUpperCase) ~
  (-- \ "lines").format[List[String]]
)(Foo.apply _, unlift(Foo.unapply))
like image 23
Erik Kaplun Avatar answered Sep 27 '22 22:09

Erik Kaplun