softwaremill / magnolia

Easy, fast, transparent generic derivation of typeclass instances
https://softwaremill.com/open-source/
Apache License 2.0
768 stars 117 forks source link

StackOverflowException for contra-variant type-class #464

Open alexandru opened 1 year ago

alexandru commented 1 year ago

Hi all,

I have this typeclass that's contravariant in its type parameter (sorry for any compilation errors, I had to modify the code in-place):

trait LogShow[-A] {
  def logShow(a: A): String
}

object LogShow {
  type Typeclass[-T] = LogShow[T]

  def join[T](ctx: CaseClass[LogShow, T]): LogShow[T] =
    (value: T) => {
      ctx.parameters.foldLeft("") {
        case (str, p) =>
          val child = p.typeclass.logShow(p.dereference(value))
          str + "; " + child
      }
    }

  def split[T](ctx: SealedTrait[LogShow, T]): LogShow[T] =
    (value: T) => {
      ctx.split(value) { sub =>
        sub.typeclass.logShow(sub.cast(value))
      }
    }

  def derive[T]: LogShow[T] =
    macro Magnolia.gen[T]
}

Then I have the following test:

sealed trait Entity

object Entity {
  final case class User(
    name: String,
    age: Int,
    email: String,
  ) extends Entity

  final case class Robot(
    id: String,
    description: String
  ) extends Entity

  implicit lazy val logShow: LogShow[Entity] =
    LogShow.derive[Entity]
}

Invoking this logShow leads to a stack-overflow error.

It makes sense to me because the auto-derivation logic looks for instances of LogShow[Robot] or LogShow[User], which are hitting the same lazy val.

Are there some tricks I could use here to deal with variance like this?

adamw commented 1 year ago

Hm good question ... not sure if that's possible. As I understand you'd like to invoke derivation for the leaves here. But how should magnolia know if in a specific case it should use the available implicit, or invoke its own derivation?

Probably not very useful, but I think explicitly specifying the implicits for the leaves might work:

implicit lazy val logShow: LogShow[Robot] = LogShow.derive[Robot]
implicit lazy val logShow: LogShow[User] = LogShow.derive[User]
implicit lazy val logShow: LogShow[Entity] = LogShow.derive[Entity]

though I haven't tested this