복붙노트

[SCALA] 어떻게 스칼라 매크로 메소드 호출에서 명명 된 매개 변수를 모델링하려면?

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)

그리고이 경우 클래스 인스턴스의 각 호출 할 수 있습니다 복사 :

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

  }

}

적용의 마지막 인수는 (위의 코드 블록의 하단 참조) 매개 변수의 목록입니다 (여기 : 메소드의 매개 변수를 '복사'). 어떻게 유형 c.Expr의 지정된 ID [옵션은 지능] [] 새로운 매크로 API의 도움으로 복사 방법에 이름을 매개 변수로 전달 될 수 있는가?

특히 다음 매크로 표현에서

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로 표시된다? * /.

해결법

  1. ==============================

    1.여기에 또한 좀 더 범용의 구현입니다 :

    여기에 또한 좀 더 범용의 구현입니다 :

    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)
    

    케이스 클래스는 ID의 멤버가없는 경우, withId는 컴파일이됩니다 단지 아무것도하지 않습니다. 당신이 경우 컴파일 오류를 원하는 경우에, 당신은 사본에 경기에 추가 조건을 추가 할 수 있습니다.

    편집 :로 유진 Burmako 그냥 트위터에 지적, 당신은 마지막에 AssignOrNamedArg를 사용하여 더 자연스럽게 작은 이것을 쓸 수 있습니다 :

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

    케이스 클래스는 ID 회원이없는 경우이 버전은 컴파일되지 않습니다,하지만 가능성이 어쨌든 원하는 동작 할 수있다.

  2. ==============================

    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!")
        }
    
      }
    
    }
    
  3. from https://stackoverflow.com/questions/13446528/howto-model-named-parameters-in-method-invocations-with-scala-macros by cc-by-sa and MIT license