【问题标题】:Implicit conversion of abstract type parameter in ScalaScala中抽象类型参数的隐式转换
【发布时间】:2012-12-27 20:24:14
【问题描述】:

我是 Scala 新手,正在探索隐式转换和著名蛋糕模式的可能性。我尝试创建将 id 值列为抽象类型的模型类,以避免泄漏实现细节。我还将它混合到蛋糕图案特征包装中。一切正常,除了从 id 到 JSON 的隐式转换(在 Play 框架内)。无论我做什么,Scala 编译器都找不到隐式转换。

这是重现问题的代码:

import anorm._
import play.api.libs.json._

trait Card {
  type Key
  val NoId: Key
  val id: Key
  val name: String
}

trait CardModelComponent {
  val cardModel: CardModel
  trait CardModel {
    def findById(id: Long): Option[Card]
    def findAll: Seq[Card]
    def delete(id: Long)
    def create(name: String): Option[Card]
  }
}

trait CardModelComponentImpl extends CardModelComponent {
  case class CardImpl(id: Pk[Long], name: String) extends Card {
    type Key = Pk[Long]

    object Key extends Writes[Key] {
      implicit def writes(key: Key): JsValue = {
        key match {
          case Id(idVal: Long) => JsNumber(idVal)
          case _ => JsNull
        }
      }
    }
    val NoId = NotAssigned
  }

  class CardModelImpl extends CardModel {
    def findById(id: Long): Option[Card] = { None }
    def findAll: Seq[Card] = { Seq(CardImpl(Id(1), "Some card"))}
    def delete(id: Long) {}
    def create(name: String): Option[Card] = { Some(CardImpl(Id(1), name)) }
  }
}

object ComponentsRegistry extends
CardModelComponentImpl {

  val cardModel = new CardModelImpl
}

val card = ComponentsRegistry.cardModel.create("Test card").get
Json.toJson(card.id)

我得到的错误输出如下:

> card: Card = CardImpl(1,Test card)
> <console>:19: error: No Json deserializer found for type card.Key. Try to implem
  ent an implicit Writes or Format for this type.
                Json.toJson(card.id)
                           ^

有什么方法可以让它工作吗?看起来蛋糕模式包装对编译器隐藏了太多类型信息,正如我从 card.Key 类型名称中猜测的那样。

我还尝试直接为 Pk 创建 Writer 实现,结果与错误相同。

【问题讨论】:

    标签: scala playframework playframework-2.0


    【解决方案1】:

    应该是隐式的 Writes[Key] 实例,而不是 writes() 成员方法。隐式实例也必须在调用toJson() 方法的范围内。

    【讨论】:

    • 我尝试了@asflierl 的建议,它奏效了。但是,是否可以避免在范围内显式导入 card.Key 对象?
    【解决方案2】:

    就像 Jesper Nordenberg 所写的那样,您必须将 Writes[Key] 的实例纳入范围。一种方法是要求 Card 的实现发布 Writer 实例,然后将其导入到您调用 toJson 的任何位置,如下所示:

    ...

    trait Card {
      type Key
    
      val NoId: Key
      val id: Key
      val name: String
      implicit val Key: Writes[Key]
    }
    

    ...

    trait CardModelComponentImpl extends CardModelComponent {
      case class CardImpl(id: Pk[Long], name: String) extends Card {
        type Key = Pk[Long]
    
        implicit object Key extends Writes[Key] {
          def writes(key: Key): JsValue = {
            key match {
              case Id(idVal: Long) => JsNumber(idVal)
              case _ => JsNull
            }
          }
        }
    
        val NoId = NotAssigned
      }
    
      class CardModelImpl extends CardModel {
        def findById(id: Long): Option[Card] = { None }
        def findAll: Seq[Card] = { Seq(CardImpl(Id(1), "Some card"))}
        def delete(id: Long) {}
        def create(name: String): Option[Card] = { Some(CardImpl(Id(1), name)) }
      }
    }
    

    ...

    val card = ComponentsRegistry.cardModel.create("Test card").get
    import card.Key
    Json.toJson(card.id)
    

    或显式传递:

    val card = ComponentsRegistry.cardModel.create("Test card").get
    Json.toJson(card.id)(card.Key)
    

    【讨论】:

    • 谢谢!那行得通。但是,在这种情况下,有什么办法可以避免“进口税”?我想避免消费者代码制作这种import card.Key 语句。
    • 我想搭载伴随对象的隐式导入,但它似乎不适用于所有这些抽象。
    • 问题是这里的隐含范围相当小。这是我理解的推理:寻找的类型是Writes[Key],它扩展为Writes[Pk[Long]]。所以只搜索WritesPkLong的同伴(如果有的话)。这些都不在你的控制之下,所以你不走运。潜在的设计问题可能是您无法隐藏实现的类型并同时隐式使用它们;那种自相矛盾(至少在我的脑海里=^.^=)
    【解决方案3】:

    我稍微修改了您的代码。我希望这对你有帮助。

    package models
    import anorm._
    import play.api.libs.json._
    
    trait Card[T] {
      val NoId: T
      val id: T
      val name: String
    }
    
    trait CardModelComponent[T] {
      val cardModel: CardModel
      trait CardModel {
        def findById(id: Long): Option[Card[T]]
        def findAll: Seq[Card[T]]
        def delete(id: Long)
        def create(name: String): Option[Card[T]]
      }
    }
    
    trait CardModelComponentImpl extends CardModelComponent[Pk[Long]] {
      type Key = Pk[Long]
      case class CardImpl(id: Key, name: String) extends Card[Key] {
        val NoId = NotAssigned
      }
    
    
      class CardModelImpl extends CardModel {
        def findById(id: Long): Option[Card[Key]] = { None }
        def findAll: Seq[Card[Key]] = { Seq(CardImpl(Id(1), "Some card"))}
        def delete(id: Long) {}
        def create(name: String): Option[Card[Key]] = { Some(CardImpl(Id(1), name)) }
      }
    }
    
    object ComponentsRegistry extends CardModelComponentImpl {
      val cardModel = new CardModelImpl
      implicit object KeyWrites extends Writes[Key] {
        def writes(key: Key): JsValue = {
          key match {
            case Id(idVal: Long) => JsNumber(idVal)
            case _ => JsNull
          }
        }
      }
    }
    

    然后你可以这样使用:

    import models.ComponentsRegistry
    import models.ComponentsRegistry.KeyWrites
    val card = ComponentsRegistry.cardModel.create("Test card").get
    Json.toJson(card.id)
    

    【讨论】:

    • 我不确定将抽象类型更改为类型参数是否会改变任何内容。如果我错了,请纠正我,但重要的是显式导入 KeyWrites 实例,不是吗?
    • 更改摘要只是重构。您需要明确导入 KeyWrites。 This 可能会有所帮助。
    • 关于与原问题无关的话题,为什么你更喜欢类型参数而不是抽象类?我首先尝试了这种方法,并认为它不如基类中的抽象类型方便。我不喜欢必须到处传递类型参数。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-01
    • 2016-03-12
    • 1970-01-01
    • 1970-01-01
    • 2015-02-13
    相关资源
    最近更新 更多