有一些用例可以創建一個對象的副本,該對象是一組具有特定值的案例類別的案例類別的實例。Howto使用Scala宏調用方法調用中的命名參數?
例如,讓我們考慮以下情況下類:
case class Foo(id: Option[Int])
case class Bar(arg0: String, id: Option[Int])
case class Baz(arg0: Int, id: Option[Int], arg2: String)
然後copy
可以在這些情況下,類實例被稱爲:
val newId = Some(1)
Foo(None).copy(id = newId)
Bar("bar", None).copy(id = newId)
Baz(42, None, "baz").copy(id = newId)
type Copyable[T] = { def copy(id: Option[Int]): T }
// THIS DOES *NOT* WORK FOR CASE CLASSES
def withId[T <: Copyable[T]](obj: T, newId: Option[Int]): T =
obj.copy(id = newId)
所以我創建了一個階宏,它完成這個工作(幾乎):
import scala.reflect.macros.Context
object Entity {
import scala.language.experimental.macros
import scala.reflect.macros.Context
def withId[T](entity: T, id: Option[Int]): T = macro withIdImpl[T]
def withIdImpl[T: c.WeakTypeTag](c: Context)(entity: c.Expr[T], id: c.Expr[Option[Int]]): c.Expr[T] = {
import c.universe._
val currentType = entity.actualType
// reflection helpers
def equals(that: Name, name: String) = that.encoded == name || that.decoded == name
def hasName(name: String)(implicit method: MethodSymbol) = equals(method.name, name)
def hasReturnType(`type`: Type)(implicit method: MethodSymbol) = method.typeSignature match {
case MethodType(_, returnType) => `type` == returnType
}
def hasParameter(name: String, `type`: Type)(implicit method: MethodSymbol) = method.typeSignature match {
case MethodType(params, _) => params.exists { param =>
equals(param.name, name) && param.typeSignature == `type`
}
}
// finding method entity.copy(id: Option[Int])
currentType.members.find { symbol =>
symbol.isMethod && {
implicit val method = symbol.asMethod
hasName("copy") && hasReturnType(currentType) && hasParameter("id", typeOf[Option[Int]])
}
} match {
case Some(symbol) => {
val method = symbol.asMethod
val param = reify((
c.Expr[String](Literal(Constant("id"))).splice,
id.splice)).tree
c.Expr(
Apply(
Select(
reify(entity.splice).tree,
newTermName("copy")),
List(/*id.tree*/)))
}
case None => c.abort(c.enclosingPosition, currentType + " needs method 'copy(..., id: Option[Int], ...): " + currentType + "'")
}
}
}
的Apply
(見上面的代碼塊的底部)最後一個參數的參數(這裏的列表:方法「複製」參數)。在新的宏API的幫助下,c.Expr[Option[Int]]
類型的給定id
如何作爲命名參數傳遞給複製方法?
特別下面的宏表達
c.Expr(
Apply(
Select(
reify(entity.splice).tree,
newTermName("copy")),
List(/*?id?*/)))
應導致
entity.copy(id = id)
,使得下式成立
case class Test(s: String, id: Option[Int] = None)
// has to be compiled by its own
object Test extends App {
assert(Entity.withId(Test("scala rulz"), Some(1)) == Test("scala rulz", Some(1)))
}
缺少的部分是由佔位符/*?id?*/
表示。
謝謝你,我喜歡這個解決方案的簡潔性。它適用於我的用例。也許s.paramss.head部分需要額外檢查nullary方法(=沒有參數列表的方法),即當s.paramss返回List()/ Nil時。但結果是一樣的:宏不能被應用。 –
@DanielDietrich:好的,我已經添加了這個檢查,但是請注意,這只是一個草圖,修改後的版本中至少還有一個類似的假設(即只有一個名爲'copy'的方法)。幸運的是,可能發生的最糟糕的情況是編譯時錯誤有點令人困惑。 –
是的,你是對的。正如你在第一篇文章中所說的那樣,可以檢查是否存在參數id。在當前的解決方案中,如果缺少參數ID,則會出現編譯錯誤。爲了得到更詳細的編譯器錯誤信息,我將模式匹配的if-guard改爲(s.paramss.flatten.map(_。name).contains(newTermName(「id」)))。有了這個,nullary方法也被捕獲。 –