Search code examples
scalashapelessscodec

Missing scodec.Codec[Command] implicit because of class with non-value fields


I'm trying to use discriminators in existing project and something is wrong with my classes I guess.

Consider this scodec example. If I change TurnLeft and its codec to

sealed class TurnLeft(degrees: Int) extends Command {
  def getDegrees: Int = degrees
}
implicit val leftCodec: Codec[TurnLeft] = uint8or16.xmap[TurnLeft](v => new TurnLeft(v), _.getDegrees)

I get

Error:(x, x) could not find Lazy implicit value of type scodec.Codec[Command]
    val codec: Codec[Either[UnrecognizedCommand, Command]] = discriminatorFallback(unrecognizedCodec, Codec[Command])

It all works if I make degrees field value field. I suspect it's something tricky with shapeless. What should I do to make it work ?

Sample project that demonstrates the issue is here.


Solution

  • shapeless's Generic is defined for "case-class-like" types. To a first approximation, a case-class-like type is one whose values can be deconstructed to it's constructor parameters which can then be used to reconstruct an equal value, ie.

    case class Foo ...
    val foo = Foo(...)
    val fooGen = Generic[Foo]
    assert(fooGen.from(fooGen.to(foo)) == foo)
    

    Case classes with a single constructor parameter list meet this criterion, whereas classes which don't have public (lazy) vals for their constructor parameters, or a companion with a matching apply/unapply, do not.

    The implementation of Generic is fairly permissive, and will treat (lazy) val members which correspond to constructor parameters (by type and order) as being equivalent to accessible constructor arguments, so the closest to your example that we can get would be something like this,

    sealed class TurnLeft(degrees: Int) extends Command {
      val getDegrees: Int = degrees
    }
    
    scala> Generic[TurnLeft]
    res0: shapeless.Generic[TurnLeft]{type Repr = Int :: HNil } = ...
    

    In this case getDegrees is treated as the accessor for the single Int constructor parameter.