2012-11-19 52 views
23

有一些用例可以創建一個對象的副本,該對象是一組具有特定值的案例類別的案例類別的實例。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) 

如上所述herehere有沒有簡單的方法摘要如下:

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?*/表示。

回答

20

這裏說的也是有點更通用的實現:

import scala.language.experimental.macros 

object WithIdExample { 
    import scala.reflect.macros.Context 

    def withId[T, I](entity: T, id: I): T = macro withIdImpl[T, I] 

    def withIdImpl[T: c.WeakTypeTag, I: c.WeakTypeTag](c: Context)(
    entity: c.Expr[T], id: c.Expr[I] 
): c.Expr[T] = { 
    import c.universe._ 

    val tree = reify(entity.splice).tree 
    val copy = entity.actualType.member(newTermName("copy")) 

    val params = copy match { 
     case s: MethodSymbol if (s.paramss.nonEmpty) => s.paramss.head 
     case _ => c.abort(c.enclosingPosition, "No eligible copy method!") 
    } 

    c.Expr[T](Apply(
     Select(tree, copy), 
     params.map { 
     case p if p.name.decoded == "id" => reify(id.splice).tree 
     case p => Select(tree, p.name) 
     } 
    )) 
    } 
} 

它會在任何情況下類工作有一個名爲id,無論成員的類型是什麼:

scala> case class Bar(arg0: String, id: Option[Int]) 
defined class Bar 

scala> case class Foo(x: Double, y: String, id: Int) 
defined class Foo 

scala> WithIdExample.withId(Bar("bar", None), Some(2)) 
res0: Bar = Bar(bar,Some(2)) 

scala> WithIdExample.withId(Foo(0.0, "foo", 1), 2) 
res1: Foo = Foo(0.0,foo,2) 

如果case class沒有id成員,withId會編譯它只是不會做任何事情。如果您想在這種情況下發生編譯錯誤,您可以在copy上爲匹配添加額外條件。


編輯:正如尤金Burmako只是指出on Twitter,你可以這樣寫多一點自然地使用AssignOrNamedArg末:

c.Expr[T](Apply(
    Select(tree, copy), 
    AssignOrNamedArg(Ident("id"), reify(id.splice).tree) :: Nil 
)) 

該版本將無法編譯,如果情況沒有按類」沒有id成員,但無論如何這更可能是所需的行爲。

+0

謝謝你,我喜歡這個解決方案的簡潔性。它適用於我的用例。也許s.paramss.head部分需要額外檢查nullary方法(=沒有參數列表的方法),即當s.paramss返回List()/ Nil時。但結果是一樣的:宏不能被應用。 –

+0

@DanielDietrich:好的,我已經添加了這個檢查,但是請注意,這只是一個草圖,修改後的版本中至少還有一個類似的假設(即只有一個名爲'copy'的方法)。幸運的是,可能發生的最糟糕的情況是編譯時錯誤有點令人困惑。 –

+0

是的,你是對的。正如你在第一篇文章中所說的那樣,可以檢查是否存在參數id。在當前的解決方案中,如果缺少參數ID,則會出現編譯錯誤。爲了得到更詳細的編譯器錯誤信息,我將模式匹配的if-guard改爲(s.paramss.flatten.map(_。name).contains(newTermName(「id」)))。有了這個,nullary方法也被捕獲。 –

2

這是特拉維斯的解決方案,所有部件都放在一起:

import scala.language.experimental.macros 

object WithIdExample { 

    import scala.reflect.macros.Context 

    def withId[T, I](entity: T, id: I): T = macro withIdImpl[T, I] 

    def withIdImpl[T: c.WeakTypeTag, I: c.WeakTypeTag](c: Context)(
    entity: c.Expr[T], id: c.Expr[I] 
): c.Expr[T] = { 

    import c.universe._ 

    val tree = reify(entity.splice).tree 
    val copy = entity.actualType.member(newTermName("copy")) 

    copy match { 
     case s: MethodSymbol if (s.paramss.flatten.map(_.name).contains(
     newTermName("id") 
    )) => c.Expr[T](
     Apply(
      Select(tree, copy), 
      AssignOrNamedArg(Ident("id"), reify(id.splice).tree) :: Nil)) 
     case _ => c.abort(c.enclosingPosition, "No eligible copy method!") 
    } 

    } 

}