将通用伴随对象传递给超级构造函数

时间:2019-01-21 16:01:29

标签: json scala generics companion-object

我正在尝试构造一个trait和一个abstract class以通过消息进行子类型化(在Akka播放环境中),以便我可以轻松地将它们转换为Json

到目前为止已完成的工作:

    abstract class OutputMessage(val companion: OutputMessageCompanion[OutputMessage]) {
        def toJson: JsValue =  Json.toJson(this)(companion.fmt)
    }


    trait OutputMessageCompanion[OT] {
        implicit val fmt: OFormat[OT]
    }

问题是,当我尝试实现上述接口时,如下所示:

    case class NotifyTableChange(tableStatus: BizTable) extends OutputMessage(NotifyTableChange)

    object NotifyTableChange extends OutputMessageCompanion[NotifyTableChange] {
        override implicit val fmt: OFormat[NotifyTableChange] = Json.format[NotifyTableChange]
    }

我从Intellij收到此错误: Type mismatch, expected: OutputMessageCompanion[OutputMessage], actual: NotifyTableChange.type

我对Scala泛型有些陌生-因此,请您提供一些解释的帮助。

P.S我愿意提供比上述解决方案更多的通用解决方案。 目标是在获取OutputMessage的任何子类型时-轻松将其转换为Json

2 个答案:

答案 0 :(得分:4)

编译器说您的companion是在OutputMessage上定义为通用参数而不是某些特定子类型的。要变通解决此问题,您想使用一个称为F-bound generic的技巧。另外,我也不喜欢在每个消息中将同伴对象存储为val的想法(毕竟,您不希望序列化它,对吗?)。恕我直言,将其定义为def是更好的折衷方案。代码将像这样(伴侣保持不变):

abstract class OutputMessage[M <: OutputMessage[M]]() {
    self: M => // required to match Json.toJson signature

    protected def companion: OutputMessageCompanion[M]

    def toJson: JsValue =  Json.toJson(this)(companion.fmt)
}

case class NotifyTableChange(tableStatus: BizTable) extends OutputMessage[NotifyTableChange] {

    override protected def companion: OutputMessageCompanion[NotifyTableChange] = NotifyTableChange
}

您可能还会看到标准Scala集合,以实现相同方法。

但是如果您只需要companion来使用JSON格式进行编码,就可以像这样摆脱它:

  abstract class OutputMessage[M <: OutputMessage[M]]() {
    self: M => // required to match Json.toJson signature

    implicit protected def fmt: OFormat[M]

    def toJson: JsValue = Json.toJson(this)
  }

  case class NotifyTableChange(tableStatus: BizTable) extends OutputMessage[NotifyTableChange] {

    override implicit protected def fmt: OFormat[NotifyTableChange] = Json.format[NotifyTableChange]
  }

很明显,您还想从JSON解码,但仍然仍然需要一个伴随对象。


评论的答案

  
      
  1. 通过def引用同伴-是一个“方法”,因此为子类型的所有实例定义一次(并且不会序列化)吗?
  2.   

您用val声明的所有内容都会得到一个存储在对象中的字段(类的实例)。默认情况下,序列化程序尝试序列化所有字段。通常,有一些说法可以忽略某些字段(例如某些@IgnoreAnnotation)。这也意味着在没有充分理由使用内存的每个对象中,您将再有一个指针/引用,这对您而言可能是问题,也可能不是问题。将其声明为def是一种方法,因此您可以将一个对象存储在诸如伴侣对象之类的“静态”位置,或者每次都按需构建它。

  
      
  1. 我对Scala还是很陌生,我已经习惯了将格式放在伴随对象中的习惯,您会推荐/参考一些有关如何确定将方法最好放置在哪里的资料吗? / li>   

Scala是一种不常见的语言,没有直接映射其他语言中object概念的所有用例。首先,object有两种主要用法:

  1. 您可以在其他语言中使用static的地方,即用于存放静态方法,常量和静态变量的容器(尽管不建议使用变量,尤其是Scala中的静态变量)

  2. 单例模式的实现。

  
      
  1. 通过f绑定泛型-您是说M的下限是OutputMessage [M](顺便问一下为什么在同一exp中两次使用M可以吗?)
  2.   

不幸的是,Wiki仅提供基本描述。 F界多态性的整个思想是能够以某种通用方式访问基类类型的子类类型。通常,A <: B约束意味着A应该是B的子类型。这里的M <: OutputMessage[M]表示M应该是OutputMessage[M]的子类型,只有声明子类才可以很容易地满足它(还有其他一些不容易的方法可以满足该):

class Child extends OutputMessage[Child}

这种技巧使您可以将M用作方法中的参数或结果类型。

  
      
  1. 我对自己的自我有点困惑...
  2.   

最后self位是另一个必要的技巧,因为在这种特殊情况下,F界多态性还不够。当特征用作mix-in时,通常与trait一起使用。在这种情况下,您可能希望限制特征可以混入哪些类中。并且在同一类型中,它允许您在mixin trait中使用该基本类型中的方法。

我会说答案中的特定用法有点不合常规,但具有双重效果:

  1. 在编译OutputMessage时,编译器可以假定该类型也将是M的类型(无论M是什么)

  2. 在编译任何子类型的编译器时,请确保满足约束#1。例如,它不会让您做

case class SomeChild(i: Int) extends OutputMessage[SomeChild]

// this will fail because passing SomeChild breaks the restriction of self:M
case class AnotherChild(i: Int) extends OutputMessage[SomeChild]

实际上,由于无论如何我都必须使用self:M,所以您可能可以删除这里的F边界部分,而只能居住

abstract class OutputMessage[M]() {
    self: M =>
     ...
}

但我会坚持以更好地传达其含义。

答案 1 :(得分:3)

正如SergGr已经回答的那样,您需要一种F界多态性来解决此问题。
但是,对于这些情况,我相信(请注意,这只是我的意见)最好改用Typeclasses

对于您而言,只要它们具有toJson类的实例,就只想为任何值提供OFormat[T]方法。
您可以通过这段(更简单的IMHO)代码来实现。

object syntax {
  object json {
    implicit class JsonOps[T](val t: T) extends AnyVal {
      def toJson(implicit: fmt: OFormat[T]): JsVal = Json.toJson(t)(fmt)
    }
  }
}

final case class NotifyTableChange(tableStatus: BizTable)

object NotifyTableChange {
  implicit val fmt: OFormat[NotifyTableChange] = Json.format[NotifyTableChange]
}

import syntax.json._
val m = NotifyTableChange(tableStatus = ???)
val mJson = m.toJson // This works!

JsonOps类是一个Implicit Class,它将为范围中存在隐式toJson实例的任何值提供OFormat方法。
并且由于NotifyTableChange类的 companion对象定义了这种隐式,因此它始终在范围内-有关where does scala look for implicits in this link的更多信息。
此外,由于它是Value Class,因此此扩展方法不需要在运行时中进行任何实例化。

Here,您可以找到有关 F边界 Typeclasses 的更详细的讨论。