マイクロアドのサーバサイドエンジニアの松宮です。本日はある日の社内日報を焼き直して、外部公開することにしました。
・・・
ある晴れた日のこと。Specs2でテストを書いていて、下記のようなコードをコンパイルをすると警告が出ることに気が付いた。
...
val hoge = mock[Hoge]
hoge.apply(any()) returns fuga
...
「よし!コンパイル!!」
「えっ」
これはその文字通り、絶対に実行されないコード(dead code)がソース中に存在する場合に教えてくれるイケてる警告文。しかし、如何せん、警告と実際のコードの関係が分からない。
そこで、気になったので調べてみた。
Ywarn-dead-codeの有効化のススメ
その前にdead codeの警告をおさらいしよう。
dead codeの警告をしてもらうためには、コンパイラオプションに-Ywarn-dead-code
を指定する。sbtを使っているならscalacOptions += "-Ywarn-dead-code"
で追加する。
すると、うっかり下記のようなコードを記述した場合に親切にも警告してくれる。
$ scala -Ywarn-dead-code scala> def func(): Unit = { | println("hello") | return | println("world") | } <console>:13: warning: dead code following this construct return ^ func: ()Unit
returnの後は実行されないのでdead codeだと分かる。便利なので有効にして損は無い。
any()の何が悪いのか?
さて、話を戻すと本題はhoge.apply(any()) returns fuga
で何故dead codeうんぬんと警告されたのかだ。
そう、主犯はany()
。これの説明は置いといて肝心なのはそのシグネチャ。
def any[T](): T = org.mockito.ArgumentMatchers.any[T]
ポイントは2つある。
- 型パラメータTを取ること
- Tが結果型になっていること
つまり、型パラメータがIntであれば、Int型を返すし、StringであればString型を返すメソッドになる。
ではコードを振り返ろう。
hoge.apply(any()) returns fuga
any()
を型パラメータの指定をせずに呼び出していることが分かる。それでもコンパイルエラーにならないのはご存知の通りScalaのコンパイラには型推論が備わっているからだ。
では、any()
を型パラメータなしでコンパイルすると何の型で推論されるのだろうか。
NothingとAny
答えはNothing
。答えが無いという意味ではなく、Nothing型に推論される。Nothing
とは「全ての型のサブタイプ」。つまりAny
の対局に位置する(Any
は全ての型のスーパータイプ)。
何故Nothing
に推論されるかだが、Nothing
はあらゆる型に適合出来るからだ。(適合という言い回しが正しいのかは不明)
例えば下記の例を見ると分かりやすいかも知れない。
class A(val value: Int) class B(value: Int) extends A(value) class C(value: Int) extends B(value) val a = new A(1) val b = new B(2) val c = new C(3) def printB(arg: B): Unit = println(arg.value) printB(a) // NG printB(b) // OK printB(c) // OK
クラスA
, B
,C
を用意してA
が最も親、C
が最も子という継承関係を作る。そして、B
を引数に取る関数を用意する。引数には自分かその子クラスしか渡せない。
つまり、B
とその子クラスのC
がprintB
に渡せる。
すごく当然だが、この考えを推し進めると、最も親であるAny
はprintB
の引数には渡せず、最も子であるNothing
はprintB
の引数に渡せる事が分かる。
これがあらゆる型に適合出来る所以であり、Nothing型に推論しておけばどんな引数型にも適合可能になる。
Nothing
の詳細はscala.Nothingは何のためにあるのか - kmizuの日記が詳しい。
Nothingとdead code
Nothing
に推論されるのは分かった。では、dead codeうんぬんの警告との関係性は何だろうか。
実はNothing
は他の型には無い特徴があり、それは「Nothingには値が無い」*1こと。
要はインスタンス化できない型のことで、メソッドが返り値を返さない事を明示するために使われる。例えばsys.exit()
は返り値を返す必要が無いのでNothing型で定義されている。
それを踏まえるとコンパイラはきっと次のように解釈するのだろう。
- お、結果型が
Nothing
だ - 返り値がないメソッドだな?
- それ以上は実行されないよね。
- それ以降にコードがあればdead codeの警告をしてあげよう!
なので、型パラメータTをNothing
で推論されたany()
以降の呼び出しは全てdead code扱いになる。
よって、タイトルの警告が発生していた。
まとめ
本日は、Nothing
と警告のお話でした。詳しくは scala - Why do I get 'Dead code following this construct' with the following code? - Stack Overflow も併せてご覧下さい。
ちなみに、警告を回避したい場合はany[Hoge]()
のように明示的に型を指定するか、scalacOptions in Test -= "-Ywarn-dead-code"
とかで対応するのが良いかと思います。