How to express this type in Scala? Existential with class type (i.e. implicit) constraint?

I use the JSON library for the Play platform, which uses a type class to implement the Json.toJson function. (I may decide to use another technique with less static typing, such as reflection, but for now I want to use this library because it helps me learn about 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. The first section may 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 general part as shown below. But this does not compile because I'm not sure how to declare a 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 problem is the type T , which I want to pass to makeSimpleWrites . It cannot be a parameter of a normal type, because T is different for each element in fields . Is this an existential type? I have not used one of them yet. Syntax interception ...

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

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

+6
source share
3 answers

Since each field has a different type, you will need one type parameter for each field. This is because in order to write these fields you need to provide (implicitly) instances of Writes for the corresponding types (for the toJson method), and they will be resolved statically.

One solution to this is to split the process into two parts: one method that you call for each field to extract the field accessory and pack it with the corresponding Writes instance (this may even be caused by an implicit conversion from past paairs) and one method that takes an integer and creates the final instance of Writes . 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 simply pass Any as a static type , but explicitly pass an instance of the <usually> implicit Writes .

+3
source

When I tried to eliminate the restriction, which, using my first solution, I need to write "title" -> {s:Section => s.title} instead of "title" -> {_.title} , I worked a bit with it, but I kept restriction on scala output. So I decided to try to solve it from a different angle and came up with a completely different solution. This is basically quasi-DSL:

 class ExpandableWrites[C]( val fields: Vector[(String, C => Any, Writes[_])] ) extends Writes[C] { def and[T:Writes](fieldAccessor: C => T)(fieldName: String): ExpandableWrites[C] = { new ExpandableWrites( fields :+ (fieldName, fieldAccessor, implicitly[Writes[T]]) ) } def writes(c: C) : JsValue = { val jsFields = fields map { case (name, get, writes) => (name, toJson[Any](get(c))(writes.asInstanceOf[Writes[Any]]) )} JsObject(jsFields) } } class UnaryExpandableWritesFactory[C] { def using[T:Writes](fieldAccessor: C => T)(fieldName: String): ExpandableWrites[C] = { new ExpandableWrites[C]( Vector( (fieldName, fieldAccessor, implicitly[Writes[T]] ) ) ) } } def makeSimpleWritesFor[C] = new UnaryExpandableWritesFactory[C] implicit val fooWrites : Writes[Foo] = makeSimpleWritesFor[Foo].using(_.title)("title") .and (_.lines)("lines") .and (_.date)("date") 

The idea is that you create your instance of Writes step by step and enrich it with new fields one by one. The only nuisance is that you need a .and delimiter, including a period. Without a dot (i.e. using infix notation), the compiler gets confused again and complains if we just do (_.title) instead of (s:Section => s.title) .

+1
source

As of January 25, 2015, play-json already has a built-in way to do 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)) 

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, for example:

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

or even two-way conversions:

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

All Articles