MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

dead code following this construct ...という警告

f:id:kmatsumiya6:20190418174106p:plain

マイクロアドのサーバサイドエンジニアの松宮です。本日はある日の社内日報を焼き直して、外部公開することにしました。

・・・

ある晴れた日のこと。Specs2でテストを書いていて、下記のようなコードをコンパイルをすると警告が出ることに気が付いた。

...
val hoge = mock[Hoge]
hoge.apply(any()) returns fuga
...

「よし!コンパイル!!」

f:id:kmatsumiya6:20190417233731p:plain:w1000

「えっ」

これはその文字通り、絶対に実行されないコード(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とその子クラスのCprintBに渡せる。

すごく当然だが、この考えを推し進めると、最も親であるAnyprintBの引数には渡せず、最も子であるNothingprintBの引数に渡せる事が分かる。 これがあらゆる型に適合出来る所以であり、Nothing型に推論しておけばどんな引数型にも適合可能になる。 Nothingの詳細はscala.Nothingは何のためにあるのか - kmizuの日記が詳しい。

Nothingとdead code

Nothingに推論されるのは分かった。では、dead codeうんぬんの警告との関係性は何だろうか。 実はNothingは他の型には無い特徴があり、それは「Nothingには値が無い」*1こと。

要はインスタンス化できない型のことで、メソッドが返り値を返さない事を明示するために使われる。例えばsys.exit()は返り値を返す必要が無いのでNothing型で定義されている。

それを踏まえるとコンパイラはきっと次のように解釈するのだろう。

  1. お、結果型がNothing
  2. 返り値がないメソッドだな?
  3. それ以上は実行されないよね。
  4. それ以降にコードがあれば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"とかで対応するのが良いかと思います。

参考文献