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

时间:2019-08-05 11:36:37

标签: scala generics macros metaprogramming parametric-polymorphism

我正在尝试使用Scala宏将无类型的Map[String, Any]类表达式转换为它们对应的有类型case类表达式。

以下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案例类:

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宏时,出现了我的问题。假设我有一个使用indirection宏的通用函数toTyped

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尚未用具体类型实例化。我认为该错误的原因是,从toTyped内部的indirection的角度来看,类型T仍然是通用的,尚未推断为Person。因此,当通过Person调用时,宏无法建立相应的indirection案例类。但是,从呼叫站点indirection[Person](untypedPerson)的角度来看,我们有T == Person,所以我想知道是否有一种方法可以获取T的实例化类型(即{{1} })放在宏Person中。

以不同的方式输入:我可以将Scala宏toTyped与泛型函数toTyped组合在一起,但仍能够找出{{1}中类型参数indirection的实例化类型。 }宏?还是我在这里没有希望,没有办法将Scala宏和泛型相结合?在后一种情况下,我想知道这里唯一的解决方案是否是将宏用法推到目前为止,以至于我可以将其实例化为T而不是toTyped

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

1 个答案:

答案 0 :(得分:0)

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

当您想要一个通用(参数)定义和算法某些部分的多个每个类型的实现时,

类型类是解决一般问题的方法之一。基本上,定义一些您可以考虑的接口(很可能需要遵循某种约定(以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

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

(请注意:我不认为类型类的作用是作为宏生成代码的载体而受到限制的。)