Scala におけるユーザランドでのコンパイラ警告

in

一ライブラリ作者として、Scala でメソッドをタグ付けしてカスタムのコンパイラ警告やエラーを発動できるといいなと前から思っている。何故意図的にコンパイラエラーを出す必要があるのかと思うかもしれない。一つのユースケースとしては、API を廃止した後でマイグレーションのためのメッセージを表示させることだ。

僕はこれを Restligeist macro、つまり地縛霊マクロと呼んでいる。例えば、sbt 1.3.8 において <<= を使うと以下のエラーメッセージが起動時に表示される。

/tmp/hello/build.sbt:13: error: `<<=` operator is removed. Use `key := { x.value }` or `key ~= (old => { newValue })`.
See http://www.scala-sbt.org/1.x/docs/Migrating-from-sbt-013x.html
    foo <<= test,
        ^
[error] sbt.compiler.EvalException: Type error in expression
[error] Use 'last' for the full log.
Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore?

これ実現可能というのは良いことだけども、わざわざマクロを使わなければいけないのいうのが仰々しい。吉田さんによると Haskell だとこれぐらいのことは型シグネチャに Whoops と書くだけでできるらしい:

-- | This function is being removed and is no longer usable.
-- Use 'Data.IntMap.Strict.insertWith'
insertWith' :: Whoops "Data.IntMap.insertWith' is gone. Use Data.IntMap.Strict.insertWith."
            => (a -> a -> a) -> Key -> a -> IntMap a -> IntMap a
insertWith' _ _ _ _ = undefined

configurable な警告

2019年3月に僕は scala/scala に #7790 という pull request を送って @compileTimeError というアノテーションを提案した。レビューの流れを受け入れているうち pull request は @restricted アノテーションと configurable な警告オプション -Wconf というものに変わっていった。@restricted はラベルによってメソッドのタグ付けを行い、-Wconf はそのタグを -Wconfig apiMayChange:foo.*:error というふうにして警告やエラーにエスカレートさせることができるというものだ。

Scala 2.13.0 のリリースが近かったということもあって残念ながら #7790 は撃沈させられてしまったが、その夏に同僚の Lukas Rytz (@lrytz) の手によって -Wconf は全ての警告をカテゴリー、メッセージ内容、ソース、タグ元、deprecation の since フィールドによってふるい分ける汎用フィルター #8373 として復活した。これを使うことで例えば、ライブラリのユーザが特定のバージョンの廃止勧告だけをエラーにするということができるようになる。#8373 は既に merge されて、次の Scala 2.13.2 に入る予定だ。

ApiMayChange アノテーション

API のステータスを表したものの一例として Lightbend の Akka ライブラリにいくつか面白いものがある。例えば、ApiMayChange はタグ付けされた API が通常のバイナリ互換性保証の例外であることを表す。つまり、これがついた機能はベータ版であって将来変わるかもしれないということだ。

これは長期的にサポートされるライブラリには便利なタグだと思う。このアノテーションの興味深い所はこれは純粋に社会的な慣習によってのみ成り立っていることだ。つまり、"may change" と言っている API を使ってもコンパイラは一切警告を出してくれない。

apiStatus アノテーション(案)

-Wconf は便利だが、今のままでは警告を出すためにライブラリ作者に渡されたツールはマクロという手段を除くと @deprecated アノテーションのみだ。先週末 #8820 を scala/scala に送って @apiStatus というユーザーランドでコンパイラ警告やエラーを出せる仕組みを再提案した。

具体例を用いて説明する。例えば <<= メソッドをエラーにしたいとする。

import scala.annotation.apiStatus, apiStatus._
 
@apiStatus(
  "method <<= is removed; use := syntax instead",
  category = Category.ForRemoval,
  since = "foo-lib 1.0",
  defaultAction = Action.Error,
)
def <<=(): Unit = ???

このメソッドを呼び出すとこうなる:

example.scala:26: error: method <<= is removed; use := syntax instead (foo-lib 1.0)
  <<=()
  ^

カスタムでコンパイラーメッセージを出せるようになった。

ApiMayChange を実装する

ApiMayChange アノテーションを実装してみよう。

package foo
 
import scala.annotation.apiStatus, apiStatus._
 
@apiStatus(
  "should DSL is incubating, and future compatibility is not guaranteed",
  category = Category.ApiMayChange,
  since = "foo-lib 1.0",
  defaultAction = Action.Silent,
)
implicit class ShouldDSL(s: String) {
  def should(o: String): Unit = ()
}

Akka にならって、デフォルトのアクションは Action.Silent なので警告は表示されない。ここで -Wconf の出番だ。-Wconf:cat=api-may-change&origin=foo\..*:warning をオプションに渡すことで、ユーザサイドで foo.* パッケージ内の api-may-change というカテゴリーのみを警告にすることができる。

example.scala:28: warning: should DSL is incubating, and future compatibility is not guaranteed (foo-lib 1.0)
  "bar" should "something"
  ^

defaultAction = Action.Warning とすることでデフォルトでこれを警告にすることも可能だ。

ユーザランドでの警告とエラー

category フィールドはただの String なので想像力を働かしてクラスやメソッドを好きなようにタグ付けすることができる。 (またクロスビルド用に古い Scala に自分で backport するのも容易になると思う)

とりあえず、ユーザランドでの警告やエラーというアイディアについてどう思うだろうか。#8820 で +1/-1 をポチっと押すかコメントでご意見を聞かせてほしい。