型引数がNothingになるとコンパイルエラーにする方法( NotNothing Pattern ? )

かなり後になって追記:
最近のcasbahに取り入れられてるっぽい
https://github.com/mongodb/casbah/compare/7e2256f155...a7e6cf5ee6#L0R109
https://github.com/mongodb/casbah/compare/7e2256f155...a7e6cf5ee6#L1R55
以下、元の文章


twitter上ではつぶやいてたんですが、結構重要で、実用的かもしれないと思ったのでblogにまとめてみる。

以前自分のblog上で、戻り値の型が型引数に依存してる関数についてというの書いて、その中で型引数が推論できない場合Nothingに推論されて実行時エラーになることを書いたわけですが、それをコンパイル時に防ぐ方法をたまたま見つけたという話。表題の NotNothing Pattern っていうのは、今勝手に自分が名づけました。経緯はこんな感じ↓


retronym さんの gistを引用↓

ちなみに、このgistのコード書いた人は、IntelliJScalaプラグイン作ってたり、Scalazっていう「Haskell likeな、Scalaでより純粋関数型プログラミングをするためのライブラリ(?)」のコミッターだったりすごい人です。

上記のコード説明しようとすると、そもそもManifestについて触れないといけないんですが、Manifestについての日本語での説明は(ちょっと古い?けど)このへんが詳しいかなぁ・・・(´・ω・)つ Scala東北のページ

誤解を恐れずにというと、ようはManifestってある意味 implicit parameter のシンタックスシュガーでもあるわけです。
で、しかもそのimplicit parameterに渡されるオブジェクトはどこからくるのかっていうのが、普通にimplicit parameterを定義したときとは違って、型引数からコンパイラが勝手にimplicitパラメータに渡しちゃうっていう感じでしょうか? *1

それを試すために以下のようなコードで説明 *2

scala> def hoge[A:Manifest] = println( manifest[A].erasure )
hoge: [A](implicit evidence$1: Manifest[A])Unit

まず、実引数はなしで、型引数に [A:Manifest] という指定をして関数定義。すると、実引数なしのはずなのにその後に、(implicit evidence$1: Manifest[A])というimplicit parameterをコンパイラが勝手に追加していることがわかります。ちなみに、erasure っていうのは、java.lang.Classを取得するメソッドです。
これを使う側はこんな感じ。

scala> hoge[Int]
int

//なにも指定しないとObjectクラスが渡るの・・・?
scala> hoge
class java.lang.Object

//(普通こんなことやらないけど) 実は明示的に渡すことできちゃう
scala> hoge(manifest[String])
class java.lang.String

// nullを明示的に渡したら、nullに対してerasureが呼ばれることになっちゃってぬるぽ発生(´・ω・`)
scala> hoge(null)
java.lang.NullPointerException
	at .hoge(<console>:7)
	at .<init>(<console>:9)
	at .<clinit>(<console>)
	at .<init>(<console>:11)
	at .<clinit>(<console>)
	at $export(<console>)
 (以下略

// 両方明示したらそりゃエラーになるよね・・・
scala> hoge[Int](manifest[String])
<console>:9: error: type mismatch;
 found   : Manifest[String]
 required: Manifest[Int]
       hoge[Int](manifest[String])
                         ^

なんだかちょっと脱線(?)して、Manifestの説明が長くなってしまいましたが、もとの NotNothingの話に戻ります。元コードでは一度 trait NotNothing[_] というtraitを定義してます。そのあとの、

implicit val amb1, amb2 = new NotNothing[Nothing]{}

というのがポイントです。

Scalaでimpicit parameterの決まりについては、渡されるオブジェクトが、

  1. スコープ内で一つもない場合はコンパイルエラー
  2. スコープ内でーつだけある場合はそのオブジェクトが自動で渡される
  3. 同じスコープ内で、(同じ優先度のものが) 2つ以上あるとコンパイルエラー

という決まりがあります。*3 これの3つ目の特徴をわざと利用するわけです。


それふまえて、casbahという、MongoDBのScala driver用ライブラリで実験していきますよ(・ω・*) *4
casbahの中に、MongoDBObjectっていうクラスがあって、それのgetAsっていうメソッドのシグネチャが以下のようになってます

def getAs[A <: Any: Manifest](key: String): Option[A] 

MongoDBってデータがBSONっていうまぁJSONとだいたい互換性のある、数値とか文字列とか複数の型が入ってて、しかもそれが入れ子になってる可能性があるので、メソッド呼ぶ側で型引数で明示してオブジェクト取得するようになってるわけです。で、このメソッドについて以下のようなことやってみました

こんなかんじで、
型引数にManifest指定しておいて、しかもそれがNothingにならないように、ダミーのManifest[Nothing]型のオブジェクトをimplicit で定義しておくパターンって結構実用的に使えるんじゃなイカ?

っていうのが今のところの自分の中の結論というか、言いたかったことです。そして、それを飛躍させて、そもそも型引数にNothingが渡る場合なんて、プログラマが意図していない場合がほとんどだから、

などと勝手に妄想していました。せめて、

object NotNothing{
  implicit val amb1 , amb2 = manifest[Nothing]
}

というのを定義しておいて、基本的にどこでもimport しちゃえばいいんじゃね?などと考えて・・・
がしかし! specsのコードで、上記のような、NotNothingをimportしたら、inのメソッドの型引数のところで、コンパイルエラーになってしまったりして*5、そんなに単純にうまくいかないなーとかいう感じです(・ω・`)もとの retronym さんの gist みたいに、一度 trait NotNothing を定義しておけば、こういう問題発生せずにうまくモジュール化とかできるのかなーとか

*1: なんだか説明が雑ですが、自分あまり詳しくないので、誰かもっと正確にわかりやすくまとめてくれないかな・・・|ω・`) ClassManifestとManifestの違いとか、さらにはOptManifestとか2.9以降入ってるし・・・

*2: version 2.9.0.1

*3: これ説明の都合上、3つ箇条書きで書いたけど、言語仕様でこういう感じに書かれているかどうかは知らない。厳密な形式的言い方するとちょっと正確じゃないと思う

*4: ちなみに、Scalaは2.8.1 で casbah は version2.1.2

*5: specsのinって、擬似的な構文として使うだけで、inの型引数ってどうでもいいわけでが、それに Manifest[Nothing] を渡そうとすることになっちゃってコンパイルエラーェ・・・