diff --git a/ja/overviews/core/macros.md b/ja/overviews/core/macros.md new file mode 100644 index 0000000000..2fbcbb7167 --- /dev/null +++ b/ja/overviews/core/macros.md @@ -0,0 +1,9 @@ +--- +layout: overview +partof: macros +overview: macros +language: ja +label-color: important +label-text: Experimental +title: マクロ +--- diff --git a/ja/overviews/macros/bundles.md b/ja/overviews/macros/bundles.md new file mode 100644 index 0000000000..fa251aa6e3 --- /dev/null +++ b/ja/overviews/macros/bundles.md @@ -0,0 +1,61 @@ +--- +layout: overview-large +language: ja + +disqus: true + +partof: macros +num: 6 +outof: 7 +title: マクロバンドル +--- +MACRO PARADISE + +**Eugene Burmako 著**
+**Eugene Yokota 訳** + +マクロバンドル (macro bundle) はマクロパラダイスと呼ばれているオフィシャル Scala リポジトリ内の実験的なブランチに含まれるリリース前の機能だ。[マクロパラダイス](/ja/overviews/macros/paradise.html)ページの説明にしたがってナイトリービルドをダウンロードしてほしい。 + +## マクロバンドル + +現行の Scala 2.10.0 においてマクロ実装は関数として表されている。コンパイラがマクロ定義の適用を見つけると、マクロ実装を呼び出すという単純なものだ。しかし、実際に使ってみると以下の理由によりただの関数では不十分なことがあることが分かった: + +
    +
  1. 関数に制限されることで複雑なマクロのモジュール化がしづらくなる。マクロのロジックがマクロ実装外のヘルパートレイトに集中していて、マクロ実装がヘルパーをインスタンス化するだけのラッパーになってしまっているのは典型的な例だ。
  2. +
  3. さらに、マクロのパラメータがマクロのコンテキストにパス依存であるため、ヘルパーと実装をつなぐのに特殊なおまじないを必要とする。
  4. +
  5. マクロが進化してくると、コンパイラとマクロ間において別のコミュニケーションのインターフェイスが必要であることが明らかになってきた。現在はコンパイラはマクロ展開しかできないが、例えばマクロを型推論に使いたいとしたらどうだろう?
  6. +
+ +マクロバンドルは、マクロ実装を `scala.reflect.macros.Macro` を継承したトレイトで実装することで、これらの問題に対する解決策となる。この基底トレイトは `c: Context` 変数を定義してあるため、マクロ実装側のシグネチャで宣言しなくても済むようになり、モジュール化を簡単にする。将来には `Macro` は `onInfer` などのコールバックメソッドを提供するかもしれない。 + + trait Macro { + val c: Context + } + +バンドルで定義されたマクロ実装を参照するのは、オブジェクトで定義された実装を参照するのと同じ方法で行われる。まずバンドル名を指定して、必要ならば型引数付きでメソッドを選択する。 + + import scala.reflect.macros.Context + import scala.reflect.macros.Macro + + trait Impl extends Macro { + def mono = c.literalUnit + def poly[T: c.WeakTypeTag] = c.literal(c.weakTypeOf[T].toString) + } + + object Macros { + def mono = macro Impl.mono + def poly[T] = macro Impl.poly[T] + } + +## マクロコンパイラ + +マクロバンドルを実装する際に気付いたのはマクロ定義とマクロ実装をリンクしている機構が硬直すぎるということだ。この機構は単に `scala/tools/nsc/typechecker/Macros.scala` でハードコードされたロジックを使っていて、マクロ定義の右辺値を静的なメソッドの参照として型検査して、そのメソッドを対応するマクロ実装に使っている。 + +これからはマクロ定義のコンパイルが拡張できるようになる。ハードコードされた実装でマクロ実装を照会するのではなく、マクロエンジンはスコープ内にある `MacroCompiler` を implicit 検索して、マクロ定義の `DefDef` を渡して `resolveMacroImpl` メソッドを呼び出し、静的なメソッドへの参照を返してもらう。もちろんこれが正しく動作するためには `resolveMacroImpl` そのものも[型指定の無い](/ja/overviews/macros/untypedmacros.html)マクロであるべきだ。 + + trait MacroCompiler { + def resolveMacroImpl(macroDef: _): _ = macro ??? + } + +この型クラスのデフォルトのインスタンス `Predef.DefaultMacroCompiler` はこれまでハードコードされていた型検査のロジックを実装する。 +代替実装を提供することで、例えばマクロ定義のためのライトウェイトな構文や `c.introduceTopLevel` を使ったアドホックに生成されるマクロ実装を提供することができる。 diff --git a/ja/overviews/macros/inference.md b/ja/overviews/macros/inference.md new file mode 100644 index 0000000000..9880e2c4c1 --- /dev/null +++ b/ja/overviews/macros/inference.md @@ -0,0 +1,182 @@ +--- +layout: overview-large +language: ja + +disqus: true + +partof: macros +num: 7 +outof: 7 +title: 型推論補助マクロ +--- +MACRO PARADISE + +**Eugene Burmako 著**
+**Eugene Yokota 訳** + +型推論補助マクロ (inference-driving macro) はマクロパラダイスと呼ばれているオフィシャル Scala リポジトリ内の実験的なブランチに含まれるリリース前の機能だ。[マクロパラダイス](/ja/overviews/macros/paradise.html)ページの説明にしたがってナイトリービルドをダウンロードしてほしい。 + +注意! これはパラダイスの実験的機能の中でも最も実験的なものだ。型マクロのような完成に近づいているものと違って、この機能は API の詳細が定まっていないだけでなく、現行の方法が正しいものであるかも確信できていない。フィードバックをこれまでになく必要としている。この[原版](/overviews/macros/inference.html)のコメント欄に直接コメントするか、[scala-internals での議論](http://groups.google.com/group/scala-internals/browse_thread/thread/ad309e68d645b775)に参加してほしい。 + +## 動機となった具体例 + +型推論補助マクロが生まれるキッカケとなったユースケースは、Miles Sabin 氏と氏が作った [shapeless](https://github.com/milessabin/shapeless) ライブラリにより提供された。Miles は 型間の同型射 (isomorphism) を表す `Iso` トレイトを定義した。 + + trait Iso[T, U] { + def to(t : T) : U + def from(u : U) : T + } + +現在は `Iso` のインスタンスは手動で定義され、暗黙の値として公開される。定義された同型射を利用したいメソッドは `Iso` 型の暗黙のパラメータを宣言して、そこに implicit 検索時に値が渡される。 + + def foo[C](c: C)(implicit iso: Iso[C, L]): L = iso.from(c) + + case class Foo(i: Int, s: String, b: Boolean) + implicit val fooIsoTuple = Iso.tuple(Foo.apply _, Foo.unapply _) + + val tp = foo(Foo(23, "foo", true)) + tp : (Int, String, Boolean) + tp == (23, "foo", true) + +見ての通り、ケースクラスとタプル間の同型射は簡単に書くことができる (実際には shapeless は Iso を用いてケースクラスと HList 間を変換するが、話を簡潔にするためにここではタプルを使う)。コンパイラは必要なメソッドを既に生成しているため、それを利用するだけでいい。残念ながら Scala 2.10.0 ではこれより単純化することは不可能で、全てのケースクラスに対して手動で implicit の `Iso` インスタンスを定義する必要がある。 + +これまでマクロはボイラープレイトを撤去するのに役立つをことを示してきたが、2.10.0 の形のままではここでは役に立たない。問題は master では数週間まえに修正されている [SI-5923](https://issues.scala-lang.org/browse/SI-5923) だけではない。 + +真の問題は `foo` のようなメソッドの適用を型検査する際に、scalac は何も知らない型引数 `L` を推論する必要があることだ (これはドメインに特化した知識なのでコンパイラのせいではない)。結果として、`Iso[C, L]` を合成する暗黙のマクロを定義したとしても、scalac はマクロを展開する前に `L` を `Nothing` と推論して全てが崩れてしまう。 + +## 型推論の内部構造 + +この数日で分かったのは Scala の型推論は `scala/tools/nsc/typechecker/Infer.scala` 内の [`inferExprInstance`](https://github.com/scalamacros/kepler/blob/d7b59f452f5fa35df48a5e0385f579c98ebf3555/src/compiler/scala/tools/nsc/typechecker/Infer.scala#L1123) と +[`inferMethodInstance`](https://github.com/scalamacros/kepler/blob/d7b59f452f5fa35df48a5e0385f579c98ebf3555/src/compiler/scala/tools/nsc/typechecker/Infer.scala#L1173) という 2つのメソッドで行われているということだ。 +今の所、型推論を使った様々なコードに対して `-Yinfer-debug` のログを表示させてみる以外、特に書くことは無い。 + + def foo[T1](x: T1) = ??? + foo(2) + + [solve types] solving for T1 in ?T1 + [infer method] solving for T1 in (x: T1)Nothing based on (Int)Nothing (solved: T1=Int) + + def bar[T2] = ??? + bar + + [solve types] solving for T2 in ?T2 + inferExprInstance { + tree C.this.bar[T2] + tree.tpe Nothing + tparams type T2 + pt ? + targs Nothing + tvars =?Nothing + } + + class Baz[T] + implicit val ibaz = new Baz[Int] + def baz[T3](implicit ibaz: Baz[T3]) = ??? + baz + + [solve types] solving for T3 in ?T3 + inferExprInstance { + tree C.this.baz[T3] + tree.tpe (implicit ibaz: C.this.Baz[T3])Nothing + tparams type T3 + pt ? + targs Nothing + tvars =?Nothing + } + inferExprInstance/AdjustedTypeArgs { + okParams + okArgs + leftUndet type T3 + } + [infer implicit] C.this.baz[T3] with pt=C.this.Baz[T3] in class C + [search] C.this.baz[T3] with pt=C.this.Baz[T3] in class C, eligible: + ibaz: => C.this.Baz[Int] + [search] considering T3 (pt contains ?T3) trying C.this.Baz[Int] against pt=C.this.Baz[T3] + [solve types] solving for T3 in ?T3 + [success] found SearchResult(C.this.ibaz, TreeTypeSubstituter(List(type T3),List(Int))) for pt C.this.Baz[=?Int] + [infer implicit] inferred SearchResult(C.this.ibaz, TreeTypeSubstituter(List(type T3),List(Int))) + + class Qwe[T] + implicit def idef[T4] = new Qwe[T4] + def qwe[T4](implicit xs: Qwe[T4]) = ??? + qwe + + [solve types] solving for T4 in ?T4 + inferExprInstance { + tree C.this.qwe[T4] + tree.tpe (implicit xs: C.this.Qwe[T4])Nothing + tparams type T4 + pt ? + targs Nothing + tvars =?Nothing + } + inferExprInstance/AdjustedTypeArgs { + okParams + okArgs + leftUndet type T4 + } + [infer implicit] C.this.qwe[T4] with pt=C.this.Qwe[T4] in class C + [search] C.this.qwe[T4] with pt=C.this.Qwe[T4] in class C, eligible: + idef: [T4]=> C.this.Qwe[T4] + [solve types] solving for T4 in ?T4 + inferExprInstance { + tree C.this.idef[T4] + tree.tpe C.this.Qwe[T4] + tparams type T4 + pt C.this.Qwe[?] + targs Nothing + tvars =?Nothing + } + [search] considering T4 (pt contains ?T4) trying C.this.Qwe[Nothing] against pt=C.this.Qwe[T4] + [solve types] solving for T4 in ?T4 + [success] found SearchResult(C.this.idef[Nothing], ) for pt C.this.Qwe[=?Nothing] + [infer implicit] inferred SearchResult(C.this.idef[Nothing], ) + [solve types] solving for T4 in ?T4 + [infer method] solving for T4 in (implicit xs: C.this.Qwe[T4])Nothing based on (C.this.Qwe[Nothing])Nothing (solved: T4=Nothing) + +## 提案 + +[マクロバンドル](/ja/overviews/macros/bundles.html)で提供されるインフラを使って、`onInfer` というコールバックを導入する。 +このコールバックが定義されていれば、コンパイラによって `inferExprInstance` と `inferMethodInstance` から呼び出される。 +これは、`inferXXX` メソッドへの引数をカプセル化し未知の型パラメータの型推論を実行するメソッドを提供する `c.TypeInferenceContext` 型のパラメータを 1つ受け取る。 + + trait Macro { + val c: Context + def onInfer(tc: c.TypeInferenceContext): Unit = tc.inferDefault() + } + + type TypeInferenceContext <: TypeInferenceContextApi + trait TypeInferenceContextApi { + def tree: Tree + def unknowns: List[Symbol] + def expectedType: Type + def actualType: Type + + // TODO: can we get rid of this couple? + def keepNothings: Boolean + def useWeaklyCompatible: Boolean + + def infer(sym: Symbol, tpe: Type): Unit + + // TODO: would be lovely to have a different signature here, namely: + // def inferDefault(sym: Symbol): Type + // so that the macro can partially rely on out-of-the-box inference + // and infer the rest afterwards + def inferDefault(): Unit + } + +このインフラがあれば、`materializeIso` マクロを書いて手動で implicit を宣言する手間を省くことができる。 +完全なソースコードは [paradise/macros](https://github.com/scalamacros/kepler/blob/paradise/macros/test/files/run/macro-programmable-type-inference/Impls_Macros_1.scala) より入手できるが、以下に主な部分を抜粋する: + + override def onInfer(tic: c.TypeInferenceContext): Unit = { + val C = tic.unknowns(0) + val L = tic.unknowns(1) + import c.universe._ + import definitions._ + val TypeRef(_, _, caseClassTpe :: _ :: Nil) = tic.expectedType // Iso[Test.Foo,?] + tic.infer(C, caseClassTpe) + val fields = caseClassTpe.typeSymbol.typeSignature.declarations.toList.collect{ case x: TermSymbol if x.isVal && x.isCaseAccessor => x } + val core = (TupleClass(fields.length) orElse UnitClass).asType.toType + val tequiv = if (fields.length == 0) core else appliedType(core, fields map (_.typeSignature)) + tic.infer(L, tequiv) + } diff --git a/ja/overviews/macros/overview.md b/ja/overviews/macros/overview.md new file mode 100644 index 0000000000..5ec52aef2a --- /dev/null +++ b/ja/overviews/macros/overview.md @@ -0,0 +1,382 @@ +--- +layout: overview-large +language: ja + +disqus: true + +partof: macros +num: 1 +outof: 7 +title: def マクロ +--- +EXPERIMENTAL + +**Eugene Burmako 著**
+**Eugene Yokota 訳** + +## 動機 + +コンパイル時におけるメタプログラミングは、それによって可能となるプログラミング技法があるため貴重なツールだ。具体的には: + + + +この概要では Scala のマクロシステムを説明する。この仕組を使ってプログラマはマクロ def (コンパイル時にコンパイラによって自動的に読み込まれ実行される関数) を書くことができる。これにより、Scala におけるコンパイル時メタプログラミングという概念が実現される。 + +## 直感 + +以下がマクロ定義のプロトタイプだ: + + def m(x: T): R = macro implRef + +一見するとマクロ定義は普通の関数定義と変わらないが、違いが 1つあってそれは本文が条件付きキーワード `macro` で始まり、次に静的なマクロ実装メソッドの識別子が続くことだ。この識別子は qualify されていてもいい (つまり、`.` で区切ってスコープ外の識別子を参照してもいいということ)。 + +もし、型検査時にコンパイラがマクロ適用 `m(args)` を見つけると、コンパイラはそのマクロに対応するマクロ実装メソッドに `args` の抽象構文木を引数として渡して呼び出すことによってマクロ適用を展開する。マクロ実装の戻り値もまた抽象構文木で、コールサイトにおいてそれはインライン化され、それが再び型検査される。 + +以下のコードはマクロ実装 `Asserts.assertImpl` を参照するマクロ定義 `assert` を宣言する (`assertImpl` の定義も後でみる): + + def assert(cond: Boolean, msg: Any) = macro Asserts.assertImpl + +そのため、`assert(x < 10, "limit exceeded")` の呼び出しはコンパイル時における以下の呼び出しにつながる: + + assertImpl(c)(<[ x < 10 ]>, <[ “limit exceeded” ]>) + +ただし、`c` はコールサイトにおいてコンパイラが収集した情報を格納したコンテキスト引数で、残りの 2つの引数は、2つの式 `x < 10` と `"limit exceeded"` を表す抽象構文木。 + +本稿においては、式 `expr` を表す抽象構文木を `<[ expr ]>` と表記する。今回提唱された Scala 言語の拡張にはこの表記法に対応するものは含まれていない。実際には、構文木は `scala.reflect.api.Trees` トレイト内の型から構築され、上記の 2つの式は以下のようになる: + + Literal(Constant("limit exceeded")) + + Apply( + Select(Ident(newTermName("x")), newTermName("$less"), + List(Literal(Constant(10))))) + +ここに `assert` マクロの実装の一例を載せる: + + import scala.reflect.macros.Context + import scala.language.experimental.macros + + object Asserts { + def raise(msg: Any) = throw new AssertionError(msg) + def assertImpl(c: Context) + (cond: c.Expr[Boolean], msg: c.Expr[Any]) : c.Expr[Unit] = + if (assertionsEnabled) + <[ if (!cond) raise(msg) ]> + else + <[ () ]> + } + +この例が示すとおり、マクロ実装はいくつかのパラメータリストを持つ。まず `scala.reflect.macros.Context` 型の パラメータを 1つ受け取るリスト。次に、マクロ定義のパラメータと同じ名前を持つパラメータを列挙したリスト。しかし、もとのマクロのパラメータの型 `T` の代わりにマクロ実装のパラメータは `c.Expr[T]` 型を持つ。`Expr[T]` は `Context` に定義され `T` 型の抽象構文木をラッピングする。マクロ実装 `assertImpl` の戻り型もまたラッピングされた構文木で、`c.Expr[Unit]` 型を持つ。 + +また、マクロは実験的で、高度な機能だと考えられているため、明示的に有効化される必要があることに注意してほしい。 +これは、ファイルごとに `import scala.language.experimental.macros` と書くか、コンパイルごとに (コンパイラスイッチとして) `-language:experimental.macros` を用いることで行われる。 + +### 多相的なマクロ + +マクロ定義とマクロ実装の両方ともジェネリックにすることができる。もしマクロ実装に型パラメータがあれば、マクロ定義の本文において実際の型引数が明示的に渡される必要がある。実装内での型パラメータは context bounds の `TypeTag` と共に宣言することができる。その場合、適用サイトでの実際の型引数を記述した型タグがマクロの展開時に一緒に渡される。 + +以下のコードはマクロ実装 `QImpl.map` を参照するマクロ定義 `Queryable.map` を宣言する: + + class Queryable[T] { + def map[U](p: T => U): Queryable[U] = macro QImpl.map[T, U] + } + + object QImpl { + def map[T: c.TypeTag, U: c.TypeTag] + (c: Context) + (p: c.Expr[T => U]): c.Expr[Queryable[U]] = ... + } + +ここで、型が `Queryable[String]` である値 `q` があるとして、そのマクロ呼び出し + + q.map[Int](s => s.length) + +を考える。この呼び出しは以下の reflective なマクロ呼び出しに展開される。 + + QImpl.map(c)(<[ s => s.length ]>) + (implicitly[TypeTag[String]], implicitly[TypeTag[Int]]) + +## 完全な具体例 + +この節ではコンパイル時に文字列を検査して形式を適用する `printf` マクロを具体例として、最初から最後までの実装をみていく。 +説明を簡略化するために、ここではコンソールの Scala コンパイラを用いるが、後に説明があるとおりマクロは Maven や sbt からも使える。 + +マクロを書くには、まずマクロの窓口となるマクロ定義から始める。 +マクロ定義はシグネチャに思いつくまま好きなものを書ける普通の関数だ。 +しかし、その本文は実装への参照のみを含む。 +前述のとおり、マクロを定義するは `scala.language.experimental.macros` をインポートするか、特殊なコンパイラスイッチ `-language:experimental.macros` を用いて有効化する必要がある。 + + import scala.language.experimental.macros + def printf(format: String, params: Any*): Unit = macro printf_impl + +マクロ実装はそれを使うマクロ定義に対応する必要がある (通常は 1つだが、複数のマクロ定義を宣言することもできる)。簡単に言うと、マクロ定義のシグネチャ内の全ての型 `T` のパラメータはマクロ実装のシグネチャ内では `c.Expr[T]` となる必要がある。このルールの完全なリストはかなり込み入ったものだが、これは問題とならない。もしコンパイラが気に入らなければ、エラーメッセージに期待されるシグネチャを表示するからだ。 + + import scala.reflect.macros.Context + def printf_impl(c: Context)(format: c.Expr[String], params: c.Expr[Any]*): c.Expr[Unit] = ... + +コンパイラ API は `scala.reflect.macros.Context` から使うことができる。そのうち最も重要な部分であるリフレクション API は `c.universe` から使える。 +よく使われる多くの関数や型を含むため、`c.universe._` をインポートするのが慣例となっている: + + import c.universe._ + +まずマクロは渡された書式文字列をパースする必要がある。 +マクロはコンパイル時に実行されるため、値ではなく構文木に対してはたらく。 +そのため、`printf` マクロの書式文字列のパラメータは `java.lang.String` 型のオブジェクトではなくコンパイル時リテラルとなる。 +また、`printf(get_format(), ...)` だと `format` は文字列リテラルではなく関数の適用を表す AST であるため、以下のコードでは動作しない。任意の式に対して動作するようにマクロを修正するのは読者への練習問題とする。 + + val Literal(Constant(s_format: String)) = format.tree + +典型的なマクロは Scala のコードを表す AST (抽象構文木) を作成する必要がある。(このマクロも例に漏れない) +Scala コードの生成については[リフレクションの概要](http://docs.scala-lang.org/ja/overviews/reflection/overview.html)を参照してほしい。AST の作成の他に以下のコードは型の操作も行う。 +`Int` と `String` に対応する Scala 型をどうやって取得しているのかに注目してほしい。 +リンクしたリフレクションの概要で型の操作の詳細を説明する。 +コード生成の最終ステップでは、全ての生成されたコードを `Block` へと組み合わせる。 +`reify` は AST を簡単に作成する方法を提供する。 + + val evals = ListBuffer[ValDef]() + def precompute(value: Tree, tpe: Type): Ident = { + val freshName = newTermName(c.fresh("eval$")) + evals += ValDef(Modifiers(), freshName, TypeTree(tpe), value) + Ident(freshName) + } + + val paramsStack = Stack[Tree]((params map (_.tree)): _*) + val refs = s_format.split("(?<=%[\\w%])|(?=%[\\w%])") map { + case "%d" => precompute(paramsStack.pop, typeOf[Int]) + case "%s" => precompute(paramsStack.pop, typeOf[String]) + case "%%" => Literal(Constant("%")) + case part => Literal(Constant(part)) + } + + val stats = evals ++ refs.map(ref => reify(print(c.Expr[Any](ref).splice)).tree) + c.Expr[Unit](Block(stats.toList, Literal(Constant(())))) + +以下のコードは `printf` マクロの完全な定義を表す。 +追随するには、空のディレクトリを作り、コードを `Macros.scala` という名前の新しいファイルにコピーする。 + + import scala.reflect.macros.Context + import scala.collection.mutable.{ListBuffer, Stack} + + object Macros { + def printf(format: String, params: Any*): Unit = macro printf_impl + + def printf_impl(c: Context)(format: c.Expr[String], params: c.Expr[Any]*): c.Expr[Unit] = { + import c.universe._ + val Literal(Constant(s_format: String)) = format.tree + + val evals = ListBuffer[ValDef]() + def precompute(value: Tree, tpe: Type): Ident = { + val freshName = newTermName(c.fresh("eval$")) + evals += ValDef(Modifiers(), freshName, TypeTree(tpe), value) + Ident(freshName) + } + + val paramsStack = Stack[Tree]((params map (_.tree)): _*) + val refs = s_format.split("(?<=%[\\w%])|(?=%[\\w%])") map { + case "%d" => precompute(paramsStack.pop, typeOf[Int]) + case "%s" => precompute(paramsStack.pop, typeOf[String]) + case "%%" => Literal(Constant("%")) + case part => Literal(Constant(part)) + } + + val stats = evals ++ refs.map(ref => reify(print(c.Expr[Any](ref).splice)).tree) + c.Expr[Unit](Block(stats.toList, Literal(Constant(())))) + } + } + +`printf` マクロを使うには、同じディレクトリ内に別のファイル `Test.scala` を作って以下のコードをコピーする。 +マクロを使用するのは関数を呼び出すのと同じぐらいシンプルであることに注目してほしい。`scala.language.experimental.macros` をインポートする必要も無い。 + + object Test extends App { + import Macros._ + printf("hello %s!", "world") + } + +マクロ機構の重要な一面は別コンパイルだ。マクロ展開を実行するためには、コンパイラはマクロ実装を実行可能な形式で必要とする。そのため、マクロ実装はメインのコンパイルを行う前にコンパイルされている必要がある。 +これをしないと、以下のようなエラーをみることになる: + + ~/Projects/Kepler/sandbox$ scalac -language:experimental.macros Macros.scala Test.scala + Test.scala:3: error: macro implementation not found: printf (the most common reason for that is that + you cannot use macro implementations in the same compilation run that defines them) + pointing to the output of the first phase + printf("hello %s!", "world") + ^ + one error found + + ~/Projects/Kepler/sandbox$ scalac Macros.scala && scalac Test.scala && scala Test + hello world! + +## コツとトリック + +### コマンドライン Scala コンパイラを用いてマクロを使う + +このシナリオは前節で説明したとおりだ。つまり、マクロとそれを使用するコードを別に呼び出した `scalac` によってコンパイルすることで、全てうまくいくはずだ。REPL をつかっているなら、さらに都合がいい。なぜなら REPL はそれぞれの行を独立したコンパイルとして扱うため、マクロを定義してすぐに使うことができる。 + +  +### Maven か SBT を用いてマクロを使う + +本稿での具体例では最もシンプルなコマンドラインのコンパイルを使っているが、マクロは Maven や SBT などのビルドツールからも使うことができる。完結した具体例としては [https://github.com/scalamacros/sbt-example](https://github.com/scalamacros/sbt-example) か [https://github.com/scalamacros/maven-example](https://github.com/scalamacros/maven-example) を見てほしいが、要点は以下の 2点だ: + + + +### Scala IDE か Intellij IDEA を用いてマクロを使う + +別プロジェクトに分かれている限り、Scala IDE と Intellij IDEA の両方において、マクロは正しく動作することが分かっている。 + +### マクロのデバッグ + +マクロのデバッグ、すなわちマクロ展開を駆動している論理のデバッグは比較的容易だ。マクロはコンパイラ内で展開されるため、デバッガ内でコンパイラを実行するだけでいい。そのためには、以下を実行する必要がある: + +
    +
  1. デバッグ設定のクラスパスに Scala home の lib ディレクトリ内の全て (!) のライブラリを追加する。(これは、scala-library.jarscala-reflect.jarscala-compiler.jar、そして forkjoin.jar の jar ファイルを含む。
  2. +
  3. scala.tools.nsc.Main をエントリーポイントに設定する。
  4. +
  5. コンパイラのコマンドラインの引数を -cp <マクロのクラスへのパス> Test.scala
  6. に設定する。ただし、Test.scala は展開されるマクロの呼び出しを含むテストファイルとする。 +
+ +上の手順をふめば、マクロ実装内にブレークポイントを置いてデバッガを起動できるはずだ。 + +ツールによる特殊なサポートが本当に必要なのはマクロ展開の結果 (つまり、マクロによって生成されたコード) のデバッグだ。このコードは手動で書かれていないため、ブレークポイントを設置することはできず、ステップ実行することもできない。Scala IDE と Intellij IDEA のチームはいずれそれぞれのデバッガにこのサポートを追加することになると思うが、それまでは展開されたマクロをデバッグする唯一の方法は `-Ymacro-debug-lite` という print を使った診断だけだ。これは、マクロによって生成されたコードを表示して、また生成されたコードの実行を追跡して println する。 + +### 生成されたコードの検査 + +`-Ymacro-debug-lite` を用いることで展開されたコードを準 Scala 形式と生の AST 形式の両方でみることができる。それぞれに利点があり、前者は表層的な解析に便利で、後者はより詳細なデバッグに不可欠だ。 + + ~/Projects/Kepler/sandbox$ scalac -Ymacro-debug-lite Test.scala + typechecking macro expansion Macros.printf("hello %s!", "world") at + source-C:/Projects/Kepler/sandbox\Test.scala,line-3,offset=52 + { + val eval$1: String = "world"; + scala.this.Predef.print("hello "); + scala.this.Predef.print(eval$1); + scala.this.Predef.print("!"); + () + } + Block(List( + ValDef(Modifiers(), newTermName("eval$1"), TypeTree().setType(String), Literal(Constant("world"))), + Apply( + Select(Select(This(newTypeName("scala")), newTermName("Predef")), newTermName("print")), + List(Literal(Constant("hello")))), + Apply( + Select(Select(This(newTypeName("scala")), newTermName("Predef")), newTermName("print")), + List(Ident(newTermName("eval$1")))), + Apply( + Select(Select(This(newTypeName("scala")), newTermName("Predef")), newTermName("print")), + List(Literal(Constant("!"))))), + Literal(Constant(()))) + +### 捕獲されない例外を投げるマクロ + +マクロが捕獲されない例外を投げるとどうなるだろうか?例えば、`printf` に妥当ではない入力を渡してクラッシュさせてみよう。 +プリントアウトが示すとおり、特に劇的なことは起きない。コンパイラは自身を行儀の悪いマクロから守る仕組みになっているため、スタックトレースのうち関係のある部分を表示してエラーを報告するだけだ。 + + ~/Projects/Kepler/sandbox$ scala + Welcome to Scala version 2.10.0-20120428-232041-e6d5d22d28 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_25). + Type in expressions to have them evaluated. + Type :help for more information. + + scala> import Macros._ + import Macros._ + + scala> printf("hello %s!") + :11: error: exception during macro expansion: + java.util.NoSuchElementException: head of empty list + at scala.collection.immutable.Nil$.head(List.scala:318) + at scala.collection.immutable.Nil$.head(List.scala:315) + at scala.collection.mutable.Stack.pop(Stack.scala:140) + at Macros$$anonfun$1.apply(Macros.scala:49) + at Macros$$anonfun$1.apply(Macros.scala:47) + at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:237) + at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:237) + at scala.collection.IndexedSeqOptimized$class.foreach(IndexedSeqOptimized.scala:34) + at scala.collection.mutable.ArrayOps.foreach(ArrayOps.scala:39) + at scala.collection.TraversableLike$class.map(TraversableLike.scala:237) + at scala.collection.mutable.ArrayOps.map(ArrayOps.scala:39) + at Macros$.printf_impl(Macros.scala:47) + + printf("hello %s!") + ^ + +### 警告とエラーの報告 + +ユーザと対話するための正式な方法は `scala.reflect.macros.FrontEnds` のメソッドを使うことだ。 +`c.error` はコンパイルエラーを報告し、`c.info` は警告を発令し、`c.abort` はエラーを報告しマクロの実行を停止する。 + + scala> def impl(c: Context) = + c.abort(c.enclosingPosition, "macro has reported an error") + impl: (c: scala.reflect.macros.Context)Nothing + + scala> def test = macro impl + defined term macro test: Any + + scala> test + :32: error: macro has reported an error + test + ^ + +[SI-6910](https://issues.scala-lang.org/browse/SI-6910) に記述されているとおり、現時点ではある位置から複数の警告やエラーの報告はサポートされていないことに注意してほしい。そのため、ある位置で最初のエラーか警告だけが報告され他は失くなってしまう。(ただし、同じ位置で後から報告されてもエラーは警告よりも優先される) + +  +### より大きなマクロを書く + +マクロ実装が実装メソッドの本文におさまりきらなくなって、モジュール化の必要性が出てくると、コンテキストパラメータを渡して回る必要があることに気付くだろう。マクロを定義するのに必要なもののほとんどがこのコンテキストにパス依存しているからだ。 + +1つの方法としては `Context` 型のパラメータを受け取るクラスを書いて、マクロ実装をそのクラス内のメソッドに分けるという方法がある。これは一見自然でシンプルにみえるが、実は正しく書くのは難しい。以下に典型的なコンパイルエラーを示す。 + + scala> class Helper(val c: Context) { + | def generate: c.Tree = ??? + | } + defined class Helper + + scala> def impl(c: Context): c.Expr[Unit] = { + | val helper = new Helper(c) + | c.Expr(helper.generate) + | } + :32: error: type mismatch; + found : helper.c.Tree + (which expands to) helper.c.universe.Tree + required: c.Tree + (which expands to) c.universe.Tree + c.Expr(helper.generate) + ^ + +このコードの問題はパス依存型のミスマッチだ。同じ `c` を使って helper を構築したにもかかわらず、Scala コンパイラは `impl` の `c` が `Helper` の `c` と同じものであることが分からない。 + +幸いなことに、少し助けてやるだけでコンパイラは何が起こっているのか気付くことができる。様々ある解法の1つは細別型 (refinement type) を使うことだ。以下の例はそのアイディアの最も簡単な例だ。例えば、`Context` から `Helper` への暗黙の変換を書いてやることで明示的なインスタンス化を回避して呼び出しを単純化することができる。 + + scala> abstract class Helper { + | val c: Context + | def generate: c.Tree = ??? + | } + defined class Helper + + scala> def impl(c1: Context): c1.Expr[Unit] = { + | val helper = new { val c: c1.type = c1 } with Helper + | c1.Expr(helper.generate) + | } + impl: (c1: scala.reflect.macros.Context)c1.Expr[Unit] + +もう1つの方法はコンテキストのアイデンティティを明示的な型パラメータとして渡す方法だ。`Helper` のコンストラクタが `c.type` を用いて `Helper.c` と元の `c` が同じであることを表していることに注目してほしい。Scala の型推論は単独ではこれを解くことができないため、手伝ってあげているわけだ。 + + scala> class Helper[C <: Context](val c: C) { + | def generate: c.Tree = ??? + | } + defined class Helper + + scala> def impl(c: Context): c.Expr[Unit] = { + | val helper = new Helper[c.type](c) + | c.Expr(helper.generate) + | } + impl: (c: scala.reflect.macros.Context)c.Expr[Unit] + +## 他の例 + +Scala マクロは、既に何人ものアーリーアダプターがいる。コミュニティが積極的に関わってくれたお陰で僕らは素早くプロトタイプを行なうことができたし、結果としてお手本として使えるコードがいくつかできた。最新の状況に関しては [http://scalamacros.org/news/2012/11/05/status-update.html](http://scalamacros.org/news/2012/11/05/status-update.html) を参照してほしい。 + +Adam Warski 氏による Scala マクロのチュートリアルも推薦したい: [http://www.warski.org/blog/2012/12/starting-with-scala-macros-a-short-tutorial](http://www.warski.org/blog/2012/12/starting-with-scala-macros-a-short-tutorial)。これはデバッグ表示用のマクロを具体例としたステップ・バイ・ステップガイドで SBT プロジェクトのセットアップや、`reify` と `splice` の用例、そして手動での AST の組み立てなどを解説している。 diff --git a/ja/overviews/macros/paradise.md b/ja/overviews/macros/paradise.md new file mode 100644 index 0000000000..277a708c6f --- /dev/null +++ b/ja/overviews/macros/paradise.md @@ -0,0 +1,27 @@ +--- +layout: overview-large +language: ja + +disqus: true + +partof: macros +num: 2 +outof: 7 +title: マクロパラダイス +--- +MACRO PARADISE + +**Eugene Burmako 著**
+**Eugene Yokota 訳** + +マクロパラダイスとはオフィシャルの Scala リポジトリ内の `paradise/macros` ブランチの別名のことで、Scala の安定性を損ねることなくマクロの迅速な開発を行うことを目的としている。このブランチに関するより詳しいことは[この講演](http://scalamacros.org/news/2012/12/18/macro-paradise.html)を参考にしてほしい。 + +Sonatype にスナップショットのアーティファクトが公開されるようにナイトリービルドを設定してある。SBT を用いたナイトリーの使い方は [https://github.com/scalamacros/sbt-example-paradise](https://github.com/scalamacros/sbt-example-paradise) を参考にしてほしいが、要点をまとめるとマクロパラダイスを使うのはビルド定義に以下の 3行を加えるだけでいい (マクロを使うように [SBT をセットアップ](/ja/overviews/macros/overview.html#using_macros_with_maven_or_sbt)済みであることが前提だが): + + scalaVersion := "2.11.0-SNAPSHOT" + scalaOrganization := "org.scala-lang.macro-paradise" + resolvers += Resolver.sonatypeRepo("snapshots") + +現行の SBT ではカスタムの `scala-compiler.jar` を新しいスナップショットに更新するときに問題が発生する。症状を説明しよう。最初にマクロパラダイスを使ってプロジェクトをコンパイルしたときは全て正しく動作する。しかし、数日後に `sbt update` を実行すると、SBT は新しい `scala-library.jar` と `scala-reflect.jar` のナイトリービルドを取得するが、`scala-compiler.jar` は取得しない。これを解決するには、`sbt reboot full` を使って SBT 自身と内部で使われている scalac のインスタンスをダウンロードし直す必要がある。この残念な問題に関しては現在調査中だが、それまでは[メーリングリストにて](https://groups.google.com/forum/?fromgroups=#!topic/simple-build-tool/UalhhX4lKmw/discussion)この問題に関する議論に参加することができる。 + +パラダイスのナイトリーに対応した Scaladocs は [Jenkins サーバ](https://scala-webapps.epfl.ch/jenkins/view/misc/job/macro-paradise-nightly-main/ws/dists/latest/doc/scala-devel-docs/api/index.html)にある。例えば、[scala.reflect.macros.Synthetics](https://scala-webapps.epfl.ch/jenkins/view/misc/job/macro-paradise-nightly-main/ws/dists/latest/doc/scala-devel-docs/api/index.html#scala.reflect.macros.Synthetics) はトップレベルの定義に関する新しい API だ。 diff --git a/ja/overviews/macros/quasiquotes.md b/ja/overviews/macros/quasiquotes.md new file mode 100644 index 0000000000..5b977a47fe --- /dev/null +++ b/ja/overviews/macros/quasiquotes.md @@ -0,0 +1,105 @@ +--- +layout: overview-large +language: ja + +disqus: true + +partof: macros +num: 4 +outof: 7 +title: 準クォート +--- +MACRO PARADISE + +**Eugene Burmako 著**
+**Eugene Yokota 訳** + +準クォート (quasiquote) はマクロパラダイスと呼ばれているオフィシャル Scala リポジトリ内の実験的なブランチに含まれるリリース前の機能だ。[マクロパラダイス](/ja/overviews/macros/paradise.html)ページの説明にしたがってナイトリービルドをダウンロードしてほしい。 + +## 直観 + +あるクラスかオブジェクトのテンプレートを受け取り、テンプレート内の全てのメソッドを複製して `future` にラッピングされた非同期版を定義する `Lifter` [型マクロ](/ja/overviews/macros/typemacros.html)を考える。 + + class D extends Lifter { + def x = 2 + // def asyncX = future { 2 } + } + + val d = new D + d.asyncX onComplete { + case Success(x) => println(x) + case Failure(_) => println("failed") + } + +そのようなマクロの実装は以下のコードの抜粋のようにできる。この取得、分解、生成コードでラッピング、再構築という流れはマクロ作者にとっては見慣れたものだ。 + + case ClassDef(_, _, _, Template(_, _, defs)) => + val defs1 = defs collect { + case DefDef(mods, name, tparams, vparamss, tpt, body) => + val tpt1 = if (tpt.isEmpty) tpt else AppliedTypeTree( + Ident(newTermName("Future")), List(tpt)) + val body1 = Apply( + Ident(newTermName("future")), List(body)) + val name1 = newTermName("async" + name.capitalize) + DefDef(mods, name1, tparams, vparamss, tpt1, body1) + } + Template(Nil, emptyValDef, defs ::: defs1) + +しかし、ベテランのマクロ作者でもこのコードは、かなりシンプルであることは確かだが、例えば、`AppliedTypeTree` と `Apply` の違いなどコードの内部表現の詳細に理解していることを必要とした必要以上に冗長なものであることを認めるだろう。準クォートはパラメータ化された Scala のコードを Scala を使って表現できるドメイン特化言語を提供する: + + val q"class $name extends Liftable { ..$body }" = tree + + val newdefs = body collect { + case q"def $name[..$tparams](...$vparamss): $tpt = $body" => + val tpt1 = if (tpt.isEmpty) tpt else tq"Future[$tresult]" + val name1 = newTermName("async" + name.capitalize) + q"def $name1[..$tparams](...$vparamss): $tpt1 = future { $body }" + } + + q"class $name extends AnyRef { ..${body ++ newdefs} }" + +現行の準クォートは [SI-6842](https://issues.scala-lang.org/browse/SI-6842) のため、上記のようには簡潔に書くことができない。[多くのキャスト](https://gist.github.com/7ab617d054f28d68901b)を適用して使えるようになる。 + +## 詳細 + +準クォートは `scala.reflect.api.Universe` cake の一部として実装されているため、マクロから準クォートを使うには `import c.universe._` とするだけでいい。公開されている API は `q` と `tq` [文字列補間子](/ja/overviews/core/string-interpolation.html)を提供し (値と型の準クォートに対応する)、構築と分解の両方をサポートする。つまり、普通のコードとパターンケースの左辺値において使うことができる。 + + + + + + + + + +
補間子対象構築分解
q値構文木q"future{ $body }"case q"future{ $body }" =>
tq型構文木tq"Future[$t]"case tq"Future[$t]" =>
+ +普通の文字列補間子と違い、準クォートは単独の構文木、構文木のリスト、構文木のリストのリストの挿入または抽出を区別するために複数のスプライシングの方法をサポートしている。スプライス対象とスプライス演算子の基数のミスマッチはコンパイル時のエラーとなる。 + + scala> val name = TypeName("C") + name: reflect.runtime.universe.TypeName = C + + scala> val q"class $name1" = q"class $name" + name1: reflect.runtime.universe.Name = C + + scala> val args = List(Literal(Constant(2))) + args: List[reflect.runtime.universe.Literal] = List(2) + + scala> val q"foo(..$args1)" = q"foo(..$args)" + args1: List[reflect.runtime.universe.Tree] = List(2) + + scala> val argss = List(List(Literal(Constant(2))), List(Literal(Constant(3)))) + argss: List[List[reflect.runtime.universe.Literal]] = List(List(2), List(3)) + + scala> val q"foo(...$argss1)" = q"foo(...$argss)" + argss1: List[List[reflect.runtime.universe.Tree]] = List(List(2), List(3)) + +## コツとトリック + +### Liftable + +非構文木のスプライシングを簡易化するために、準クォートは `Lifttable` 型クラスを提供して、値がスプライスされたときにどのように構文木に変換されるかを定義する。プリミティブ型と文字列を `Literal(Constant(...))` にラッピングする `Liftable` インスタンスは提供されている。簡単なケースクラスやリストのための独自のインスタンスを定義することをお勧めする (また、[SI-6839](https://issues.scala-lang.org/browse/SI-6839) を参照のこと)。 + + trait Liftable[T] { + def apply(universe: api.Universe, value: T): universe.Tree + } diff --git a/ja/overviews/macros/typemacros.md b/ja/overviews/macros/typemacros.md new file mode 100644 index 0000000000..9242cd1fe4 --- /dev/null +++ b/ja/overviews/macros/typemacros.md @@ -0,0 +1,104 @@ +--- +layout: overview-large +language: ja + +disqus: true + +partof: macros +num: 3 +outof: 7 +title: 型マクロ +--- +MACRO PARADISE + +**Eugene Burmako 著**
+**Eugene Yokota 訳** + +型マクロ (type marco) はマクロパラダイスと呼ばれているオフィシャル Scala リポジトリ内の実験的なブランチに含まれるリリース前の機能だ。[マクロパラダイス](/ja/overviews/macros/paradise.html)ページの説明にしたがってナイトリービルドをダウンロードしてほしい。 + +## 直観 + +def マクロがコンパイラが特定をメソッドの呼び出しを見つけた時にカスタム関数を実行させることができるように、型マクロは特定の型が使われた時にコンパイラにフックできる。以下のコードの抜粋は、データベースのテーブルから簡単な CRUD 機能を持ったケースクラスを生成する `H2Db` マクロの定義と使用例を示す。 + + type H2Db(url: String) = macro impl + + object Db extends H2Db("coffees") + + val brazilian = Db.Coffees.insert("Brazilian", 99, 0) + Db.Coffees.update(brazilian.copy(price = 10)) + println(Db.Coffees.all) + +`H2Db` マクロの完全なソースコードは [Github にて](https://github.com/xeno-by/typemacros-h2db)提供して、本稿では重要な点だけをかいつまんで説明する。まず、マクロは、コンパイル時にデータベースに接続することで静的に型付けされたデータベースのラッパーを生成する。(構文木の生成に関しては[リフレクションの概要](http://docs.scala-lang.org/ja/overviews/reflection/overview.html)にて説明する) 次に、NEW `c.introduceTopLevel` API ([Scaladoc](https://scala-webapps.epfl.ch/jenkins/view/misc/job/macro-paradise-nightly-main/ws/dists/latest/doc/scala-devel-docs/api/index.html#scala.reflect.macros.Synthetics)) を用いて生成されたラッパーをコンパイラによって管理されているトップレベル定義のリストに挿入する。最後に、マクロは生成されたクラスのスーパーコンストラクタを呼び出す `Apply` ノードを返す。注意 `c.Expr[T]` に展開される def マクロとちがって型マクロは `c.Tree` に展開されることに注意してほしい。これは、`Expr` が値を表すのに対して、型マクロは型に展開することによる。 + + type H2Db(url: String) = macro impl + + def impl(c: Context)(url: c.Expr[String]): c.Tree = { + val name = c.freshName(c.enclosingImpl.name).toTypeName + val clazz = ClassDef(..., Template(..., generateCode())) + c.introduceTopLevel(c.enclosingPackage.pid.toString, clazz) + val classRef = Select(c.enclosingPackage.pid, name) + Apply(classRef, List(Literal(Constant(c.eval(url))))) + } + + object Db extends H2Db("coffees") + // equivalent to: object Db extends Db$1("coffees") + +合成クラスを生成してその参照へと展開するかわりに、型マクロは `Template` 構文木を返すことでそのホストを変換することもできる。scalac 内部ではクラス定義とオブジェクト定義の両方とも `Template` 構文木の簡単なラッパーとして表現されているため、テンプレートへと展開することで型マクロはクラスやオブジェクトの本文全体を書き換えることができるようになる。このテクニックを活用した例も [Github で](https://github.com/xeno-by/typemacros-lifter)みることができる。 + + type H2Db(url: String) = macro impl + + def impl(c: Context)(url: c.Expr[String]): c.Tree = { + val Template(_, _, existingCode) = c.enclosingTemplate + Template(..., existingCode ++ generateCode()) + } + + object Db extends H2Db("coffees") + // equivalent to: object Db { + // + // + // } + +## 詳細 + +型マクロは def マクロと型メンバのハイブリッドを表す。ある一面では、型マクロはメソッドのように定義される (例えば、値の引数を取ったり、context bound な型パラメータを受け取ったりできる)。一方で、型マクロは型と同じ名前空間に属し、そのため型が期待される位置においてのみ使うことができるため、型や型マクロなどのみをオーバーライドすることができる。(より網羅的な例は [Github](https://github.com/scalamacros/kepler/blob/paradise/macros/test/files/run/macro-typemacros-used-in-funny-places-a/Test_2.scala) を参照してほしい) + + + + + + + + + + + + + + + +
機能def マクロ型マクロ型メンバ
定義と実装に分かれているYesYesNo
値パラメータを取ることができるYesYesNo
型パラメータを取ることができるYesYesYes
変位指定付きの 〃NoNoYes
context bounds 付きの 〃YesYesNo
オーバーロードすることができるYesYesNo
継承することができるYesYesYes
オーバーライドしたりされたりできるYesYesYes
+ +Scala のプログラムにおいて型マクロは、type、applied type、parent type、new、そして annotation という 5つ役割 (role) のうちの 1つとして登場する。マクロが使われた役割によって許される展開は異なっている。また、役割は NEW `c.macroRole` API ([Scaladoc](https://scala-webapps.epfl.ch/jenkins/view/misc/job/macro-paradise-nightly-main/ws/dists/latest/doc/scala-devel-docs/api/index.html#scala.reflect.macros.Enclosures)) によって検査することができる。 + + + + + + + + + + + + +
役割使用例クラス非クラス?Apply?Template?
type def x: TM(2)(3) = ???YesYesNoNo
applied type class C[T: TM(2)(3)]YesYesNoNo
parent type class C extends TM(2)(3)
new TM(2)(3){}
YesNoYesYes
new new TM(2)(3)YesNoYesNo
annotation @TM(2)(3) class CYesNoYesNo
+ +要点をまとめると、展開された型マクロは型マクロの使用をそれが返す構文木に置き換える。ある展開が理にかなっているかどうかを考えるには、頭の中でマクロの使用例を展開される構文木で置き換えてみて結果のプログラムが正しいか確かめてみればいい。 + +例えば、 `class C extends TM(2)(3)` の中で `TM(2)(3)` のように使われている型マクロは `class C extends B(2)` となるように `Apply(Ident(newTypeName("B")), List(Literal(Constant(2))))` と展開することができる。しかし、同じ展開は `TM(2)(3)` が `def x: TM(2)(3) = ???` の中の型として使われた場合は `def x: B(2) = ???` となるため、意味を成さない。(ただし、`B` そのものが型マクロではないとする。その場合は再帰的に展開され、その展開の結果がプログラムの妥当性を決定する。) + +## コツとトリック + +### クラスやオブジェクトの生成 + +[StackOverflow](http://stackoverflow.com/questions/13795490/how-to-use-type-calculated-in-scala-macro-in-a-reify-clause) でも説明したが、型マクロを作っていると `reify` がどんどん役に立たなくなっていくことに気付くだろう。その場合は、手で構文木を構築するだけではなく、マクロパラダイスにあるもう1つの実験的機能である[準クォート](/ja/overviews/macros/quasiquotes.html)を使うことも検討してみてほしい。 diff --git a/ja/overviews/macros/untypedmacros.md b/ja/overviews/macros/untypedmacros.md new file mode 100644 index 0000000000..5a77ff3e28 --- /dev/null +++ b/ja/overviews/macros/untypedmacros.md @@ -0,0 +1,63 @@ +--- +layout: overview-large +language: ja + +disqus: true + +partof: macros +num: 5 +outof: 7 +title: 型指定の無いマクロ +--- +MACRO PARADISE + +**Eugene Burmako 著**
+**Eugene Yokota 訳** + +型指定の無いマクロ (untyped macro) はマクロパラダイスと呼ばれているオフィシャル Scala リポジトリ内の実験的なブランチに含まれるリリース前の機能だ。[マクロパラダイス](/ja/overviews/macros/paradise.html)ページの説明にしたがってナイトリービルドをダウンロードしてほしい。 + +## 直観 + +静的に型付けされることは素晴らしいが、それは時として重荷ともなりうる。例えば、Alois Cochard 氏の型マクロを使った列挙型の実装の実験、いわゆる [Enum Paradise](https://github.com/aloiscochard/enum-paradise) をみてみよう。Alois は以下のように、ライトウェイトなスペックから列挙型モジュールを生成する型マクロを書かなければいけない: + + object Days extends Enum('Monday, 'Tuesday, 'Wednesday...) + +`Monday` や `Friday` のようにクリーンな識別子名を使う代わりに、タイプチェッカーが存在しない識別子に関して怒らないようにこれらの名前をクォートする必要がある。`Enum` マクロでは既存のバインディングを参照しているのではなく、新しいものを導入したいわけだが、コンパイラはマクロに渡されるものを全て型検査しようとするためこれを許さない。 + +`Enum` マクロがどのように実装されているかをマクロ定義と実装のシグネチャを読むことでみていこう。マクロ定義のシグネチャに `symbol: Symbol*` と書いてあることが分かる。これで、対応する引数の型検査をコンパイラに強制している: + + type Enum(symbol: Symbol*) = macro Macros.enum + object Macros { + def enum(c: Context)(symbol: c.Expr[Symbol]*): c.Tree = ... + } + +型指定の無いマクロはマクロに渡された引数の型検査をコンパイラの代わりに実行すると伝える記法と機構を提供する。 +そのためには、単にマクロ定義のパラメータ型をアンダースコアで置き換えマクロ定義のパラメータ型を `c.Tree` とする: + + type Enum(symbol: _*) = macro Macros.enum + object Macros { + def enum(c: Context)(symbol: c.Tree*): c.Tree = ... + } + +## 詳細 + +型検査を停止するアンダースコアは Scala プログラムの中で以下の 3ヶ所において使うことができる: + +
    +
  1. マクロへのパラメータ型
  2. +
  3. マクロへの可変長パラメータ型
  4. +
  5. マクロの戻り値型
  6. +
+ +マクロ以外や複合型の一部としてのアンダースコアの使用は期待通り動作しない。 +前者はコンパイルエラーに、後者、例えば `List[_]` は通常通り存在型を返すだろう。 + +もしマクロに型指定の無いパラメータがあった場合、マクロ展開を型付けする際にタイプチェッカーは引数に関しては何もせずに型指定の無いままマクロに渡す。もしいくつかのパラメータが型アノテーションを持っていたとしても、現行では無視される。これは将来改善される予定だ: [SI-6971](https://issues.scala-lang.org/browse/SI-6971)。引数が型検査されていないため、implicit の解決や型引数の推論は実行されない (しかし、両者ともそれぞれ `c.typeCheck` と `c.inferImplicitValue` として実行できる)。 + +明示的に渡された型引数はそのままマクロに渡される。もし型引数が渡されなかった場合は、値引数を型検査しない範囲で可能な限り型引数を推論してマクロに渡す。つまり、型引数は型検査されるということだが、この制約は将来無くなるかもしれない: [SI-6972](https://issues.scala-lang.org/browse/SI-6972)。 + +もし、def マクロが型指定の無い戻り値を持つ場合、マクロ展開後に実行される 2つの型検査のうち最初のものが省略される。ここで復習しておくと、def マクロが展開されるとまずその定義の戻り値の型に対して型検査され、次に展開されたものに期待される型に対して型検査が実行される。これに関しては Stack Overflow の [Static return type of Scala macros](http://stackoverflow.com/questions/13669974/static-return-type-of-scala-macros) を参照してほしい。型マクロは最初の型検査が行われないため、何も変わらない (そもそも型マクロに戻り値の型は指定できないからだ)。 + +最後に、型指定のないマクロのパッチは `c.Expr[T]` の代わりにマクロ実装のシグネチャのどこでも `c.Tree` を使うことを可能とする。 +マクロ定義の型指定なし/型付きと、構文木/式によるマクロ実装の 4通りの組み合わせ全てがパラメータと戻り値の型の両方においてサポートされている。 +さらに詳しいことはユニットテストを参照してほしい: [test/files/run/macro-untyped-conformance](https://github.com/scalamacros/kepler/blob/b55bda4860a205c88e9ae27015cf2d6563cc241d/test/files/run/macro-untyped-conformance/Impls_Macros_1.scala)。 diff --git a/overviews/core/_posts/2012-12-20-macros.md b/overviews/core/_posts/2012-12-20-macros.md index d38acd1587..a291455e94 100644 --- a/overviews/core/_posts/2012-12-20-macros.md +++ b/overviews/core/_posts/2012-12-20-macros.md @@ -4,6 +4,7 @@ title: Macros disqus: true partof: macros overview: macros +languages: [ja] label-color: important label-text: Experimental --- diff --git a/overviews/macros/bundles.md b/overviews/macros/bundles.md index 88e6cbc868..10b0ac7cd7 100644 --- a/overviews/macros/bundles.md +++ b/overviews/macros/bundles.md @@ -7,6 +7,7 @@ disqus: true partof: macros num: 6 outof: 7 +languages: [ja] --- MACRO PARADISE diff --git a/overviews/macros/inference.md b/overviews/macros/inference.md index 5269e62274..086cfbe5a7 100644 --- a/overviews/macros/inference.md +++ b/overviews/macros/inference.md @@ -7,6 +7,7 @@ disqus: true partof: macros num: 7 outof: 7 +languages: [ja] --- MACRO PARADISE diff --git a/overviews/macros/overview.md b/overviews/macros/overview.md index c930107c50..a3bca7b78b 100644 --- a/overviews/macros/overview.md +++ b/overviews/macros/overview.md @@ -7,6 +7,7 @@ disqus: true partof: macros num: 1 outof: 7 +languages: [ja] --- EXPERIMENTAL diff --git a/overviews/macros/paradise.md b/overviews/macros/paradise.md index a9f69dc51b..fa3820ee63 100644 --- a/overviews/macros/paradise.md +++ b/overviews/macros/paradise.md @@ -7,6 +7,7 @@ disqus: true partof: macros num: 2 outof: 7 +languages: [ja] --- MACRO PARADISE diff --git a/overviews/macros/quasiquotes.md b/overviews/macros/quasiquotes.md index 29adebdefd..f74e71836b 100644 --- a/overviews/macros/quasiquotes.md +++ b/overviews/macros/quasiquotes.md @@ -7,6 +7,7 @@ disqus: true partof: macros num: 4 outof: 7 +languages: [ja] --- MACRO PARADISE diff --git a/overviews/macros/typemacros.md b/overviews/macros/typemacros.md index 75b40273a8..92d43a7eb7 100644 --- a/overviews/macros/typemacros.md +++ b/overviews/macros/typemacros.md @@ -7,6 +7,7 @@ disqus: true partof: macros num: 3 outof: 7 +languages: [ja] --- MACRO PARADISE diff --git a/overviews/macros/untypedmacros.md b/overviews/macros/untypedmacros.md index 20bbfb9050..9d2be6fa88 100644 --- a/overviews/macros/untypedmacros.md +++ b/overviews/macros/untypedmacros.md @@ -7,6 +7,7 @@ disqus: true partof: macros num: 5 outof: 7 +languages: [ja] --- MACRO PARADISE