简体   繁体   English

如何在Scala中表达这种类型?存在类型类(即隐式)限制?

[英]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 . 我正在使用Play框架的JSON库,它使用类型类来实现Json.toJson函数 (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.) (我可能决定使用另一种静态类型较少的技术,比如反射;但是现在我想使用这个库,因为它帮助我学习了Scala类型系统。)

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. 我有一些需要传递给toJson的简单case类,所以我必须为它们中的每一个实现一个隐式的Writes[T]对象。 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 . 问题是我要传递给makeSimpleWrites的类型T It can't be a normal type parameter because that T is different for each item in fields . 它不能是普通的类型参数,因为fields每个项目的T都不同。 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? 这可能在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. 这是因为要编写这些字段,您需要(隐式)提供相应类型的Writes实例(方法toJson ),并且这些实例是静态解析的。

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. 解决这个问题的一个解决方案是将过程拆分为两部分:一个方法,您为每个字段调用以提取字段访问器并使用相应的WriteS实例打包它(这甚至可以从您的桌面进行隐式转换)已经传递了),并且有一个方法可以获取整个并创建最终的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]]) . 有趣的部分是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. 您只需将Any作为静态类型传递, 显式传递(通常是隐式的) Writes实例。

When trying to address the restriction that with my first solution one has to write "title" -> {s:Section => s.title} instead of "title" -> {_.title} , I tinkered a bit with it, only to run in scala's inference limitation all the time. 当试图解决限制时,我的第一个解决方案必须写"title" -> {s:Section => s.title}而不是"title" -> {_.title} ,我用它修改了一下,只是一直在scala的推理限制中运行。 So I decided to try to tackle it from another angle and came with a completly different solution. 因此,我决定尝试从另一个角度解决它,并提出了一个完全不同的解决方案。 This is basically a quasi-DSL: 这基本上是一个准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 Writes instance step by step, and enrich it with new fields one by one. 这个想法是你逐步创建你的Writes实例,并逐一用新的字段来丰富它。 The only annoyance is that you do need the .and separator, including the dot. 唯一的烦恼是你需要.and分隔符,包括点。 Without the dot (that is, using infix notation), the compiler seems to be confused again and complains if we just do (_.title) instead of (s:Section => s.title) . 如果没有点(也就是使用中缀表示法),编译器似乎再次混淆并抱怨如果我们只是(_.title)而不是(s:Section => s.title)

As of at least Jan 25 2015, play-json already has a built-in way of doing what you want: 截至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))

in fact, this also works with Reads[T] 事实上,这也适用于Reads[T]

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

and Format[T] : Format[T]

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

you can also apply transforms, eg: 你也可以应用变换,例如:

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))

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM