🐾

Scala3のマクロでメソッド名を取得する

2025/02/17に公開

目的

あるメソッド内で生成する文字列にメソッド名を入れたいということがあり、ハードコードするとメソッド名の変更に際し変更漏れが発生することがあり得るのでマクロで自動取得することを目的とする。
下記のコードの文字列リテラル中の"process"を自動で取得したいというイメージである。

def process: Unit = {
  println("process: xx中")
}

実装

マクロ呼出自体の情報はSymbol.spliceOwnerによって取得できる。マクロ呼出のコードが含まれているメソッドの情報を取りたいので.owerによって呼出元情報を取得する。また.nameによって名前が取得できるので名前を取得したいメソッドの直下で呼び出すことを前提とするのであればこのように書ける。

inline def methodName: String = ${ methodNameImpl }
private def methodNameImpl(using Quotes): Expr[String] = {
  import quotes.reflect.*
  Expr(Symbol.spliceOwner.owner.name)
}

上記の実装の場合、無名関数の中で呼び出されると意図したように機能しない。
PlayframeworkのControllerのActionを書くときは下記のようにActionBuilderのblockとして無名関数を書かざるを得ない。このとき、取得したいのはアクションを実行するindexメソッドの名前なのでこのようなケースにも対応するためにはもう少し変更を加える必要がある。

def index: Action[AnyContent] = Action { implicit request => ??? }

名前を取得したいのはメソッド定義かつ無名関数ではないものなので、それが見つかるまで呼出元を辿る必要がある。isDefDefisAnonymousFunctionを使って判定し、条件に合致しない場合は呼出元に対して適用するということを再帰的に行う。(エントリポイントのmainメソッドがあるので無限呼び出しにはならないはず......)

@tailrec
def callerMethod(symbol: Symbol): Symbol = {
  if (symbol.isDefDef && !symbol.isAnonymousFunction) { symbol }
  else callerMethod(symbol.owner)
}

上記のメソッドを利用すると最終形は下記のようになる。

inline def methodName: String = ${ methodNameImpl }
private def methodNameImpl(using Quotes): Expr[String] = {
  import quotes.reflect.*
  @tailrec
  def callerMethod(symbol: Symbol): Symbol = {
    if (symbol.isDefDef && !symbol.isAnonymousFunction) { symbol }
    else { callerMethod(symbol.owner) }
  }
  Expr(callerMethod(Symbol.spliceOwner.owner).name)
}

参考資料

Discussion