是否可以从通用 Scala 代码调用 Scala 宏?

Is it possible to call a scala macro from generic scala code?

我正在尝试使用 Scala 宏将无类型的、类似 Map[String, Any] 的表达式转换为其对应的类型化大小写 class 表达式。

以下 Scala 宏(几乎)完成了工作:

trait ToTyped[+T] {
  def apply(term: Any): T
}

object TypeConversions {
  // At compile-time, "type-check" an untyped expression and convert it to 
  // its appropriate typed value.
  def toTyped[T]: ToTyped[T] = macro toTypedImpl[T]

  def toTypedImpl[T: c.WeakTypeTag](c: Context): c.Expr[ToTyped[T]] = {
    import c.universe._
    val tpe = weakTypeOf[T]

    if (tpe <:< typeOf[Int] || tpe <:< typeOf[String]) {
      c.Expr[ToTyped[T]](
        q"""new ToTyped[$tpe] { 
          def apply(term: Any): $tpe = term.asInstanceOf[$tpe] 
        }""")
    } else {
      val companion = tpe.typeSymbol.companion
      val maybeConstructor = tpe.decls.collectFirst { 
        case m: MethodSymbol if m.isPrimaryConstructor => m 
      }
      val constructorFields = maybeConstructor.get.paramLists.head

      val subASTs = constructorFields.map { field =>
        val fieldName = field.asTerm.name
        val fieldDecodedName = fieldName.toString
        val fieldType = tpe.decl(fieldName).typeSignature
        q"""
           val subTerm = term.asInstanceOf[Map[String, Any]]($fieldDecodedName)
           TypeConversions.toTyped[$fieldType](subTerm)
        """
      }
      c.Expr[ToTyped[T]](
        q"""new ToTyped[$tpe] { 
          def apply(term: Any): $tpe = $companion(..$subASTs) 
        }""")
    }
  }
}

使用上面的 toTyped 函数,例如,我可以将未类型化的人值转换为其对应的类型化 Person 大小写 class:

object TypeConversionTests {
  case class Person(name: String, age: Int, address: Address)
  case class Address(street: String, num: Int, zip: Int)

  val untypedPerson = Map(
    "name" -> "Max",
    "age" -> 27,
    "address" -> Map("street" -> "Palm Street", "num" -> 7, "zip" -> 12345))
  val typedPerson = TypeConversions.toTyped[Person](untypedPerson)

  typedPerson shouldEqual Person("Max", 27, Address("Palm Street", 7, 12345))
}

但是,当尝试在通用 Scala 代码中使用上面的 toTyped 宏时,我的问题出现了。假设我有一个使用 toTyped 宏的通用函数 indirection

object CanIUseScalaMacrosAndGenerics {
  def indirection[T](value: Any): T = TypeConversions.toTyped[T](value)

  import TypeConversionTests._

  val indirectlyTyped = indirection[Person](untypedPerson)

  indirectlyTyped shouldEqual Person("Max", 27, Address("Palm Street", 7, 12345))

在这里,我从 toTyped 宏中得到一个编译时错误,抱怨类型 T 尚未用具体类型实例化。我认为错误的原因是,从indirection内部的toTyped来看,类型T仍然是通用的,还没有被推断为Person。因此,当通过 indirection 调用时,宏无法构建相应的 Person 案例 class。但是,从调用点indirection[Person](untypedPerson)的角度来看,我们有T == Person,所以我想知道是否有办法获得T的实例化类型(即Person) 在宏里面 toTyped.

换句话说:我能否将 Scala 宏 toTyped 与泛型函数 indirection 结合起来,并且能够找出 [=] 中类型参数 T 的实例化类型14=]宏?还是我在这里走投无路,没有办法像这样组合 Scala 宏和泛型?在后一种情况下,我想知道这里唯一的解决方案是否是将宏使用推到 "out",我可以将其实例化为 toTyped[Person] 而不是 toTyped[T].

非常感谢任何见解。谢谢! :-)

需要扩展宏。每次你使用一个主体是宏的函数时,Scala 都必须生成代码并将其放在那里。正如您所怀疑的那样,这是非常非常具体的,并且与参数多态性的想法相矛盾,在参数多态性中您编写的代码独立于您的类型的特定知识。

Type 类 是当您希望拥有一个通用(参数)定义和算法的某些部分的多个每种类型实现时的一般问题的解决方案之一。基本上,您可以定义一些您可以考虑的接口(很可能)需要遵循一些契约(用 OOP 术语来说)并将此接口作为参数传递:

// example
trait SpecificPerType[T] {

  def doSomethingSpecific(t: T): String
}

val specificForString: SpecificPerType[String] = new SpecificPerType[String] {
  def doSomethingSpecific(t: String): String = s"MyString: $t"
}

val specificForInt: SpecificPerType[Int] = new SpecificPerType[Int] {
  def doSomethingSpecific(t: Int): String = s"MyInt: $t"
}

def genericAlgorithm[T](values: List[T])(specific: SpecificPerType[T]): String =
  values.map(specific.doSomethingSpecific).mkString("\n")

genericAlgorithm(List(1,2,3))(specificForInt)
genericAlgorithm(List("a","b","c"))(specificForString)

如您所见,传递这个特定部分会很烦人,这是引入隐式的原因之一。

所以你可以像这样使用隐式来编写它:

implicit val specificForString: SpecificPerType[String] = new SpecificPerType[String] {
  def doSomethingSpecific(t: String): String = s"MyString: $t"
}

implicit val specificForInt: SpecificPerType[Int] = new SpecificPerType[Int] {
  def doSomethingSpecific(t: Int): String = s"MyInt: $t"
}

def genericAlgorithm[T](values: List[T])(implicit specific: SpecificPerType[T]): String =
  values.map(specific.doSomethingSpecific).mkString("\n")
/* for implicits with one type parameter there exist a special syntax
   allowing to express them as if they were type constraints e.g.:

def genericAlgorithm[T: SpecificPerType](values: List[T]): String =
  values.map(implicitly[SpecificPerType[T]].doSomethingSpecific).mkString("\n")

implicitly[SpecificPerType[T]] is a summoning that let you access implicit
by type, rather than by its variable's name
*/

genericAlgorithm(List(1,2,3)) // finds specificForString using its type
genericAlgorithm(List("a","b","c")) // finds specificForInt using its type

如果您使用宏生成该特征实现,您将能够拥有一个通用算法,例如:

implicit def generate[T]: SpecificPerType[T] =
  macro SpecificPerTypeMacros.impl // assuming that you defined this macro there

据我所知,这(将宏提取到类型 类)是一种常见的模式 能够用宏生成一些代码,同时仍在其上构建逻辑 使用正常的参数代码。

(明确一点:我并不是说类类型的作用仅限于宏生成代码的载体)。