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?
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.
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))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With