softwaremill / magnolia

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

Type classes for parameter are not lazy #466

Open steinybot opened 1 year ago

steinybot commented 1 year ago

I'm not sure why this is happening since they ought to be CallByNeed but it looks as though type classes are being summoned for case class parameters even if they are not used:

import magnolia1.{CaseClass, Derivation, SealedTrait}

trait Thing[A]

object Thing extends Derivation[Thing]:
  override def split[T](sealedTrait: SealedTrait[Thing, T]): Thing[T] = ???
  override def join[T](caseClass: CaseClass[Thing, T]): Thing[T] = ???

case class Foo(bar: String)

object Main extends App:
  Thing.derived[Foo]

Fails to compile with:

[error] -- Error: /Users/jason/src/bug-reports/src/main/scala/Main.scala:12:2 ----------
[error]  12 |  Thing.derived[Foo]
[error]     |  ^^^^^^^^^^^^^^^^^^
[error]     |  No given instance of type Thing[String] was found
[error]     |---------------------------------------------------------------------------
[error]     |Inline stack trace
[error]     |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]     |This location contains code that was inlined from impl.scala:86
[error]     |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]     |This location contains code that was inlined from impl.scala:86
[error]     |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]     |This location contains code that was inlined from impl.scala:86
[error]     |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]     |This location contains code that was inlined from impl.scala:86
[error]     |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]     |This location contains code that was inlined from impl.scala:86
[error]      ---------------------------------------------------------------------------
[error] one error found

Now obviously I wouldn't expect this to work since Derivation is not implemented. In my real use case I am only looking at certain fields within join and was surprised to get this error when I wasn't using the type class for a String parameter.

Reproduction: https://github.com/steinybot/bug-reports/tree/magnolia/typeclasses-too-eager

joroKr21 commented 1 year ago

That's pretty much a limitation of the library. It's assumed that the typeclasses are "regular" in the sense that when deriving for a case class, we need them for all fields and when deriving for a sealed trait we need them for all child classes. The CallByNeed is there really to support recursion rather than to avoid summoning a typeclass. E.g. for things like case class Nel[A](head: A, tail: Option[Nel[A]]). But the instances still need to be there for it to compile.

There is one workaround you could use to convert this from a compile time error to a runtime error. You could derive a wrapper typeclass instead:

trait OptionalThing[A] {
  def thing: Option[Thing[A]]
}

but then when you're using it, you would have to throw some kind of error when it's missing and you actually need it.

steinybot commented 1 year ago

That makes sense now that I think about how it is implemented. You would need to be able to filter the params before that code gets inlined but that isn't possible. The only thing I can think of is if it were implemented as a macro instead then perhaps we could override a method that returns an Expr for the params.

joroKr21 commented 1 year ago

I think that could work theoretically in Scala 3 with inline - but I think it's a totally new design of the library. Perhaps worth a try but it would be a breaking change.