2012-11-19 36 views
23

Có những trường hợp sử dụng hữu ích khi tạo bản sao của một đối tượng là một thể hiện của một lớp chữ của một tập hợp các lớp chữ hoa, có một giá trị cụ thể chung.Mô hình howto được đặt tên tham số trong lời gọi phương thức với macro Scala?

Ví dụ chúng ta hãy xem xét các lớp trường hợp sau đây:

case class Foo(id: Option[Int]) 
case class Bar(arg0: String, id: Option[Int]) 
case class Baz(arg0: Int, id: Option[Int], arg2: String) 

Sau đó copy có thể được gọi vào mỗi trường hợp này trường hợp lớp:

val newId = Some(1) 

Foo(None).copy(id = newId) 
Bar("bar", None).copy(id = newId) 
Baz(42, None, "baz").copy(id = newId) 

Như đã trình bày herehere không có cách nào đơn giản để trừu tượng điều này như sau:

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) 

Vì vậy, tôi đã tạo ra một vĩ mô scala, mà làm công việc này (hầu như):

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 + "'") 
    } 

    } 

} 

Đối số cuối cùng của Apply (xem dưới cùng của khối mã ở trên) là một danh sách các thông số (ở đây: các tham số của phương pháp 'bản sao'). Làm cách nào để có thể nhập id loại c.Expr[Option[Int]] như tham số được đặt tên cho phương thức sao chép với sự trợ giúp của API macro mới?

Đặc biệt khái niệm vĩ mô sau

c.Expr(
    Apply(
    Select(
     reify(entity.splice).tree, 
     newTermName("copy")), 
    List(/*?id?*/))) 

nên kết quả trong

entity.copy(id = id) 

để sau giữ

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))) 

} 

Phần thiếu được ký hiệu bởi các placeholder /*?id?*/.

Trả lời

20

Dưới đây là một thực hiện đó cũng là một chút chung chung hơn:

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) 
     } 
    )) 
    } 
} 

Nó sẽ làm việc trên bất kỳ lớp trường hợp với một thành viên mang tên id, không có vấn đề gì loại của nó là:

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) 

Nếu trường hợp không có thành viên id, withId sẽ biên dịch — nó sẽ không làm gì cả. Nếu bạn muốn có một lỗi biên dịch trong trường hợp đó, bạn có thể thêm một điều kiện bổ sung cho trận đấu trên copy.


Edit: Theo Eugene Burmako chỉ chỉ ra on Twitter, bạn có thể viết những dòng này một cách tự nhiên hơn một chút bằng AssignOrNamedArg ở cuối:

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

Phiên bản này sẽ không biên dịch nếu lớp trường hợp doesn' t có một thành viên id, nhưng đó có thể là hành vi mong muốn nhất.

+0

Cảm ơn bạn, tôi thích conciseness của giải pháp này.Nó áp dụng tốt cho trường hợp sử dụng của tôi. Có lẽ phần s.paramss.head cần kiểm tra bổ sung cho các phương thức nullary (= các phương thức không có danh sách đối số), tức là khi s.paramss trả về List()/Nil. Nhưng kết quả là như nhau: macro không thể được áp dụng. –

+0

@DanielDietrich: Điểm tốt, và tôi đã thêm kiểm tra đó, nhưng lưu ý rằng đây chỉ là một bản phác thảo và vẫn còn ít nhất một giả định tương tự trong phiên bản sửa đổi (chỉ có một phương thức có tên là 'copy'). May mắn thay điều tồi tệ nhất có thể xảy ra là một lỗi biên dịch hơi khó hiểu. –

+0

Vâng, bạn nói đúng. Như bạn đã nói trong bài viết đầu tiên của bạn, một kiểm tra cho sự tồn tại của id param có thể được thực hiện. Trong giải pháp hiện tại đã có lỗi biên dịch, nếu thiếu id thông số. Để nhận được thông báo lỗi trình biên dịch chi tiết hơn, tôi sẽ thay đổi if-guard của mẫu khớp với (s.paramss.flatten.map (_. Name) .contains (newTermName ("id"))). Với điều này, phương pháp nullary cũng bị bắt. –

2

Đây là giải pháp của Travis nơi mà tất cả các bộ phận được đặt lại với nhau:

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!") 
    } 

    } 

} 
Các vấn đề liên quan