【问题标题】:How to express this type in Scala? Existential with type class (ie, implicit) restriction?如何在 Scala 中表达这种类型?存在类型类(即隐式)限制?
【发布时间】:2012-12-13 18:15:54
【问题描述】:

我正在使用 Play 框架的 JSON 库,它使用类型类来实现 Json.toJson function。 (我可能会决定使用另一种静态类型较少的技术,例如反射;但现在我想使用这个库,因为它可以帮助我学习 Scala 类型系统。)

我有一堆简单的案例类需要传递给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 ...

问题是我想传递给makeSimpleWrites 的类型T。它不能是普通类型参数,因为 fields 中的每个项目的 T 都不同。这是存在主义的类型吗?我还没有使用其中之一。在语法上挥舞...

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

这在 Scala 中可行吗?如果是这样,语法是什么?

【问题讨论】:

  • 鉴于 OP 明确提到 type classes ,我认为他至少在表面上知道这是什么,对吧?或者,也许您可​​以指出在这种情况下可能对他有帮助的类型类的具体细节?
  • 对了,我想我理解类型类模式了。
  • 确实很抱歉,我读到你的问题太快了......类型 T 的唯一限制是它必须有一个隐式类型 Writes[T] ?你可以使用def makeSimpleWrites[C, T : Writes](fields: (String, C=>T)*)

标签: scala existential-type implicits


【解决方案1】:

因为每个字段都有不同的类型,所以每个字段都需要一个类型参数。这是因为要编写这些字段,您需要(隐式)提供相应类型的 Writes 实例(到方法 toJson),并且这些实例是静态解析的。

解决此问题的一个解决方案是将流程分为两部分:一种方法是您为每个字段调用以提取字段访问器并将其与相应的 WriteS 实例打包(这甚至可以是隐式转换来自您已经通过的配对),以及一种方法,它采用整体并创建最终的 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 实例。

【讨论】:

  • 非常好的答案!我忘记了,这里我们每个字段都需要一个类型参数...
  • 啊哈,我想我明白为什么我需要像你一样打包隐式写入了。虽然现在我在编译它时遇到了一些麻烦。我添加了 (f.writes.asInstanceOf[Any]),但现在它不喜欢最后一行的匿名函数。 missing parameter type for expanded function ((x$1) => x$1.title)。稍后我会报告更多。
  • 这宁愿是f.writes.asInstanceOf[Writes[Any]]。我会更新我的答案。
  • 看来我必须写 "title" -> {s:Section => s.title} 而不是 "title" -> {_.title} 才能编译它。没什么大不了的,但你还有什么技巧可以解决这个问题吗?
  • 看看我的其他答案。
【解决方案2】:

在尝试解决我的第一个解决方案必须写 "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)

【讨论】:

    【解决方案3】:

    至少截至 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 _)
    

    甚至是 2 路转换:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-11-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-25
      • 2013-04-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多