Scala 3のマクロTips 100連発
この記事はScala Advent Calendar 2023の12日目だ!
Scala 3のマクロを書く上で役に立つ, メタれたTipsたちを紹介するぜ! 勢いに任せて書いていくからサンプルコードがちゃんと動かなかったらごめんな. 一応, Scala 3.3.1を想定しているぞ.
マクロ
1. メソッドをマクロとして定義する
マクロを定義するにはdef
の前にinline
をつけ, メソッド本体は ${ ... }
の中でマクロを実装するメソッドを呼ぶようにするぞ! 引数をマクロ実装に渡すときはクォート'
をつけることだけ覚えてくれ!
inline def someMethod[T](arg: T): T =
${ someMethodImpl('arg) }
2. マクロの本体を実装する
マクロの本体は普通のメソッドだ! ただし, 元のマクロの型引数には: Type
をつけ, 引数や返り値の型はExpr[_]
に入れ, using Quotes
したものになっている必要があるぞ! Type
やExpr
やQuotes
のためにimport scala.quoted.*
も必要だ!
import scala.quoted.* def someMethodImpl[T: Type](arg: Expr[T])(using Quotes): Expr[T] = ???
3. マクロ実装の記法の意味を知る
${ ... }
や'
の意味が知りたいだって? この記事を読んでくれ!
4. マクロで生成されるコードの内容を確認する
以下のinspect
のようなマクロを用意しておくと, こいつに渡した式がマクロでどう展開されるか出力してくれるぞ!
import scala.quoted.* inline def inspect[T](inline x: T): T = ${ inspectImpl('x) } def inspectImpl[T: Type](x: Expr[T])(using Quotes): Expr[T] = { println(x.show) x }
5. 引数の式を評価せずに使う
引数をinline
にした場合, マクロ実装に渡ってくるのは評価前の式になっているぞ! 逆に言うとinline
をつけないと評価した結果の値を束縛する変数が渡ってくるぞ!
import scala.quoted.* inline def repeat5[T](inline x: T): Seq[T] = ${ repeat5Impl('x) } def repeat5Impl[T: Type](x: Expr[T])(using Quotes): Expr[Seq[T]] = '{ Seq($x, $x, $x, $x, $x) }
inspect(repeat5(println("hoge"))) // (scala.Seq.apply[scala.Unit](scala.Predef.println("hoge"), scala.Predef.println("hoge"), scala.Predef.println("hoge"), scala.Predef.println("hoge"), scala.Predef.println("hoge")): scala.collection.immutable.Seq[scala.Unit]) // hoge // hoge // hoge // hoge // hoge // val res0: Seq[Unit] = List((), (), (), (), ())
6. 返り値の型をマクロの実行結果によって決める
inline
の代わりにtransparent inline
にするとマクロ実装の実行結果によって返り値の型を決めることができるぞ!
import scala.quoted.* transparent inline def zero[T]: Any = ${ zeroImpl[T] } def zeroImpl[T: Type](using Quotes): Expr[Any] = Type.of[T] match { case '[Int] => Expr(0) case '[String] => Expr("") }
scala> zero[Int] val res0: Int = 0 scala> zero[String] val res1: String = ""
7. マクロの返り値の型を制限する
transparent inline
なマクロに返り値の型が書いてあると, マクロ実装の実行結果の型はその型の部分型に制限されるぞ!
制限に沿わない型を書くと怒られるから気をつけろ!
import scala.quoted.* transparent inline def zero[T]: Option[Any] = ${ zeroImpl[T] } def zeroImpl[T: Type](using Quotes): Expr[Any] = Type.of[T] match { case '[Int] => Expr(0) // ^ // Found: (0 : Int) // Required: Option[Any] case '[String] => Expr("") // ^^ // Found: ("" : String) // Required: Option[Any] }
制限に沿う型を書いた場合は, マクロのシグネチャ(この例の場合はOption[Any]
)ではなくマクロ実装の実行結果の型(この例の場合はSome[Int]
やSome[String]
やNone.type
)で返るぞ!
import scala.quoted.* transparent inline def zero[T]: Option[Any] = ${ zeroImpl[T] } def zeroImpl[T: Type](using Quotes): Expr[Option[Any]] = Type.of[T] match { case '[Int] => '{ Some(${ Expr(0) }) } case '[String] => '{ Some(${ Expr("") }) } case _ => '{ None } }
scala> zero[Int] val res0: Some[Int] = Some(0) scala> zero[String] val res1: Some[String] = Some() scala> zero[Int => Int] val res2: None.type = None
8. マクロの返り値の型を書かない
transparent inline
の場合はマクロのシグネチャとは違う型で返ってきてわかりにくいから, あえて返り値型を書かないのもアリだと思うぞ!
transparent inline def zero[T] =
${ zeroImpl[T] }
9. マクロで計算された型をテストする
マクロ実装の実行結果によって型が変わるんなら, 意図した型になったかどうかテストしたいよナァ? もし間違った型になっていたらコンパイルエラーにしたいよナァ? そういうときはこうするといいぞ!
import scala.compiletime.summonInline extension [T1](anything: T1) { inline def isA[T2]: Unit = { val _ = summonInline[T1 <:< T2] } inline def isExactlyA[T2]: Unit = { val _ = summonInline[T1 =:= T2] } }
zero[Int].isA[Some[Int]]
zero[Int].isExactlyA[Option[Int]]
// Cannot prove that Some[Int] =:= Option[Int].
式
11. 定数式の値を得る
コンパイル時に値が分かっているなら, マクロ内でvalue
を呼ぶと値を取り出せるぞ!
import scala.quoted.* inline def square(x: Int): Option[Int] = ${ squareImpl('x) } def squareImpl(x: Expr[Int])(using Quotes): Expr[Option[Int]] = x.value match { case Some(v) => Expr(Some(v * v)) case None => Expr(None) }
inspect(square(5)) // (scala.Some.apply[scala.Int](25): scala.Option[scala.Int]) // val res0: Option[Int] = Some(25) val i = 5 square(i) // val res1: Option[Int] = None
12. 定数式でなければコンパイルエラーにする
定数しか受け付けたくない場合はvalueOrAbort
するといいぞ!
import scala.quoted.* inline def square(x: Int): Int = ${ squareImpl('x) } def squareImpl(x: Expr[Int])(using Quotes): Expr[Int] = { val v = x.valueOrAbort Expr(v * v) }
square(5) // val res0: Int = 25 val i = 5 square(i) // ^ // Expected a known value.
13. 定数値の式を作る
既に例に登場しているから察しているかもしれないが, マクロの中でExpr(値)
とすると, 定数値を表すコードになるぞ!
Expr(v * v)
14. コードを生成する
既に例に登場しているから察しているかもしれないが, マクロの中でクォートを使って'{ 任意のScalaのコード }
とすると, 任意のコードを生成できるぞ!
'{ None }
16. 式をパターンマッチする
Expr
型の値はクォート'{ ... }
でパターンマッチできるぞ! スプライス${ ... }
した部分は変数に束縛できるぞ! 以下のrewriteFlatMap
はList
のmap(...).flatten
をflatMap(...)
に書き換えるぞ!
import scala.quoted.* inline def rewriteFlatMap[T](inline e: List[T]): List[T] = ${ rewriteFlatMapImpl('e) } def rewriteFlatMapImpl[T: Type]( e: Expr[List[T]], )(using Quotes): Expr[List[T]] = e match { case '{ type t type u <: IterableOnce[T] (${ ls }: List[`t`]) .map(${ f }: `t` => `u`) .flatten } => '{ ${ ls }.flatMap(${ f }) } case _ => e }
inspect(rewriteFlatMap(List(1, 2, 3).map(x => List(x, x*x)).flatten)) // (scala.List.apply[scala.Int](1, 2, 3).flatMap[scala.Int](((x: scala.Int) => scala.List.apply[scala.Int](x, x.*(x)))): scala.collection.immutable.List[scala.Int]) // val res0: List[Int] = List(1, 1, 2, 4, 3, 9)
17. 式をタプルにパターンマッチする
タプルを(_, _)
で書いても_ -> _
で書いてもどちらでもいいようにしたいときってあるよナァ? こうすればいいぞ!
def exprTupleOf[A: Type, B: Type]( e: Expr[(A, B)], )(using Quotes): (Expr[A], Expr[B]) = e match { case '{ (${ a }: A, ${ b }: B) } => (a, b) case '{ ArrowAssoc(${ a }: A).->(${ b }: B) } => (a, b) }
18. 式のパターンマッチで不要な変数を束縛しない
束縛しなくていいときは${ _ }
にするといいぞ!
def exprSnd[A: Type, B: Type]( e: Expr[(A, B)], )(using Quotes): Expr[B] = e match { case '{ (${ _ }: A, ${ b }: B) } => b case '{ ArrowAssoc(${ _ }: A).->(${ b }: B) } => b }
19. 式のコンパイル時の型を得る
既に例に近いものが登場しているから察しているかもしれないが, '{ e: tpe }
みたいに型名(小文字)つきで式にパターンマッチすると, その式のコンパイル時の型を取り出せるぞ!
val e: Expr[A] = ??? e match { case '{ ${ _ }: tpe } => ??? }
型はクォートの内と外で書き方が変わらない($
で括ったりしない)点に注意しような! ちなみに, パターン中の小文字の型が新しい変数束縛の宣言でないときは`...`
で括るルールだ. これはrewriteFlatMap
の例でも見たな!
20. 式を印字する
inspect
の定義で既にやっているが, Expr[?]
に対してshow
を呼ぶと式を印字できるぞ!
def inspectImpl[T: Type](x: Expr[T])(using Quotes): Expr[T] = {
println(x.show)
x
}
型
21. 型の情報を得る
型に対してなにかするにはType[T]
型の値を得るんだ. Type.of
で得られるぞ!
val tpe: Type[T] = Type.of[T]
22. Type
から型を取り出す
Type[T]
は'[ ... ]
でパターンマッチできるぞ! 実際の型が不明なType[?]
からも型を取り出すことができるぞ!
val tpe: Type[?] = ??? tpe match { case '[ t ] => ??? }
23. 型をパターンマッチする
型の構造にもパターンマッチできるぞ! タプル型かどうか調べるときは*:
でパターンマッチするといいぞ!
def isTuple[T: Type](using Quotes): Expr[Boolean] = { import quotes.reflect.* val b = Type.of[T] match { case '[_ *: _] => true case _ => false } Expr(b) }
24. 型を印字する
Type.show
で型を印字できるぞ!
println(Type.show[String]) // scala.Predef.String
式の抽象構文木
26. リテラルの抽象構文木を作る
Term
の部分型として抽象構文木の構成要素が定義されていて, Literal
がリテラル値を表しているぞ!
import quotes.reflect.* val term: Term = Literal(StringConstant("foo"))
27. 式の構造を印字する
Term
型をtoString
するとそのまま構造が出力されるぞ!
println('{ 1 + 1 }.asTerm) // Inlined(Ident(MacroTips$),List(),Apply(Select(Literal(Constant(1)),+),List(Literal(Constant(1)))))
29. 抽象構文木から式を作る
Term
のasExprOf
を呼ぶと, 指定した型のExpr[?]
に変換できるぞ!
import quotes.reflect.* val term: Term = Literal(StringConstant("foo")) val expr: Expr[String] = term.asExprOf[String]
asExprOf
になんの型を渡してもマクロのコンパイル時には怒られず, 間違った型を渡すとマクロの実行時(普通のコンパイル時)にエラーになるから気をつけような!
import quotes.reflect.* val term: Term = Literal(StringConstant("foo")) val expr: Expr[String] = term.asExprOf[Int] // Exception occurred while executing macro expansion. // java.lang.Exception: Expr cast exception: "foo" // of type: "foo" // did not conform to type: scala.Int
型の抽象構文木
31. 型を短く印字する
TypeRepr
のshow
を使うとパッケージ名を省略して型を印字できるぞ!
import quotes.reflect.* println(TypeRepr.of[String].show(using Printer.TypeReprShortCode)) // String
32. Type
から抽象構文木を得る
Type[?]
からTypeRepr
を得るには, いったんパターンマッチで型を取り出してからTypeRepr.of
するといいぞ!
val tpe: Type[?] = ??? tpe match { case '[tpe] => TypeRepr.of[tpe] }
33. リテラル型の抽象構文木を作る
TypeRepr
の部分型として型の抽象構文木の構成要素が定義されていて, ConstantType
がリテラル型を表しているぞ!
import quotes.reflect.* ConstantType(StringConstant("foo"))
34. 型の構造を印字する
TypeRepr
のshow
を使うと型の構造を印字できるぞ!
import quotes.reflect.* println(TypeRepr.of[String].show(using Printer.TypeReprStructure)) // TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String") println(TypeRepr.of[(Int, String)].show(using Printer.TypeReprStructure)) // AppliedType(TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Tuple2"), List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int"), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String"))) println(TypeRepr.of[{ val name: String; val age: Int }].show(using Printer.TypeReprStructure)) // Refinement(Refinement(TypeRef(ThisType(TypeRef(NoPrefix(), "lang")), "Object"), "name", TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String")), "age", TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "<root>")), "scala"), "Int"))
実はTypeRepr
をtoString
してもだいたい同じだ! こっちだけ覚えておけばいいな!
println(TypeRepr.of[String]) // TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class scala)),object Predef),type String)
36. TypeRepr
からType[?]
を得る
TypeRepr
のasType
を呼ぶとType[?]
に変換できるぞ!
import quotes.reflect.* ConstantType(StringConstant("foo")).asType
37. TypeRepr
から型を取り出す
asType
したものにパターンマッチすれば型を取り出せるぞ!
import quotes.reflect.* ConstantType(StringConstant("foo")).asType match { case '[tpe] => ??? }
38. 型が等しいかどうか調べる
TypeRepr
の=:=
を使うと2つの型が等しいかどうか調べられるぞ!
import quotes.reflect.* TypeRepr.of[String] =:= TypeRepr.of[scala.Predef.String] // true
39. 部分型かどうか調べる
TypeRepr
の<:<
を使うと部分型かどうか調べられるぞ!
import quotes.reflect.* TypeRepr.of[(Int, String)] <:< TypeRepr.of[Tuple] // true
40. 型エイリアスを解決する
型エイリアスを解決したいときもあるよナァ? そんなときはTypeRepr
のdealias
を呼ぶといいぞ!
type MyInt = Int
import quotes.reflect.* val tpr = TypeRepr.of[MyInt].dealias println(tpr.show(using Printer.TypeReprShortCode)) // Int
41. 型レベルの計算結果を得る
型レベルのmatch
とかで計算したいことってあるよナァ? その計算結果も知りたいよナァ? そんなときはTypeRepr
のsimplified
を呼ぶと計算してくれるぞ!
def isTuple1[T: Type](using Quotes): Expr[String] = { import quotes.reflect.* val tpr = TypeRepr.of[ T match { case _ *: _ => true case _ => false }, ] Expr(tpr.show) }
inline isTuple[T]: String = ${ isTuple1[T] } isTuple[(Int, Int)] // val res0: String = scala.Tuple2[scala.Int, scala.Int] match { // [_$1 >: scala.Nothing <: scala.Any, _$2 >: scala.Nothing <: scala.Tuple] => case scala.*:[_$1, _$2] => true // case scala.Any => false // }
def isTuple2[T: Type](using Quotes): Expr[String] = { import quotes.reflect.* val tpr = TypeRepr.of[ T match { case _ *: _ => true case _ => false }, ] Expr(tpr.simplified.show) }
inline isTuple[T]: String = ${ isTuple2[T] } isTuple[(Int, Int)] // val res0: String = true
42. 型を汎化する
Scalaではなにかとシングルトン型になっているよナァ? TypeRepr
のwiden
でシングルトンではない型にできるぞ!
val tpr = TypeRepr.of[3] tpr.show(using Printer.TypeReprShortCode) // 3
val tpr = TypeRepr.of[3].widen tpr.show(using Printer.TypeReprShortCode) // Int
43. 型の構造を再帰的に走査する
型の構造を辿るには再帰することになるぞ! @tailrec
をうまく使っていこうな!
import scala.quoted.* import scala.annotation.tailrec def seqFromTuple[T: Type](using Quotes): Seq[Type[?]] = { @tailrec def rec[T: Type](acc: Seq[Type[?]] = Seq.empty): Seq[Type[?]] = Type.of[T] match { case '[ head *: tail ] => rec[tail](acc :+ Type.of[head]) case _ => acc } rec[T]() }
44. 構造的な型を再帰的に走査する
Scala 3のstructural typesはRefinement
というTypeRepr
で, 右のフィールドほど構文木の根に近い方に配置されていて, 再帰で辿ると右から見ることになるから注意が必要だ.
import scala.quoted.* import scala.annotation.tailrec def seqOfRefinement[T: Type](using Quotes): Seq[Type[?]] = { import quotes.reflect.* @tailrec def rec(tpr: TypeRepr, acc: Seq[Type[?]]): Seq[Type[?]] = tpr match { case Refinement(base, fieldName, fieldType) => val nameType = ConstantType(StringConstant(fieldName)).asType (nameType, fieldType.asType) match { case ('[name], '[tpe]) => rec(base, Type.of[(name, tpe)] +: acc) } case _ => acc } rec(TypeRepr.of[T], Seq.empty) }
for { tpe <- seqOfRefinement[{ val name: String; val age: Int }] } println(tpe match { case '[t] => Type.show[t] }) // scala.Tuple2["name", java.lang.String] // scala.Tuple2["age", scala.Int]
シンボル
45. 定義を参照する
TypeRepr
のtypeSymbol
で型のSymbol
を取得すると, 定義などを参照できるぞ!
class A { def foo: Int = ??? def bar: String = ??? }
import quotes.reflect.* println(TypeRepr.of[A].typeSymbol.declarations.map(_.name)) // List(<init>, foo, bar)
46. コンパニオンオブジェクトを参照する
型のSymbol
からはcompanionModule
でコンパニオンオブジェクトが, companionClass
でその型のSymbol
が得られるぞ!
import quotes.reflect.* val sym = TypeRepr.of[A].typeSymbol val companion = sym.companionModule val companionClass = sym.companionClass
47. 特定の名前のメソッドを呼ぶ式を生成する
Select.unique
でメソッドを選択することで, メソッド呼出しの式を生成できるぞ!
class A { def foo: Int = ??? def bar: String = ??? } object A { def apply(): A = new A }
inline def genericApply[T]: T = ${ genericApplyImpl[T] } def genericApplyImpl[T: Type](using Quotes): Expr[T] = { import quotes.reflect.* val sym = TypeRepr.of[T].typeSymbol Select .unique(Ref(sym.companionModule), "apply") .appliedToArgs(List()) .asExprOf[T] }
genericApply[A]
// val res0: A = A@6681707
48. オーバーロードされたメソッドを呼ぶ式を生成する
49. 引数のないメソッドを呼ぶ式を生成する
appliedToNone
を使え! 「引数のないメソッド」というのはtoString
みたいに()
をつけずに呼ぶメソッドのことだ.
50. 抽象構文木の親を辿る
Symbol
のspliceOwner
やowner
で抽象構文木の親要素を辿れるぞ! 以下ではval foo =
の部分(ValDef
)まで親を辿ることで, val
で宣言した名前をマクロ内で使っているぞ!
case class Named(name: String) inline def named: Named = ${ namedImpl } def namedImpl(using Quotes): Expr[Named] = { import quotes.reflect.* def enclosingTerm(sym: Symbol): Symbol = sym match { case _ if sym.flags is Flags.Macro => enclosingTerm(sym.owner) case _ if !sym.isTerm => enclosingTerm(sym.owner) case _ => sym } val name = enclosingTerm(Symbol.spliceOwner).name '{ Named(${ Expr(name) }) } }
val foo = named // val foo: Named = Named(foo)
51. リフレクションAPIの使い方を調べる
Term
, TypeRepr
, Symbol
など, import quotes.reflect.*
して使うもののことをリフレクションAPIと言うぞ!
リフレクションAPIで可能な操作を知るには, APIリファレンスを見るよりもscala.quoted.Quotes
のソースコードを見てくれ! Term
ならTermModule
が実際の定義で, TermMethods
に拡張メソッドが定義されているぞ! 知りたいものの*Module
と*Methods
を見ればいいってことだ!
エラー処理
52. コンパイルエラーにする
マクロ実行中のエラーはコンパイルエラーになってほしいよナァ? report.errorAndAbort
でコンパイルエラーにできるぞ! report
を使うにはimport quotes.reflect.*
が必要だ!
import scala.quoted.* transparent inline def sum[T1, T2](x: T1, y: T2) = ${ sumImpl('x, 'y) } def sumImpl[T1: Type, T2: Type](x: Expr[T1], y: Expr[T2])(using Quotes, ): Expr[Any] = { import quotes.reflect.* (x, y) match { case ('{ ${ x }: Int }, '{ ${ y }: Int }) => '{ ${ x } + ${ y } } case ('{ ${ x }: String }, _) => '{ Seq(${ x }, ${ y }.toString).mkString } case _ => report.errorAndAbort("unsupported") } }
sum(1, "foo") // unsupported
53. コンパイルエラーの発生箇所を表示する
Scalaのコンパイラみたいに, エラーの原因箇所をお洒落に表示したいよナァ? errorAndAbort
の第2引数にExpr
を渡せばそこを指し示してくれるぞ!
import scala.quoted.* transparent inline def sum[T1, T2](x: T1, y: T2) = ${ sumImpl('x, 'y) } def sumImpl[T1: Type, T2: Type](x: Expr[T1], y: Expr[T2])(using Quotes, ): Expr[Any] = { import quotes.reflect.* x match { case '{ ${ x }: Int } => y match { case '{ ${ y }: Int } => '{ ${ x } + ${ y } } case _ => report.errorAndAbort( s"""Found: ${Type.show[T2]} |Required: Int |""".stripMargin, y, ) } case '{ ${ x }: String } => '{ Seq(${ x }, ${ y }.toString).mkString } case _ => report.errorAndAbort("unsupported") } }
sum(1, "foo") // ^^^^^ // Found: java.lang.String // Required: Int
54. コンパイルエラーをテストする
マクロのエラーの出力が正しくできているかテストしたいよナァ? scala.compiletime.testing.typeCheckErrors
を使うとできるぞ!
import scala.compiletime.testing.{Error, ErrorKind} def checkErrors(errs: List[Error]): Boolean = errs.nonEmpty && errs.head.kind == ErrorKind.Typer && errs.head.message.startsWith("Found:")
import scala.compiletime.testing.typeCheckErrors checkErrors(typeCheckErrors("""sum(1, "foo")""")) // val res0: Boolean = true
55. マクロ内のエラーから回復する
report.errorAndAbort
はコンパイラが直接ハンドルするためか, 普通にtry
-catch
しようとしてもうまくいかないぞ! 余計な例外を飲み込んでしまわないためにも, 回復が必要なエラーは自分で例外を定義してtry
-catch
するか, Option
やEither
などでやろう!
応用
56. Dynamic
やSelectable
のselectDynamic
をマクロ化する
Dynamic
やSelectable
は, obj.foo
のようなフィールドアクセスをobj.selectDynamic("foo")
に書き換えてくれる. 実は, このselectDynamic
をマクロにしておくと, 任意のコードに書き換えることができるぞ!
import scala.language.dynamics class A extends Dynamic { inline def selectDynamic(name: String): Int = ${ A.selectImpl('name) } } object A { import scala.quoted.* def selectImpl(name: Expr[String])(using Quotes): Expr[Int] = { import quotes.reflect.* val constName = name.valueOrAbort constName match { case "zero" => Expr(0) case "one" => Expr(1) case _ => report.errorAndAbort("not implemented") } } }
val a = new A inspect(a.zero) // (0: scala.Int) // val res0: Int = 0
57. Dynamic
やSelectable
のapplyDynamicNamed
をマクロ化する
同様に, applyDynamic
やapplyDynamicNamed
もマクロにすると, 任意のメソッド呼出しをマクロで書き換えることができるぞ! とくにapplyDynamicNamed
は, obj.m(foo = 1)
のfoo
の部分の名前も文字列として得られるぞ!
import scala.language.dynamics class A extends Dynamic { transparent inline def applyDynamicNamed(method: String)( inline args: (String, Any)*, ) = ${ A.applyImpl('this, 'method, 'args) } } object A { import scala.quoted.* def applyImpl( a: Expr[A], method: Expr[String], args: Expr[Seq[(String, Any)]], )(using Quotes): Expr[Any] = ??? }
58. 可変長引数を式のリストにする
Expr[Seq[?]]
型で渡されてくる可変長引数は, Varargs
とパターンマッチすることで式のリストにできるぞ!
import scala.language.dynamics class A extends Dynamic { transparent inline def applyDynamicNamed(method: String)( inline args: (String, Any)*, ) = ${ A.applyImpl('this, 'method, 'args) } } object A { import scala.quoted.* def applyImpl( a: Expr[A], method: Expr[String], args: Expr[Seq[(String, Any)]], )(using Quotes): Expr[Any] = { import quotes.reflect.* args match { case Varargs(list) => // list: Seq[Expr[(String, Any)]] ??? case _ => report.errorAndAbort("Expected explicit varargs sequence", args) } } }
59. 可変長引数の各引数の型を得る
可変長引数の各引数の型はまとめてAny
になって渡ってくるが, パターンマッチすれば各引数の型もわかるぞ! 以下の例ではtpe
に各引数の型を取り出しているぞ!
import scala.language.dynamics class A extends Dynamic { transparent inline def applyDynamicNamed(method: String)( inline args: (String, Any)*, ) = ${ A.applyImpl('this, 'method, 'args) } } object A { import scala.quoted.* def applyImpl( a: Expr[A], method: Expr[String], args: Expr[Seq[(String, Any)]], )(using Quotes): Expr[Any] = { import quotes.reflect.* val triples = // triples: Seq[(String, Expr[Any], Type[?])] args match { case Varargs(list) => list.map { case '{ ($labelExpr: String, $valueExpr: tpe) } => (labelExpr.valueOrAbort, valueExpr, Type.of[tpe]) } case _ => report.errorAndAbort("Expected explicit varargs sequence", args) } ??? } }
60. 式の列を可変長引数としてスプライスする
Seq[Expr[?]]
はExpr.ofSeq
でExpr[Seq[?]]
に変換できるぞ! 以下はSet(1, 2, 3)
というコードを生成するぞ!
val exprs = Seq(Expr(1), Expr(2), Expr(3)) '{ Set(${ Expr.ofSeq(exprs) }: _*) }
61. 文字列補間をマクロ化する
できる! とくに変なところはなく, ただマクロにするだけだ!
trait A object A { extension (sc: StringContext) { inline def a(inline args: Any*): A = ${ interpolationImpl('sc, 'args) } } import scala.quoted.* def interpolationImpl(sc: Expr[StringContext], args: Expr[Seq[Any]])(using Quotes, ): Expr[A] = ??? }
62. unapply
をtransparent inline
にする
なんとできない!! コンパイラのバグだ!
修正されていそうだから, 近々できるようになるだろう.
63. 型変数かどうか調べる
TypeRepr
のtypeSymbol
からisTypeParam
を呼ぶと型変数かどうか調べられるぞ!
import scala.quoted.* inline def isTypeVar[T]: Boolean = ${ isTypeVarImpl[T] } def isTypeVarImpl[T: Type](using Quotes): Expr[Boolean] = { import quotes.reflect.* val b = TypeRepr.of[T].typeSymbol.isTypeParam Expr(b) }
マクロの中でこれがtrue
になる場合, マクロの呼出しコンテキストでは型引数に具体的な型が埋められていないということだな!
def nonGenericCall: Unit = println(isTypeVar[String]) nonGenericCall // false
def genericCall[T]: Unit = println(isTypeVar[T]) genericCall[String] // true
64. マクロに具体的な型引数が渡るようにする
だから, マクロに具体的な型を渡さなければならない場合は, マクロを呼び出しているメソッドは必然的にinline
になるぞ!
inline def inlineCall[T]: Unit = println(isTypeVar[T]) inlineCall[String] // false
65. 型に対する複雑な条件を調べる
型が条件を満たしているかをType[?]
やTypeRepr
のインタフェースで調べるのは大変なときがあるよナァ? そういう場合は(マクロではない普通の)given
で条件を表現しておいて, マクロの中ではExpr.summon
するだけにしておくと簡単だ!
以下ではopaque type
で表現されたTag[?]
かどうかを調べるIsTag[_]
を定義しているぞ!
opaque type Tag[T] = Any object Tag { final class IsTag[T] private () object IsTag { private[Tag] val instance = new IsTag[Nothing] } given [T]: IsTag[Tag[T]] = IsTag.instance.asInstanceOf[IsTag[Tag[T]]] }
Expr.summon[Tag.IsTag[T]]
できればT
はTag[?]
だということだ!
given
とマクロ
66. マクロ内でsummon
する
Expr.summon
を使うと, マクロを呼び出した箇所のコンテキストでgiven
インスタンスを探索できるぞ!
Expr.summon[Ordering[T]] match { case Some(ord) => ??? case None => ??? }
67. マクロ内でsummon
できなかったらコンパイルエラーにする
Expr.summon
してgetOrElse
でコンパイルエラーにするメソッドを用意しておくと便利だぞ!
def evidenceOf[T: Type](using Quotes): Expr[T] = Expr.summon[T].getOrElse { errorAndAbort( s"No given instance of ${Type.show[T]}", ) }
68. given
をマクロ化する
実はgiven
自体をマクロにすることができるぞ! given
インスタンスの探索中にマクロが実行されるぞ!
trait MyTypeClass[A] inline given myTypeClss[A]: MyTypeClass[A] = ${ derivedMyTypeClassImpl } import scala.quoted.* def derivedMyTypeClassImpl[A: Type](using Quotes): Expr[MyTypeClass[A]] = ???
69. given
マクロで型情報だけ計算する
given
をtransparent inline
にすると, 型の計算だけするマクロを作れるぞ!
class MyTyper[A] { type Out } transparent inline given myTyper[A]: MyTyper[A] = ${ derivedMyTyperImpl } import scala.quoted.* def derivedMyTyperImpl[A: Type](using Quotes): Expr[MyTyper[A]] = { val tpe = Type.of[A] match { case '[Int] => Type.of[Seq[Int]] case '[String] => Type.of[Option[String]] case _ => Type.of[Nothing] } tpe match { case '[tpe] => '{ new MyTyper[A] { type Out = tpe } } } }
val tp = summon[MyTyper[Int]] // val tp: MacroTips.MyTyper[Int]{type Out = Seq[Int]} = anon$1@3577de62 val x: tp.Out = Seq(3) // val x: tp.Out = List(3)
70. given
マクロで計算された結果の型を取り出す
以下のようにするとMyTyper
のtype Out
をtpe
という名前で取り出せるぞ!
Expr.summon[MyTyper[A]] match { case Some('{ ${ _ }: MyTyper[A] { type Out = tpe } }) => ??? case _ => ??? }
71. 型境界つきの型にパターンマッチする
MyTyper
の結果はIterableOnce[A]
だと定義されていて, IterableOnce[?]
を要求するOnIterableOnce
があったとしよう.
class MyTyper[A] { type Out <: IterableOnce[A] } trait OnIterableOnce[It <: IterableOnce[?]]
この場合, 次のコードは動かない!
Expr.summon[MyTyper[A]] match { case Some('{ ${ _ }: MyTyper[A] { type Out = tpe } }) => val tp = Type.of[OnIterableOnce[tpe]] // ^ // Type argument tpe does not conform to upper bound IterableOnce[?] ??? case _ => ??? }
これは, type Out = tpe
としたときに, もともとのOut
の上限をなぜか考慮してくれないからだッ!
こういう場合は次のように書くとうまくいくぞ!
Expr.summon[MyTyper[A]] match { case Some('{ type tpe <: IterableOnce[A] ${ _ }: MyTyper[A] { type Out = `tpe` } }) => val _ = Type.of[OnIterableOnce[tpe]] ??? case _ => ??? }
先にtype tpe <: IterableOnce[A]
と上限つきで束縛変数を宣言しておいて, type Out =
の方では`tpe`
とバッククォートすることで宣言済みの型を参照して制約を課しているんだな!
72. 型情報の計算しかしないgiven
マクロの生成コストを減らす
マクロの中でnew
するときは気をつけろ!
'{ new MyTyper[A] { type Out = tpe } }
これだと毎回新たなインスタンスが作られてしまうぞ! 型情報が違うだけならインスタンスは1つで十分だ!
final class MyTyper[A] private () { type Out } object MyTyper { val instance = new MyTyper[Nothing] }
def derivedMyTyperImpl[A: Type](using Quotes): Expr[MyTyper[A]] = { val tpe = ??? tpe match { case '[tpe] => '{ MyTyper .instance .asInstanceOf[ MyTyper[A] { type Out = tpe }, ] } } }
73. マクロをなるべくgiven
マクロに押し込める
given
マクロをうまく使うと, すべてのマクロをgiven
インスタンスの計算の部分に押し込んでしまえることが多いぞ!
コンパクトな例や普遍的なやり方を示すのは難しいから実際にやられている例を貼っておこう.
inline given decoder[R <: %](using r: RecordLike[R], dec: Decoder[ArrayRecord[r.TupledFieldTypes]], c: typing.Record.Concat[%, ArrayRecord[r.TupledFieldTypes]], ev: c.Out =:= R, ): Decoder[R] = new Decoder[R] { final def apply(c: HCursor): Decoder.Result[R] = dec(c).map(ar => ev(ar.toRecord)) }com.github.tarao.record4s.circe.Codec
コードの意味はわからなくてもいい! c.Out
はgiven
マクロで計算される型で, それをうまく他の制約と組み合わせることでapply
メソッド本体はマクロなしで書けてしまっている点だけ見てくれ!
74. given
マクロで型チェックしてメソッドの実装では型安全なやり方を諦める
given
マクロで型を計算するのは, 独自の型検査の仕組みを実装していると考えることができるぞ! 不正な使い方はgiven
マクロのインスタンス化の失敗として検出できるから, 検査に通ってしまえばasInstanceOf
をしまくっても問題ないはずだ!
これもコンパクトな例を考えるのが面倒だから作るのは難しいから, 実際に使われている例を貼っておくぞ!
def lookup[R <: %, L <: String & Singleton, Out](record: R, label: L)(using Lookup.Aux[R, L, Out], ): Out = record.__lookup(label).asInstanceOf[Out]com.github.tarao.record4s.Record.lookup
Lookup.Aux
が型を計算するgiven
マクロだ. このgiven
インスタンスが得られた時点で, record.__lookup(label)
の結果(静的にはAny
型)はOut
型になっていることが保証されているから, 一見すると安全ではないasInstanceOf
を呼んでも問題ないというわけだ!
75. given
マクロのエラーをわかりやすいコンパイルエラーにする
given
マクロの中でコンパイルエラーになると, エラーメッセージは表示されずに単にNo given instance of ~
と言われてしまう. これはわかりにくい! 利用者には使い方がどう悪かったのか説明してあげた方がいいだろう.
あまりいいやり方ではないかもしれないが, given
マクロの実行は必ず成功するようにして, エラー時にはtype Out = Nothing
にすることで, わかりやすいエラーメッセージを表示する手があるぞ!
準備として次のようなものを用意しておく.
trait MaybeError { type Out type Msg <: String } inline def showTypingError(using err: typing.MaybeError): Unit = { import scala.compiletime.{constValue, erasedValue, error} inline erasedValue[err.Out] match { case _: Nothing => error(constValue[err.Msg]) case _ => // no error } } inline def withPotentialTypingError[T]( inline block: => T, )(using err: typing.MaybeError): T = { showTypingError block }
型を計算するための型はMaybeError
を継承しておく.
final class MyTyper[A] private () extends MaybeError object MyTyper { val instance = new MyTyper[Nothing] }
エラーのときはtype Out = Nothing
にしてtype Msg
にエラーメッセージを入れておく.
def derivedMyTyperImpl[A: Type](using Quotes): Expr[MyTyper[A]] = { val tpe = ??? if (/* エラー時 */) { '{ MyTyper .instance .asInstanceOf[ MyTyper[A] { type Out = Nothing type Msg = "わかりやすいエラーメッセージ" }, ] } } else { tpe match { case '[tpe] => '{ MyTyper .instance .asInstanceOf[ MyTyper[A] { type Out = tpe type Msg = Nothing }, ] } } } }
これらを使うメソッドは以下を満たすようにする.
inline
にする- 本体を
withPotentialTypingError { ... }
で囲む
inline def withMyTyper[A](using t: MyTyper[A]): t.Out =
withPotentialTypingError {
???
}
こうすることでgiven
マクロのエラーメッセージを, using
しているメソッドまで伝播させることができるぞ!
マクロアノテーション (実験的機能)
76. マクロでメソッド定義を書き換える
MacroAnnotation
を使うと, メソッド定義を書き換えるアノテーションを定義できるぞ! この機能は実験的だから@experimental
アノテーションをつけような!
以下の例は任意の1引数関数をメモ化する@memoize
アノテーションだ!
import scala.annotation.{experimental, MacroAnnotation} import scala.quoted.* @experimental class memoize extends MacroAnnotation { def transform(using Quotes, )(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] = { import quotes.reflect.* tree match { case DefDef( name, TermParamClause(param :: Nil) :: Nil, tpt, Some(rhsTree), ) => (Ref(param.symbol).asExpr, rhsTree.asExpr) match { case ('{ $paramRefExpr: t }, '{ $rhsExpr: u }) => val cacheTpe = TypeRepr.of[collection.mutable.Map[t, u]] val cacheSymbol = Symbol.newVal( tree.symbol.owner, name + "Cache", cacheTpe, Flags.Private, Symbol.noSymbol, ) val cacheRhs = '{ collection.mutable.Map.empty[t, u] }.asTerm val cacheVal = ValDef(cacheSymbol, Some(cacheRhs)) val cacheRefExpr = Ref(cacheSymbol).asExprOf[collection.mutable.Map[t, u]] val newRhs = '{ $cacheRefExpr.getOrElseUpdate($paramRefExpr, $rhsExpr) }.asTerm val newTree = DefDef.copy(tree)( name, TermParamClause(param :: Nil) :: Nil, tpt, Some(newRhs), ) List(cacheVal, newTree) } case _ => report.error( "Annotation only supported on `def` with a single argument", ) List(tree) } } }import scala.annotation.experimental @experimental @memoize def fib(n: Int): Int = if (n <= 1) n else fib(n - 1) + fib(n - 2)Macro annotation (part 1) by nicolasstucki · Pull Request #16392 · lampepfl/dotty
import scala.annotation.experimental @experimental val r = fib(8) // val r: Int = 21
77. マクロでクラス定義を書き換える
MacroAnnotation
はclass
やobject
にも使えるぞ!
長いのでコードは貼らないが, たとえばequals
を自動生成するアノテーションなんかも定義できるぞ! こういう使い方のイメージだな.
@equals class Foo(val a: String, val b: Int) //> override def equals(that: Any): Boolean = //> that match //> case that: Foo => this.a.==(that.a).&&(this.b.==(that.b)) //> case _ => false //> private lazy val hash$macro$1: Int = //> var acc = -1566937476 // scala.runtime.Statics.mix(-889275714, "Foo".hashCode) //> acc = scala.runtime.Statics.mix(acc, scala.runtime.Statics.anyHash(a)) //> acc = scala.runtime.Statics.mix(acc, b) //> scala.runtime.Statics.finalizeHash(acc, 2) //> override def hashCode(): Int = hash$macro$1lampepfl/dotty:tests/run-macros/annot-mod-class-equals/Test_2.scala
78. 変数を定義するコードを生成する
マクロアノテーションのコード例を見ると分かるが, ValDef
で変数定義を作ることができるぞ!
val cacheTpe = TypeRepr.of[collection.mutable.Map[t, u]] val cacheSymbol = Symbol.newVal( tree.symbol.owner, name + "Cache", cacheTpe, Flags.Private, Symbol.noSymbol, ) val cacheRhs = '{ collection.mutable.Map.empty[t, u] }.asTerm val cacheVal = ValDef(cacheSymbol, Some(cacheRhs))
リフレクションAPIばかりになってしまう! Symbol
を作ったりするのが面倒だな!
79. メソッドを定義するコードを生成する
メソッド定義はDefDef
で作ることができるぞ!
val newRhs = '{ $cacheRefExpr.getOrElseUpdate($paramRefExpr, $rhsExpr) }.asTerm val newTree = DefDef.copy(tree)( name, TermParamClause(param :: Nil) :: Nil, tpt, Some(newRhs), )
既存メソッドを置き換える場合はSymbol
を新しく作る必要はなくて少し楽だな!
80. クラスを定義するコードを生成する
クラス定義はClassDef
で作ることができるぞ! やり方は@equals
の例や新規にクラスを追加する例を見てくれ!
コード整理
81. 生成コードはなるべくコンパクトにする
マクロで生成するコードはなるべくコンパクトにしような! でないと実行ファイルが巨大になってしまうぞ!
82. 外から自由にはアクセスできないメソッドをマクロの生成コードで使う
マクロによる生成コードが巨大になってきたら, マクロではないメソッドに括り出して, そのメソッドをマクロ内から呼び出すようにするといいぞ! ただし, マクロから呼ばれるメソッドはprivate
にはできないから注意しような. パッケージprivate
なら大丈夫だぞ.
83. using Quotes
をいちいち書かなくてよくする
using Quotes
をいちいち書くのは面倒だよナァ?
トップレベルのマクロ実装は仕方がないが, マクロ実装で使うサブルーチンは以下のようなInternalMacros
にまとめておくと, using Quotes
を書かなくてよくなってやりやすいかもしれないぞ!
class InternalMacros(using scala.quoted.Quotes) { import scala.quoted.* import quotes.reflect.* def subroutine(...) ... // using Quotes 不要 } object InternalMacros { import scala.quoted.* given (using Quotes): InternalMacros = new InternalMacros transparent inline def internal(using i: InternalMacros): i.type = i inline def withInternal[T](using Quotes, InternalMacros)( inline block: InternalMacros ?=> T, ): T = block(using summon[InternalMacros]) }
トップレベルのマクロ実装ではこうする想定だ!
import InternalMacros.{internal, withInternal} def macroImpl(...)(using Quotes): Expr[Any] = withInternal { import internal.* subroutine(...) }
トラブルシューティング
84. Nothing
がタプルにマッチしてしまう問題の対策
なぜだかNothing
(もしくは型変数??)がタプルにマッチしてしまうことがある!
もし遭遇したら頑張って避けるしかないだろう.
if TypeRepr.of[T] != TypeRepr.of[Nothing]
いいやり方があればむしろ教えてほしい!
85. マクロの中で型引数の情報が失われる場合の対策
なぜかgiven
マクロの型引数の情報が一部失われてしまうことがある!
全く意味のないcontext boundを1つ挟むと大丈夫だったりするようだ.
trait Context[T]
transparent inline given [T: Context]: MyTyper[T] = ???
原因や正しい回避策を知っている人がいたらむしろ教えてほしい!
86. マクロ内の変数でunused警告が出てしまう場合の対策
パターンの中でtype
を書いているときなど, 未使用のローカル変数と誤判定されることがある! @nowarn("msg=unused local")
してしまおう!
87. コンパイラのクラッシュをデバッグする
コンパイラがクラッシュして困ったら-Xcheck-macros
をつけてみるといいぞ!
詳しくはここを見てくれ!
参考資料
88. マクロについての公式ガイド
89. クォートについての公式ガイド
90. リフレクションについての公式ガイド
91. マクロについてのFAQ
92. マクロのベストプラクティス
93. マクロについての公式リファレンス
94. マクロの仕様
95. given
マクロについての公式リファレンス
96. 公式のマクロの例
97. StackOverflowで例を探す
98. record4sのマクロの例
99. shapeless-3のマクロの例
100. circeのマクロの例
おわりに
「100連発」って言って本当に100個あるとか頭おかしいんじゃねーの!