如何在实现类型的Scala宏中使用ClassTag

时间:2018-09-18 17:47:13

标签: scala scala-macros

我写了一个宏,它读取类字段:

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

object ArrayLikeFields {
  def extract[T]: Set[String] = macro extractImpl[T]

  def extractImpl[T: c.WeakTypeTag](c: whitebox.Context): c.Expr[Set[String]] = {

    import c.universe._

    val tree = weakTypeOf[T].decls
      .collectFirst {
        case m: MethodSymbol if m.isPrimaryConstructor => m
      }
      .map(y => y.paramLists.headOption.getOrElse(Seq.empty))
      .getOrElse(Seq.empty)
      .map(s => q"${s.name.decodedName.toString}")

    c.Expr[Set[String]] {
      q"""Set(..$tree)"""
    }
  }

}

我能够针对具体类型进行编译和运行:

object Main extends App {
  case class Person(name:String)
  val res: Set[String] = ArrayLikeFields.extract[Person]
}

但是我想将它用于这样的通用类型:

object Lib {
  implicit class SomeImplicit(s: String) {

    def toOrgJson[T]: JSONObject = {
      val arrayLikeFields: Set[String] = ArrayLikeFields.extract[T]
      //some code, that uses fields, etc
      null
    }
  }
}

编译错误:

  

错误:(14,65)类型不匹配;发现:   scala.collection.immutable.Set [不需要]:Set [字符串]注意:   什么都不是:字符串,但是特征集在类型A中是不变的。您可能希望   调查诸如_ <: String之类的通配符类型。 (SLS 3.2.10)         val arrayLikeFields:Set [String] = ArrayLikeFields.extract [T]

我不能理解这一点。我该如何解决我的问题?

更新
我读过关于实现的scala 2.10.2 calling a 'macro method' with generic type not work,但我没有课程实例

3 个答案:

答案 0 :(得分:3)

尝试实现像1中的类型类的方法

object Main extends App {
  case class Person(name:String)
  val res: Set[String] = ArrayLikeFields.extract[Person] //Set(name)

  import Lib._
  "abc".toOrgJson[Person] // prints Set(name)
}

object Lib {
  implicit class SomeImplicit(s: String) {
    def toOrgJson[T: ArrayLikeFields.Extract]: JSONObject = {
      val arrayLikeFields: Set[String] = ArrayLikeFields.extract[T]
      //some code, that uses fields, etc
      println(arrayLikeFields) //added
      null
    }
  }
}

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

object ArrayLikeFields {    
  def extract[T](implicit extr: Extract[T]): Set[String] = extr()

  trait Extract[T] {
    def apply(): Set[String]
  }

  object Extract {
    implicit def materializeExtract[T]: Extract[T] = macro materializeExtractImpl[T]

    def materializeExtractImpl[T: c.WeakTypeTag](c: whitebox.Context): c.Expr[Extract[T]] = {
      import c.universe._

      val tree = weakTypeOf[T].decls
        .collectFirst {
          case m: MethodSymbol if m.isPrimaryConstructor => m
        }
        .map(y => y.paramLists.headOption.getOrElse(Seq.empty))
        .getOrElse(Seq.empty)
        .map(s => q"${s.name.decodedName.toString}")

      c.Expr[Extract[T]] {
        q"""new ArrayLikeFields.Extract[${weakTypeOf[T]}] {
          override def apply(): _root_.scala.collection.immutable.Set[_root_.java.lang.String] =
            _root_.scala.collection.immutable.Set(..$tree)
        }"""
      }
    }
  }
}

实际上,我认为您不需要白盒宏,黑盒宏就足够了。因此,您可以将(c: whitebox.Context)替换为(c: blackbox.Context)

顺便说一句,可以通过Shapeless而不是宏来解决相同的问题(宏可以在Shapeless的背景下工作)

object Main extends App {
  case class Person(name:String)
  val res: Set[String] = ArrayLikeFields.extract[Person] //Set(name)
}

object ArrayLikeFields {
  def extract[T: Extract]: Set[String] = implicitly[Extract[T]].apply()

  trait Extract[T] {
    def apply(): Set[String]
  }

  object Extract {
    def instance[T](strs: Set[String]): Extract[T] = () => strs

    implicit def genericExtract[T, Repr <: HList](implicit
      labelledGeneric: LabelledGeneric.Aux[T, Repr],
      extract: Extract[Repr]
      ): Extract[T] = instance(extract())

    implicit def hconsExtract[K <: Symbol, V, T <: HList](implicit
      extract: Extract[T],
      witness: Witness.Aux[K]
      ): Extract[FieldType[K, V] :: T] =
      instance(extract() + witness.value.name)

    implicit val hnilExtract: Extract[HNil] = instance(Set())
  }
}

答案 1 :(得分:2)

链接的问题scala 2.10.2 calling a 'macro method' with generic type not work的答案也适用于此。

您正在尝试使用编译时宏解决运行时问题,这是不可能的。

被调用的方法toOrgJson[T]无法知道T在编译时表示的具体类型,而只能在运行时获取该信息。因此,仅在运行时,您将无法在T上对ArrayLikeFields.extract[T]进行任何具体操作(例如列出其字段)。

您可以在运行时使用Reflection实现类似onClick之类的操作,请参见例如Get field names list from case class

答案 2 :(得分:0)

我对宏没有非常扎实的理解,但是似乎编译器无法理解宏函数的返回类型是 Set[String]

以下技巧在scala 2.12.7中对我有效

def toOrgJson[T]: JSONObject = {
      val arrayLikeFields: Set[String] = ArrayLikeFields.extract[T].map(identity[String])
      //some code, that uses fields, etc
      null
    } 

编辑

实际上要获取一个非空的Set T需要一个上限,例如T <: Person ...而那不是您想要的...

因为代码可以编译,所以在这里留下答案,这可能会帮助某个人找到答案