본문 바로가기

programming/scala

scala - macro keyword

 

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