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したものになっている必要があるぞ! TypeExprQuotesのために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].
10. マクロで計算された型をScalaTestでテストする

ScalaTestでmatchersを使っているなら「shouldなんとか」な記法でテストしたいよナァ? こういうのを定義しておけばできるぞ!

zero[Int] shouldStaticallyBe a[Some[Int]]
zero[Int] shouldStaticallyBe an[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 }
15. 生成コードの中に計算された値を埋め込む

さらにクォート'{ ... }の中には任意の式をスプライス$で埋め込めるぞ!

'{ Some(${ Expr(0) }) }
16. 式をパターンマッチする

Expr型の値はクォート'{ ... }でパターンマッチできるぞ! スプライス${ ... }した部分は変数に束縛できるぞ! 以下のrewriteFlatMapListmap(...).flattenflatMap(...)に書き換えるぞ!

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

式の抽象構文木

25. 式の抽象構文木を得る

Expr[?]に対してasTermすると, 式の抽象構文木(Term型の値)が得られるぞ!

'{ 1 + 1 }.asTerm
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)))))
28. 式の抽象構文木の構成要素を知る

ここを見てくれ!

29. 抽象構文木から式を作る

TermasExprOfを呼ぶと, 指定した型の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

型の抽象構文木

30. 型の抽象構文木を得る

TypeRepr.of[T]で型の抽象構文木が得られるぞ!

31. 型を短く印字する

TypeReprshowを使うとパッケージ名を省略して型を印字できるぞ!

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. 型の構造を印字する

TypeReprshowを使うと型の構造を印字できるぞ!

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"))

実はTypeReprtoStringしてもだいたい同じだ! こっちだけ覚えておけばいいな!

println(TypeRepr.of[String])
// TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class scala)),object Predef),type String)
35. 型の抽象構文木の構成要素を知る

ここを見てくれ!

36. TypeReprからType[?]を得る

TypeReprasTypeを呼ぶと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. 型エイリアスを解決する

型エイリアスを解決したいときもあるよナァ? そんなときはTypeReprdealiasを呼ぶといいぞ!

type MyInt = Int
import quotes.reflect.*

val tpr = TypeRepr.of[MyInt].dealias
println(tpr.show(using Printer.TypeReprShortCode))
// Int
41. 型レベルの計算結果を得る

型レベルのmatchとかで計算したいことってあるよナァ? その計算結果も知りたいよナァ? そんなときはTypeReprsimplifiedを呼ぶと計算してくれるぞ!

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ではなにかとシングルトン型になっているよナァ? TypeReprwidenでシングルトンではない型にできるぞ!

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 typesRefinementという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. 定義を参照する

TypeReprtypeSymbolで型の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. オーバーロードされたメソッドを呼ぶ式を生成する

Select.overloadedを使え!

49. 引数のないメソッドを呼ぶ式を生成する

appliedToNoneを使え! 「引数のないメソッド」というのはtoStringみたいに()をつけずに呼ぶメソッドのことだ.

50. 抽象構文木の親を辿る

SymbolspliceOwnerownerで抽象構文木の親要素を辿れるぞ! 以下では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)

参考: Scala 3 マクロ入門 · eed3si9n

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するか, OptionEitherなどでやろう!

応用

56. DynamicSelectableselectDynamicをマクロ化する

DynamicSelectableは, 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. DynamicSelectableapplyDynamicNamedをマクロ化する

同様に, applyDynamicapplyDynamicNamedもマクロにすると, 任意のメソッド呼出しをマクロで書き換えることができるぞ! とくに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.ofSeqExpr[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. unapplytransparent inlineにする

なんとできない!! コンパイラのバグだ!

修正されていそうだから, 近々できるようになるだろう.


63. 型変数かどうか調べる

TypeReprtypeSymbolから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]]できればTTag[?]だということだ!

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マクロで型情報だけ計算する

giventransparent 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マクロで計算された結果の型を取り出す

以下のようにするとMyTypertype Outtpeという名前で取り出せるぞ!

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.Outgivenマクロで計算される型で, それをうまく他の制約と組み合わせることで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
              },
            ]
        }
    }
  }
}

これらを使うメソッドは以下を満たすようにする.

  1. inlineにする
  2. 本体を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. マクロでクラス定義を書き換える

MacroAnnotationclassobjectにも使えるぞ!

長いのでコードは貼らないが, たとえば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$1
lampepfl/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個あるとか頭おかしいんじゃねーの!