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

Magnolia macro doesn't "use" instances with path-dependent types #450

Closed ashleymercer closed 1 year ago

ashleymercer commented 1 year ago

I've found an interesting case where magnolia needs an implicit to be in scope for a member variable in a case class, but the compiler complains that the implicit is not used. Commenting the implicit out causes magnolia to fail (as expected).

The slightly strange thing about this construction is that the implicit for the member variable lives in another object which uses type members - I wonder if this is some issue in magnolia (my macro-fu is very basic at best) or possibly even in the compiler.

import magnolia1.{CaseClass, Magnolia}
import scala.language.experimental.macros

//
// Ordinary Show example, nothing to see here :)
//
trait Show[A] {
  def apply(a: A): String
}

object Show {
  def join[A](cc: CaseClass[Show, A]): Show[A] =
    (a: A) =>
      cc.typeName.short +
      cc.parameters
        .map { p => p.typeclass(p.dereference(a)) }
        .mkString("(", ", ", ")")

  type Typeclass[A] = Show[A]
  implicit def gen[A]: Show[A] = macro Magnolia.gen[A]
}

//
// Because Reasons my code holds my typeclass 
// instances in a trait which uses type members
//
trait ShowHolder {
  type A
  val show: Show[A]
}

object ShowHolder {
  def apply[A0](showA0: Show[A0]): ShowHolder =
    new ShowHolder {
      override type A = A0
      override val show: Show[A] = showA0
    }
}

//
// Now try to use ShowHolder
//
// - if you leave `bringIntoScope` where it is, the
//   compiler complains that it's unused
//
// - if you comment it out, obviously `Show.gen`
//   fails because there's no instance for `holder.A`
//
object ShowMain extends App {
  def showForTuple2(holder: ShowHolder) = {
    implicit val bringIntoScope: Show[holder.A] = holder.show
    Show.gen[(holder.A, holder.A)]
  }
}

Since I have -Xfatal-warnings enabled, the only way I can get this to work is to mark my implicits @unused (even though they're not).

Scala version: 2.13.8 and 2.13.10 (both tested) Magnolia version: magnolia1_2 % 1.1.2

joroKr21 commented 1 year ago

The reason is that macros are a bit special when it comes to warnings because they generate code and they might use code from their scope as you already noticed. That means that there are potentially two places where a warning can come from - your own code (before the macro is expanded) or the generated code (after the macro is expanded). Neither is ideal. If you warn before expanding the macro you might get an unused warning for code the macro needs (like your case). If you warn after expanding the macro, you might get a warning from the generated code and as a user there is not much you can do apart from filing an issue to the macro authors.

Because neither choice works for everyone the Scala compiler has an option to control it:

Another way to resolve your issue is to annotate that implicit in question with @unused

ashleymercer commented 1 year ago

Ahh I hadn't seen that compiler option before #til

Can confirm that switching to -Wmacros:after removes the problem (although as you say, comes with its own risks) so I'll have to think about what approach to take in our build.

I had thought I'd seen a case where the equivalent but with a type parameter instead of a type member (i.e. ShowHolder[A] in the example above) didn't display this issue - so thought / assumed it might be a possible issue in magnolia (or even the compiler) - but I now can't reproduce that behaviour, so I'll go ahead and close this.

Thanks for your help :)