justinpatterson
justinpatterson

Reputation: 85

Shouldn't an implicit parameter in scope be available as an implicit argument for macro-generated code?

I'm trying to make an existing macro that generates typeclass instances work for parameterized types, where the type argument already has an instance of the typeclass. I'm surprised that it fails to resolve the existing (type argument's) typeclass in this case, but that appears to be what's happening.

I've tried to pare it down to a small example that exhibits the behavior. Here's the macro definition:

package test

import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context

trait Read[A] {
  def read(in: String): A
}

object Read {
  def CaseClassReadImpl[A: c.WeakTypeTag](c: Context): c.Expr[Read[A]] = {
    import c.universe._
    val aType = weakTypeOf[A]

    val params = aType.decls.collect {
      case m: MethodSymbol if m.isCaseAccessor => m
    }.toList

    val paramList = params.map(param => q"Read.read[${param.typeSignature}](in)")

    val src = q"""
      new Read[$aType] {
        def read(in: String) = ${aType.typeSymbol.companion}.apply(..$paramList)
      }
    """

    c.Expr[Read[A]](src)
  }

  def readFor[A]: Read[A] = macro CaseClassReadImpl[A]

  def read[A](in: String)(implicit A: Read[A]): A = A.read(in)
}

Here's the code that exercises it:

package test

object MacroTest {
  case class Foo[A](bar: A)

  implicit def fooRead[A](implicit A: Read[A]): Read[Foo[A]] =
    Read.readFor[Foo[A]]
}

I was expecting this to succeed, thinking that the implicit parameter to the generated call to Read.read would resolve to the implicit argument to the fooRead function. Instead, it fails when defining fooRead with:

Error:(7, 17) could not find implicit value for parameter A: test.Read[A]
    Read.readFor[Foo[A]]

Why is it not using the implicit parameter A to fooRead? It's in lexical scope.

I realize that I should convert this codebase to use shapeless or some such library, but right now I'm just trying to get it working with as little effort as possible.


Update:

I've figured out that the problem derives from having two distinct As. The error above makes it look like the required implicit is the same type as the one I have in scope, but after fiddling with it a bit and passing the implicit in explicitly, I managed to get this (much more helpful) error message:

Error: type mismatch;
 found   : A(in class Foo)
 required: A(in method fooRead)
  implicit def fooRead[A](implicit read: Read[A]): Read[Foo[A]] = Read.readFor[Foo[A]]

I still haven't been able to find a way to get Scala to understand that I want to the As to be the same: the one passed into the fooRead function.

Upvotes: 1

Views: 153

Answers (1)

Dmytro Mitin
Dmytro Mitin

Reputation: 51703

Try to replace

param => q"Read.read[${param.typeSignature}](in)"

with

param => q"Read.read[${param.typeSignatureIn(aType)}](in)"

https://github.com/scala/scala/blob/2.13.x/src/reflect/scala/reflect/api/Symbols.scala#L335-L345

/** @see [[infoIn]] */
def typeSignatureIn(site: Type): Type

/** The type signature of this symbol seen as a member of given type `site`.
 *
 *  @group Basics
 */
def infoIn(site: Type): Type

/** @see [[info]] */
def typeSignature: Type

/** The type signature of this symbol.
 *
 *  This method always returns signatures in the most generic way possible, even if the underlying symbol is obtained from an
 *  instantiation of a generic type. For example, signature
 *  of the method `def map[B](f: (A) ⇒ B): List[B]`, which refers to the type parameter `A` of the declaring class `List[A]`,
 *  will always feature `A`, regardless of whether `map` is loaded from the `List[_]` or from `List[Int]`. To get a signature
 *  with type parameters appropriately instantiated, one should use `infoIn`.
 *
 *  @group Basics
 */
def info: Type

Upvotes: 0

Related Questions