Вложенные неявные макросы, похоже, не работают для параметров типа?

Я пишу неявный макрос Scala, который автоматически генерирует класс типа для классов case (с использованием квазиквадрата, как Scala 2.10.3, с плагином компилятора макроса и Scala 2.11.0-M7).

Неявный макрос рекурсивно ищет типы классов для параметра.

Пока класс case не принимает параметры типа или параметры типа не используются в генерируемом коде, он отлично работает.

Но когда требуется неявное значение <TypeClass>[<TypeParameter of case class>], компиляция сайта вызова не выполняется с "не удалось найти неявное значение для параметра e".

Вот код, который воспроизводит проблему:

trait TestTypeClass[A] {
  def str(x: A): String
}
object Test {
  implicit def BooleanTest = new TestTypeClass[Boolean] {
    def str(x: Boolean) = x.toString
  }
  def CaseClassTestImpl[A: c.WeakTypeTag](c: Context): c.Expr[TestTypeClass[A]] = {
    import c.universe._
    val aType = weakTypeOf[A]
    val TestTypeClassType = weakTypeOf[TestTypeClass[_]]
    val typeName = aType.typeSymbol.name.decoded
    val params = aType.declarations.collectFirst { case m: MethodSymbol if m.isPrimaryConstructor => m }.get.paramss.head
    val paramTypes = aType.declarations.collectFirst { case m: MethodSymbol if m.isPrimaryConstructor => m }.get.paramss.head.map(_.typeSignature)
    val paramList = for (i <- 0 until params.size) yield {
      val param = params(i)
      val paramType = paramTypes(i)
      val paramName = param.name.decoded
      q"str($param)"
    }
    println(paramList)
    val src =
      q"""
    new TestTypeClass[$aType] {
      def str(x: $aType) = Seq(..$paramList).mkString(",")
    }
    """
    c.Expr[TestTypeClass[A]](src)
  }
  implicit def CaseClassTest[A]: TestTypeClass[A] = macro CaseClassTestImpl[A]
  def str[A: TestTypeClass](x: A) = implicitly[TestTypeClass[A]].str(x)
}

// somewhere in other module
implicitly[TestTypeClass[TestClass]] // OK.
implicitly[TestTypeClass[TestClass2[Boolean]]] // Error
// could not find implicit value for parameter e: TestTypeClass[TestClass2[Boolean]]
implicitly[TestTypeClass[TestClass2[TestClass]]] // Error
// could not find implicit value for parameter e: TestTypeClass[TestClass2[TestClass]]

Это так по дизайну, я делаю что-то неправильно или это ошибка компилятора?

Ответ 1

Есть некоторые проблемы на уровне поверхности, которые должны помешать вашей версии работать вообще, но как только они будут рассмотрены, вы должны быть в состоянии делать именно то, что вы хотите (хорошая идея или нет, это другой вопрос, который Я попытаюсь обратиться в конце этого ответа).

Три самые большие проблемы в этой строке:

      q"str($param)"

Прежде всего, в контексте сгенерированного кода str будет ссылаться на метод анонимного класса, который вы определяете и создаете, а не на метод str на Test. Затем это сгенерирует код, который выглядит как str(member), но member ничего не будет означать в контексте сгенерированного кода - вам нужно что-то вроде str(x.member). Наконец (и, соответственно), каждый param будет конструктором, а не аксессуаром.

Ниже приведен полный рабочий пример (проверен на 2.10.3):

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

trait TestTypeClass[A] { def str(x: A): String }

object Test {
  implicit def BooleanTest = new TestTypeClass[Boolean] {
    def str(x: Boolean) = x.toString
  }

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

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

    val paramList = params.map(param => q"Test.str(x.$param)")

    val src = q"""
      new TestTypeClass[$aType] {
        def str(x: $aType) = Seq(..$paramList).mkString(",")
      }
    """

    c.Expr[TestTypeClass[A]](src)
  }
  implicit def CaseClassTest[A]: TestTypeClass[A] = macro CaseClassTestImpl[A]
  def str[A: TestTypeClass](x: A) = implicitly[TestTypeClass[A]].str(x)
}

И затем некоторые демонстрационные установки:

import Test._

case class Foo(x: Boolean, y: Boolean)
case class Bar[A](a: A)

И наконец:

scala> str(Bar(Foo(true, false)))
res0: String = true,false

Что показывает нам, что компилятор успешно нашел экземпляр для Bar[Foo], применяя макрос рекурсивно.

Таким образом, этот подход работает, но он также подрывает некоторые из больших преимуществ, которые классы классов предоставляют, например, решения, основанные на времени, для решения этой проблемы. Намного легче понять, какие экземпляры доступны, когда у нас есть макрос, который просто вытаскивает их из воздуха. Логика, которая определяет, что она может найти, похожа на код реализации макроса, который будет выполняться во время компиляции, поэтому он по-прежнему безопасен в определенном смысле, но менее прозрачен.

Эта реализация также чересчур порождает экземпляры (try str(1)), которые можно легко исправить, но это хорошая иллюстрация того, насколько опасны такие вещи.

Для чего стоит альтернативное решение с использованием класса Shapeless 2.0 TypeClass, упомянутого выше в Miles (вы также можете увидеть мой пост в блоге здесь для аналогичного сравнения).

implicit def BooleanTest = new TestTypeClass[Boolean] {
  def str(x: Boolean) = x.toString
}

def str[A: TestTypeClass](x: A) = implicitly[TestTypeClass[A]].str(x)

import shapeless._

implicit object `TTC is a type class` extends ProductTypeClass[TestTypeClass] {
  def product[H, T <: HList](htc: TestTypeClass[H], ttc: TestTypeClass[T]) =
    new TestTypeClass[H :: T] {
      def str(x: H :: T) = {
        val hs = htc.str(x.head)
        val ts = ttc.str(x.tail)
        if (ts.isEmpty) hs else hs + "," + ts
      }
    }
  def emptyProduct = new TestTypeClass[HNil] { def str(x: HNil) = "" }
  def project[F, G](inst: => TestTypeClass[G], to: F => G, from: G => F) =
    new TestTypeClass[F] { def str(x: F) = inst.str(to(x)) }
}

object TestTypeClassHelper extends TypeClassCompanion[TestTypeClass]
import TestTypeClassHelper.auto._

Это не более краткий, но более общий и менее вероятный, чем вы не ожидаете. Там все еще происходит магия, но легче контролировать и рассуждать.