scala - macro keyword
macro keyword?
- compile time에 코드를 만들어주는 기능이다. (compile-time metaprogramming)
Simple example
- add() 함수
- 선언과 정의
object MacroDemo { def add[T](num1: Int, num2: Int): Int = macro add_impl def add_impl(c: blackbox.Context)(num1: c.Expr[Int], num2: c.Expr[Int]): c.Expr[Int] = { import c.universe.reify reify { num1.splice + num2.splice } } }
- 사용
object MacroTestMain { def main(args: Array[String]): Unit = { val addValue = MacroDemo.add(10, 20) println(s"add: $addValue") } }
- 설명
- 개발자의 이해 → main 함수에서 MacroDemo.add(10, 20) 을 호출하면 MacroDemo의 def add[T](num1: Int, num2: Int): Int = macro add_impl 구분을 호출하고 이를 실제 impl로 가서 수행하여 값을 가져온다.
- 실제로 MacroDemo를 컴파일하면 아래와 같은 바이트코드가 만들어진다.
//decompiled from MacroDemo$.class package me.gordonlee.MacroDemo; import scala.collection.immutable..colon.colon; import scala.collection.immutable.Nil.; import scala.reflect.api.Mirror; import scala.reflect.api.TreeCreator; import scala.reflect.api.TypeCreator; import scala.reflect.api.Exprs.Expr; import scala.reflect.api.Names.NameApi; import scala.reflect.api.Trees.TreeApi; import scala.reflect.api.Types.TypeApi; import scala.reflect.macros.Universe; import scala.reflect.macros.blackbox.Context; public final class MacroDemo$ { public static MacroDemo$ MODULE$; static { new MacroDemo$(); } public Expr add_impl(final Context c, final Expr num1, final Expr num2) { Universe $u = c.universe(); Mirror $m = c.universe().rootMirror(); final class $treecreator1$1 extends TreeCreator { private final Expr num1$1$1; private final Expr num2$1$1; public TreeApi apply(final Mirror $m$untyped) { scala.reflect.api.Universe $u = $m$untyped.universe(); return $u.Apply().apply($u.Select().apply(this.num1$1$1.in($m$untyped).tree(), (NameApi)$u.TermName().apply("$plus")), new colon(this.num2$1$1.in($m$untyped).tree(), .MODULE$)); } public $treecreator1$1(final Expr num1$1$1, final Expr num2$1$1) { this.num1$1$1 = num1$1$1; this.num2$1$1 = num2$1$1; } } final class $typecreator2$1 extends TypeCreator { public TypeApi apply(final Mirror $m$untyped) { scala.reflect.api.Universe $u = $m$untyped.universe(); return $m$untyped.staticClass("scala.Int").asType().toTypeConstructor(); } public $typecreator2$1() { } } return $u.Expr().apply($m, new $treecreator1$1(num1, num2), $u.TypeTag().apply($m, new $typecreator2$1())); } private MacroDemo$() { MODULE$ = this; } } //decompiled from MacroDemo.class package me.gordonlee.MacroDemo; import scala.reflect.ScalaSignature; import scala.reflect.api.Exprs.Expr; import scala.reflect.macros.blackbox.Context; @ScalaSignature( bytes = "\u0006\u0001\u0005\u0005r!B\u0003\u0007\u0011\u0003aa!\u0002\b\u0007\u0011\u0003y\u0001\"\u0002\f\u0002\t\u00039\u0002B\u0002\r\u0002\u0005\u0013\u0005\u0011\u0004C\u0003e\u0003\u0011\u0005Q0A\u0005NC\u000e\u0014x\u000eR3n_*\u0011Qa\u0002\u0006\u0003\u0011%\t\u0011bZ8sI>tG.Z3\u000b\u0003)\t!!\\3\u0004\u0001A\u0011Q\"A\u0007\u0002\r\tIQ*Y2s_\u0012+Wn\\\n\u0003\u0003A\u0001\"!\u0005\u000b\u000e\u0003IQ\u0011aE\u0001\u0006g\u000e\fG.Y\u0005\u0003+I\u0011a!\u00118z%\u00164\u0017A\u0002\u001fj]&$h\bF\u0001\r\u0003\r\tG\rZ\u000b\u00035\t\"2a\u0007\u0010!!\t\tB$\u0003\u0002\u001e%\t\u0019\u0011J\u001c;\t\u000b}\u0019\u0001\u0019A\u000e\u0002\t9,X.\r\u0005\u0006C\r\u0001\raG\u0001\u0005]Vl'\u0007B\u0003$\u0007\t\u0007AEA\u0001U#\t)\u0003\u0006\u0005\u0002\u0012M%\u0011qE\u0005\u0002\b\u001d>$\b.\u001b8h!\t\t\u0012&\u0003\u0002+%\t\u0019\u0011I\\=)\u0007\rac\u0007\u0005\u0002.i5\taF\u0003\u00020a\u0005A\u0011N\u001c;fe:\fGN\u0003\u00022e\u00051Q.Y2s_NT!a\r\n\u0002\u000fI,g\r\\3di&\u0011QG\f\u0002\n[\u0006\u001c'o\\%na2\f\u0014bH\u001c9u\r[5\u000bX3\f\u0001E\"AeN\u0006:\u0003\u0015i\u0017m\u0019:pc\u00111rgO 2\u0007\u0015bThD\u0001>C\u0005q\u0014aC7bGJ|WI\\4j]\u0016\f4!\n!B\u001f\u0005\t\u0015%\u0001\"\u0002KY<d\u0006\r\u0011)S6\u0004H.Z7f]R,G\rI5oAM\u001b\u0017\r\\1!e9\n\u0014G\f\u0019.\u001bbJ\u0013\u0007\u0002\f8\t\"\u000b4!J#G\u001f\u00051\u0015%A$\u0002\u0011%\u001c()\u001e8eY\u0016\f4!J%K\u001f\u0005Q\u0015$\u0001\u00012\tY9D\nU\u0019\u0004K5su\"\u0001(\"\u0003=\u000b!\"[:CY\u0006\u001c7NY8yc\r)\u0013KU\b\u0002%f\t\u0011!\r\u0003\u0017oQC\u0016gA\u0013V->\ta+I\u0001X\u0003%\u0019G.Y:t\u001d\u0006lW-M\u0002&3j{\u0011AW\u0011\u00027\u0006\tS.\u001a\u0018h_J$wN\u001c7fK:j\u0015m\u0019:p\t\u0016lwNL'bGJ|G)Z7pIE\"acN/bc\r)clX\b\u0002?\u0006\n\u0001-\u0001\u0006nKRDw\u000e\u001a(b[\u0016\f4!\n2d\u001f\u0005\u0019\u0017%\u00013\u0002\u0011\u0005$GmX5na2\fDAF\u001cgUF\u001aQe\u001a5\u0010\u0003!\f\u0013![\u0001\ng&<g.\u0019;ve\u0016\fTaH\u001cle^\fD\u0001J\u001cm[&\u0011QN\\\u0001\u0005\u0019&\u001cHO\u0003\u0002pa\u0006I\u0011.\\7vi\u0006\u0014G.\u001a\u0006\u0003cJ\t!bY8mY\u0016\u001cG/[8oc\u0011yrg\u001d;2\t\u0011:D.\\\u0019\u0004KU4x\"\u0001<\u001e\u0003}\u0010TaH\u001cysr\fD\u0001J\u001cm[F\u001aQE_>\u0010\u0003ml\u0012A\u0000\u0019\u0004Ki\\Hc\u0001@\u0002\u0006Q)q0!\b\u0002 A)\u0011\u0011AA\u000b79!\u00111AA\u0003\u0019\u0001Aq!a\u0002\u0005\u0001\u0004\tI!A\u0001d!\u0011\tY!!\u0005\u000e\u0005\u00055!bAA\ba\u0005A!\r\\1dW\n|\u00070\u0003\u0003\u0002\u0014\u00055!aB\"p]R,\u0007\u0010^\u0005\u0005\u0003/\tIB\u0001\u0003FqB\u0014\u0018bAA\u000ea\t9\u0011\t\\5bg\u0016\u001c\b\"B\u0010\u0005\u0001\u0004y\b\"B\u0011\u0005\u0001\u0004y\b" ) public final class MacroDemo { public static Expr add_impl(final Context c, final Expr num1, final Expr num2) { return MacroDemo$.MODULE$.add_impl(var0, var1, var2); } }
- 위 에서 알수 있는 것은 우리가 add() 라고 했던 함수의 선언이 사라졌다는 것과 + 를 구현한 함수 본문이 apply 안으로 들어가서 번역되었다는 것이다.
- 이를 사용하는 사용처는 어떻게 되어 있을까?
//decompiled from MacroTestMain$.class package me.gordonlee.Main; import scala.Predef.; import scala.collection.mutable.ArrayOps.ofRef; import scala.runtime.BoxedUnit; public final class MacroTestMain$ { public static MacroTestMain$ MODULE$; static { new MacroTestMain$(); } public void main(final String[] args) { int addValue = 30; .MODULE$.println((new StringBuilder(5)).append("add: ").append(addValue).toString()); } private MacroTestMain$() { MODULE$ = this; } } //decompiled from MacroTestMain.class package me.gordonlee.Main; import scala.reflect.ScalaSignature; @ScalaSignature( bytes = "\u0006\u00015:Q\u0001B\u0003\t\u000211QAD\u0003\t\u0002=AQAF\u0001\u0005\u0002]AQ\u0001G\u0001\u0005\u0002e\tQ\"T1de>$Vm\u001d;NC&t'B\u0001\u0004\b\u0003\u0011i\u0015-\u001b8\u000b\u0005!I\u0011!C4pe\u0012|g\u000e\\3f\u0015\u0005Q\u0011AA7f\u0007\u0001\u0001\"!D\u0001\u000e\u0003\u0015\u0011Q\"T1de>$Vm\u001d;NC&t7CA\u0001\u0011!\t\tB#D\u0001\u0013\u0015\u0005\u0019\u0012!B:dC2\f\u0017BA\u000b\u0013\u0005\u0019\te.\u001f*fM\u00061A(\u001b8jiz\"\u0012\u0001D\u0001\u0005[\u0006Lg\u000e\u0006\u0002\u001b;A\u0011\u0011cG\u0005\u00039I\u0011A!\u00168ji\")ad\u0001a\u0001?\u0005!\u0011M]4t!\r\t\u0002EI\u0005\u0003CI\u0011Q!\u0011:sCf\u0004\"a\t\u0016\u000f\u0005\u0011B\u0003CA\u0013\u0013\u001b\u00051#BA\u0014\f\u0003\u0019a$o\\8u}%\u0011\u0011FE\u0001\u0007!J,G-\u001a4\n\u0005-b#AB*ue&twM\u0003\u0002*%\u0001" ) public final class MacroTestMain { public static void main(final String[] args) { MacroTestMain$.MODULE$.main(var0); } }
- main 함수에서 보는 것과 같이 아예 10, 20을 더한 30이라는 수로 선언되었다. (컴파일러의 최적화 영향인듯 하다)
- isEven() 예제
- 선언과 정의
object MacroDemo2 { def isEven(number: Int): Unit = macro isEvenMacro def isEvenMacro(c: blackbox.Context)(number: c.Tree): c.Tree = { import c.universe._ //MEMO: gordonlee. q is quasiquotes (detail info: https://docs.scala-lang.org/overviews/quasiquotes/intro.html ) q""" if($number %2 == 0) println("even number") else println("odd number") """ } }
- 사용
object MacroTestMain { def main(args: Array[String]): Unit = { MacroDemo2.isEven(20) MacroDemo2.isEven(21) val input = Integer.parseInt(args.head) MacroDemo2.isEven(input) } }
- 설명
- add 함수와 동일하게 구성하였으나, 한 가지 다른 점이 있다. 바로 jar 파일을 실행할 때 넘겨받는 값(args)을 input으로 사용하게 한 것이다. 상수는 컴파일타임에 최적화되어 컴파일타임에 코드를 생략할 수 있으나, main의 실행인자로 받은 값은 컴파일타임엔 알 수 없기 때문에 최적화 할 수 없다.
- 이에 메인 함수는 아래와 같이 바이트 코드로 변환되었다.
//decompiled from MacroTestMain$.class package me.gordonlee.Main; import scala.Predef.; import scala.collection.mutable.ArrayOps.ofRef; import scala.runtime.BoxedUnit; public final class MacroTestMain$ { public static MacroTestMain$ MODULE$; static { new MacroTestMain$(); } public void main(final String[] args) { .MODULE$.println("even number"); BoxedUnit var10000 = BoxedUnit.UNIT; .MODULE$.println("odd number"); var10000 = BoxedUnit.UNIT; int input = Integer.parseInt((String)(new ofRef(.MODULE$.refArrayOps((Object[])args))).head()); if (input % 2 == 0) { .MODULE$.println("even number"); var10000 = BoxedUnit.UNIT; } else { .MODULE$.println("odd number"); var10000 = BoxedUnit.UNIT; } } private MacroTestMain$() { MODULE$ = this; } } //decompiled from MacroTestMain.class package me.gordonlee.Main; import scala.reflect.ScalaSignature; @ScalaSignature( bytes = "\u0006\u00015:Q\u0001B\u0003\t\u000211QAD\u0003\t\u0002=AQAF\u0001\u0005\u0002]AQ\u0001G\u0001\u0005\u0002e\tQ\"T1de>$Vm\u001d;NC&t'B\u0001\u0004\b\u0003\u0011i\u0015-\u001b8\u000b\u0005!I\u0011!C4pe\u0012|g\u000e\\3f\u0015\u0005Q\u0011AA7f\u0007\u0001\u0001\"!D\u0001\u000e\u0003\u0015\u0011Q\"T1de>$Vm\u001d;NC&t7CA\u0001\u0011!\t\tB#D\u0001\u0013\u0015\u0005\u0019\u0012!B:dC2\f\u0017BA\u000b\u0013\u0005\u0019\te.\u001f*fM\u00061A(\u001b8jiz\"\u0012\u0001D\u0001\u0005[\u0006Lg\u000e\u0006\u0002\u001b;A\u0011\u0011cG\u0005\u00039I\u0011A!\u00168ji\")ad\u0001a\u0001?\u0005!\u0011M]4t!\r\t\u0002EI\u0005\u0003CI\u0011Q!\u0011:sCf\u0004\"a\t\u0016\u000f\u0005\u0011B\u0003CA\u0013\u0013\u001b\u00051#BA\u0014\f\u0003\u0019a$o\\8u}%\u0011\u0011FE\u0001\u0007!J,G-\u001a4\n\u0005-b#AB*ue&twM\u0003\u0002*%\u0001" ) public final class MacroTestMain { public static void main(final String[] args) { MacroTestMain$.MODULE$.main(var0); } }
- 첫 번째 20, 21처럼 상수를 넣은 결과는 println을 직접적으로 뽑아서 함수 본문으로 가져왔다.
- 다만 args를 가져온 값은 함수 본문 자체가 메인 함수로 그대로 복사-붙여넣기를 한 것처럼 가져와졌다.
- C++ 의 매크로와 유사하게 함수 자체가 치환되는 결과를 확인할 수 있었다.
- 선언과 정의
코드 참고
Git gist: https://gist.github.com/gordonlee/f34fa4f3ec8d600bfd431c9a5d27c7e8
sbt 프로젝트 설정
project
|-- macros
| -- src -- scala -- <package_name> -- source_code( *.scala )
|-- main
| -- src -- scala -- <package_name> -- source_code( *.scala )
|-- build.sbt
- macros 프로젝트와 main 프로젝트를 별개로 구분해야 한다. 분리하지 않으면 compile time에 코드를 생성함과 동시에 main 을 컴파일 해야하는데, 의존성이 있는 컴파일이 동일 프로젝트에서는 불가능하다.
사용 후 알게된 점
- compile 이후에 코드가 생성되기 때문에 macro를 만들어야 하는 코드조각과 컴파일 후에 만들어진 코드를 사용하는 사용처 코드 조각이 같은 패키지에 있으면 로딩되지 않는다.
- sbt dependsOn 등을 찾아서 해보았지만 뾰족한 해결방법을 찾지 못했다.
- macro가 들어있는 프로젝트(?) 를 먼저 compile 하고, 이후에 사용처를 컴파일하면 컴파일러가 만든 매크로 코드를 사용할 수 있다
- 이걸 어디다 쓸 수 있을까 라는 고민이 된다. → C 계열의 매크로처럼 퍼포먼스에 (미미하겠지만)영향이 있을 것 같다.
- 스칼라 코드를 string 형식으로 넣어도 이를 컴파일타임에 컴파일하여 사용할 수 있다.
- 예시는 아래와 같다
object MacroDemo2 { def isEven(number: Int): Unit = macro isEvenMacro def isEvenMacro(c: blackbox.Context)(number: c.Tree): c.Tree = { import c.universe._ q""" if($number %2 == 0) println("even number") else println("odd number") """ } }
- 이 예시에서 isEven은 "even number" 혹은 "odd number"를 리턴할 수 있다.
- 다만, 컴파일타임에 코드가 결정되므로 스트링은 컴파일타임에 읽어올 수 있어야 하겠다.
references
'programming > scala' 카테고리의 다른 글
scala - 소개와 intellij 로 환경 설정 (0) | 2020.04.22 |
---|---|
scala study 시작 (0) | 2020.04.22 |