我正在使用Play框架的JSON库,它使用类型类来实现Json.toJson
function。 (我可能决定使用另一种静态类型较少的技术,比如反射;但是现在我想使用这个库,因为它帮助我学习了Scala类型系统。)
我有一堆简单的case类需要传递给toJson
,因此我必须为每个类实现一个隐式Writes[T]
对象。对于每个类,第一个剪切可能看起来像这样。
// 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)
}
}
每个类都有一个类似的隐含值,所以我可以抽象出公共部分,如下所示。但这不会编译,因为我不确定如何声明类型。
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 ...
问题是我要传递给T
的{{1}}类型。它不能是普通的类型参数,因为makeSimpleWrites
中的每个项目的T都不同。这是一种存在主义类型吗?我还没有使用其中之一。语法闪烁......
fields
这在Scala中是否可行?如果是这样,语法是什么?
答案 0 :(得分:3)
由于每个字段的类型不同,因此每个字段需要一个类型参数。这是因为要编写这些字段,您需要(隐式)提供相应类型的Writes
实例(方法toJson
),并且这些实例将被静态解析。
解决此问题的一个解决方案是将流程分为两部分:一个方法,您为每个字段调用以提取字段访问器并将其与相应的WriteS
实例打包(这甚至可以是一个来自您已经传递的pambrel的隐式转换),以及一个获取整体并创建最终WriteS
实例的方法。这样的事情(说明性的,未经测试的):
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})
有趣的部分是toJson[Any](f.accessor(c))(f.writes..asInstanceOf[Writes[Any]])
。您只需将Any
作为静态类型传递,但显式传递(通常是隐式的)Writes
实例。
答案 1 :(得分:1)
当试图解决限制时,我的第一个解决方案必须写"title" -> {s:Section => s.title}
而不是"title" -> {_.title}
,我用它修改了一下,只是一直在scala的推理限制中运行。
因此,我决定尝试从另一个角度解决它,并提出了一个完全不同的解决方案。
这基本上是一个准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")
我们的想法是,您逐步创建Writes
实例,并逐个使用新字段进行丰富。
唯一的烦恼是你需要.and
分隔符,包括点。没有点(即使用中缀表示法),编译器似乎再次混淆并抱怨我们只是(_.title)
而不是(s:Section => s.title)
。
答案 2 :(得分:1)
截至2015年1月25日,play-json已经有了内置的方式来做你想做的事情:
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))
实际上,这也适用于Reads[T]
implicit val fooReads = (
(__ \ "title").read[String] ~
(-- \ "lines").read[List[String]]
)(Foo.apply _)
和Format[T]
:
implicit val fooFormat = (
(__ \ "title").format[String] ~
(-- \ "lines").format[List[String]]
)(Foo.apply _, unlift(Foo.unapply))
您也可以应用变换,例如:
implicit val fooReads = (
(__ \ "title").read[String].map(_.toLowerCase) ~
(-- \ "lines").read[List[String]].map(_.filter(_.nonEmpty))
)(Foo.apply _)
甚至是双向变换:
implicit val fooFormat = (
(__ \ "title").format[String].inmap(_.toLowerCase, _.toUpperCase) ~
(-- \ "lines").format[List[String]]
)(Foo.apply _, unlift(Foo.unapply))