Search code examples
jsonscalagenericscompanion-object

Passing generic companion object to super constructor


I'm trying to construct a trait and an abstract class to subtype by messages (In an Akka play environment) so I can easily convert them to Json.

What have done so far:

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


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

Problem is, when I'm trying to implement the mentioned interfaces as follows:

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

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

I get this error from Intellij: Type mismatch, expected: OutputMessageCompanion[OutputMessage], actual: NotifyTableChange.type

I'm kinda new to Scala generics - so help with some explanations would be much appreciated.

P.S I'm open for any more generic solutions than the one mentioned. The goal is, when getting any subtype of OutputMessage - to easily convert it to Json.


Solution

  • The compiler says that your companion is defined over the OutputMessage as the generic parameter rather than some specific subtype. To work this around you want to use a trick known as F-bound generic. Also I don't like the idea of storing that companion object as a val in each message (after all you don't want it serialized, do you?). Defining it as a def is IMHO much better trade-off. The code would go like this (companions stays the same):

    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
    }
    

    You may also see standard Scala collections for an implementation of the same approach.

    But if all you need the companion for is to encode with JSON format, you can get rid of it like this:

      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]
      }
    

    Obviously is you also want to decode from JSON you still need a companion object anyway.


    Answers to the comments

    1. Referring the companion through a def - means that is a "method", thus defined once for all the instances of the subtype (and doesn't gets serialized)?

    Everything you declare with val gets a field stored in the object (instance of the class). By default serializers trying to serialize all the fields. Usually there is some way to say that some fields should be ignored (like some @IgnoreAnnotation). Also it means that you'll have one more pointer/reference in each object which uses memory for no good reason, this might or might not be an issue for you. Declaring it as def gets a method so you can have just one object stored in some "static" place like companion object or build it on demand every time.

    1. I'm kinda new to Scala, and I've peeked up the habit to put the format inside the companion object, would you recommend/refer to some source, about how to decide where is best to put your methods?

    Scala is an unusual language and there is no direct mapping the covers all the use cases of the object concept in other languages. As a first rule of thumb there are two main usages for object:

    1. Something where you would use static in other languages, i.e. a container for static methods, constants and static variables (although variables are discouraged, especially static in Scala)

    2. Implementation of the singleton pattern.

    1. By f-bound generic - do you mean the lower bound of the M being OutputMessage[M] (btw why is it ok using M twice in the same expr. ?)

    Unfortunately wiki provides only a basic description. The whole idea of the F-bounded polymorphism is to be able to access to the type of the sub-class in the type of a base class in some generic manner. Usually A <: B constraint means that A should be a subtype of B. Here with M <: OutputMessage[M], it means that M should be a sub-type of the OutputMessage[M] which can easily be satisfied only by declaring the child class (there are other non-easy ways to satisfy that) as:

    class Child extends OutputMessage[Child}
    

    Such trick allows you to use the M as a an argument or result type in methods.

    1. I'm a bit puzzled about the self bit ...

    Lastly the self bit is another trick that is necessary because F-bounded polymorphism was not enough in this particular case. Usually it is used with trait when traits are used as a mix-in. In such case you might want to restrict in what classes the trait can be mixed in. And at the same type it allows you to use the methods from that base type in your mixin trait.

    I'd say that the particular usage in my answer is a bit unconventional but it has the same twofold effect:

    1. When compiling OutputMessage the compiler can assume that the type will also somehow be of the type of M (whatever M is)

    2. When compiling any sub-type compiler ensures that the constraint #1 is satisfied. For example it will not let you to do

    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]
    

    Actually since I had to use self:M anyway, you probably can remove the F-bounded part here, living just

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

    but I'd stay with it to better convey the meaning.