Scala 3で独自にunion typeのleast upper boundを計算する仕組みを作った

Scala 3では、以下のように | というunion type?or type?という新しい仕様が追加されました。

(仕様というか、少なくとも以下の公式ドキュメントではunionと呼んでるが、Scala 3のquoteのAPI内部ではOrTypeでややこしい)

https://docs.scala-lang.org/scala3/book/types-union.html

Welcome to Scala 3.3.0-RC3 (11.0.18, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
                                                                                                                        
scala> List(2, "a")
val res0: List[Int | String] = List(2, a)

しかし、これをScala 2でおこなったら、当然以下のように Any などになります

Welcome to Scala 2.13.10 (OpenJDK 64-Bit Server VM, Java 11.0.18).
Type in expressions for evaluation. Or try :help.

scala> List(2, "a")
val res0: List[Any] = List(2, a)

このAnyなどの 「共通の親のうちで一番近い型」 になることをleast upper boundと呼びます。 (Scala 2公式の仕様として) (Javaなどのsub typeがある他の言語でもある程度同じかも? )

leastなupper、で一瞬何言ってるのか?みたいな気持ちになるかもしれませんが、 「最小公倍数」とか「最大公約数」みたいな感じと同じです (雑な説明と例・・・伝われ・・・)

つまり

class A
class B extends A
class C extends B
class D extends B
class E extends A

という場合に CD の共通の親は BA 、その他暗黙的に継承する AnyAnyRef などがありますが、一番近いのはBなので、この場合の CD のleast upper boundは(Aではなく) B です、という具合です。

さて、Scala 3でunion typeをどんどん使っていく方針、かつ、Scala 2とのcross buildする必要がない状況なら、今回の話は基本的に関係ない可能性が高いです。 逆に?言い換えるというか、これをやりたくなったのは

  • Scala 2とScala 3でcross buildする状況である
  • よって、変な状態のunion type(詳細後述)になるのは避けたい
  • しかし「変な状態のunion typeになったかどうか?」の判断基準が難しい
  • よってwartremoverでそれを作った

という流れです。

さて「変な状態のunion type」とは 「union typeのleast upper boundを計算した結果がAnyRef、Matchable、Anyなどになる」 ということです。

つまり最初に出した例のStringとIntのleast upper boundは、Scala 2ではAnyだし、Scala 3ではMatchableです。

よって、これを検出したい、ということです。

ちなみに、Scala 3.2以前と、Scala 3.3以降で、このあたりの仕様が変わってるようなので気をつけてください。 Scala 3.2では、例えばIntとStringを混ぜた場合に、デフォルトでは least upper bound のMatchableになるようです。

Welcome to Scala 3.2.2 (11.0.18, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
                                                                                                                        
scala> List(2, "a")
val res0: List[Matchable] = List(2, a)

なので、Scala 3.2以前なら、単にMatchable検出をすればよかったパターンが多かったのですが、Scala 3.3以降で、そのテクニックが使えなくなりました

それで作ったものが以下です

詳細な実装としては

  • まずwartremoverで普通にOrTypeを検出
  • それを無理やりcompilerの内部の型にcastして(!?)、widenUnionというメソッドで、least upper boundを計算する
  • 計算したleast upper boundが、AndTypeになっていたらそれを分解
  • 分解して出てきた型の中に、禁止したい型(Any、AnyRef、Matchableなど)が存在したらerrorにする

という流れです。

Scala 2では

  • そもそも標準で "-Xlint:infer-any" がある
  • でもそれだと検出される箇所が少ない
  • 逆にwartremoverのScala 2のものは、余計な場所まで検出し過ぎる(どうして・・・)

といった状況なのですが、このScala 3のwartremoverのOrTypeLeastUpperBoundを使うと、なぜか現状かなりいい感じに検出してくれます。 実際これ使ったら細かいミスがいくつか見つかりました。

以上で大体説明は終わりです。

また、これに関連して、今後、Scala 3で仕様から消えたweak conformance関連のwartremoverのruleを作りたい、というか作ってる最中なので、 それが完成してリリースしたら、また何か書くかもしれません。

最後に完全に関係ない余談として、 諸事情により1年くらい前から魚にやられて、指にイボができて液体窒素で治療したのに復活したりして治ってないのですが、タスケテ・・・

wartremoverで画像検索すると、自分のスライドやblogと同時に、今の自分の指の状態とそっくりなもの出てくる・・・wartremoverでScalaコード内のwartは除去出来るけど自分の指のwartの除去が・・・

wart-remover-en-to-ja