型推論ふりかえり
型推論、よく聞きますね。よく聞くのでWikipediaにも型推論のページがあります。
この仕組みの恩恵は案外自然と享受しているものですが、機能を提供することライブラリの開発においては知らないと始まらない前提のようなものであると最近思います。が、自然と享受しているので「あーはいはい」となんとなくの理解だったりします。主に私が。
なのでどう考えればいいかを順に辿っていきます。
型推論をしないケース
型推論の話をする前に、まったく型推論しないケースを考えてみます。以下のようなケースはそうです。
val name: String = "takkkun"
変数name
の型はString
で、String
型の値である"takkkun"
を代入しているので、まあ普通のコードです。ただやたら丁寧なだけですね。
型推論とは「型に関する情報が欠けている箇所を補うもの」である
私は特別型推論に詳しいわけではないのですが、型推論が何をしてくれるものかと言うと、型に関する情報が欠けている箇所を補ってくれるものだと思っています。例えば以下のケース。
val name = "takkkun"
型推論をしないケースと比べると、変数name
の型が指定されていません。つまり型に関する情報が欠けています。ですので型推論が働き、右辺の"takkkun"
がString
型の値であるので、変数name
はString
型だな、となります。難しい話じゃないですよね。
型の情報が欠けるケースは代入先だけかと言われるとそんなことはありません。代入元(右辺)に型の情報がないこともありえます。型パラメータを使っている場合です。以下はまたまた型推論しないケース。
val lover: Option[Person] = Option.empty[Person]
型推論を使うと:
val lover = Option.empty[Person]
とも書けるし:
val lover: Option[Person] = Option.empty
とも書けます*1。
前者は変数name
に"takkkun"
を代入するのと同じケースです。後者について詳しく見ていきます。まずOption
objectのempty
メソッドの定義を見てみます。
def empty[A] : Option[A] = None
型パラメータA
を取っていますね。なのでOption.empty
と呼び出すと、やはり型の情報が欠けることになるわけです。というわけで型推論の出番。以下のように考えると分かりやすいかなーと思います。
- 代入先の変数
lover
の型はOption[String]
である empty[A]
メソッドの戻り値はOption[A]
であるempty[A]
メソッドの戻り値をOption[String]
にすると型が一致するので、A
はString
とする
おお、A
はString
と解決できましたね。こんな風に求める側の型(変数の型, 関数の仮引数の型, 関数の戻り値の型, etc…)をJavaではターゲット型と呼ぶそうですが、そのターゲット型から推論するケースですね。
具体例
以下は型推論が働いていて「おお、ナイス」と個人的に思ったケースです。他にもきっとあります。
関数を代入するケース
val intToString: Int => String = { i => i.toString }
変数側にInt => String
型を指定しない場合、仮引数i
の型が不明となるため、val intToString = { i: Int => ... }
と書かなければならくなります。「まあ別にそれでもいいじゃん」と言ってしまえばそうです。が、このケースでは仮引数であるi
に対してtoString
メソッドを呼んでいるだけですので:
val intToString: Int => String = _.toString
と書けます。この書き方にすると、そもそもi
という仮引数が現れなくなるため、代入先の方に型を指定して推論させる必要が出て来ます。
当然ですが:
val lover: Option[Person] = Option.empty lover.foreach { person => // do something }
というケースでも、仮引数person
に型を指定しなくても大丈夫なのは、Option[Person]
型のforeach
メソッドがPerson => Unit
(正確にはUnit
じゃありませんが割愛)となり、そこから型推論できるためです。「変数への代入」も「実引数として与える」のも同じ推論が働くと思っていいと思います。
ScalikeJDBC
RDBとあれこれする場合はよくScalikeJDBCを使っているんですが、このときもよくこんな書き方(というかscalikejdbc-mapper-generatorによって生成される)をします。
object Person { def apply(p: ResultName[Person])(rs: WrappedResultSet): Person = new Person( id = rs.get(p.id), name = rs.get(p.name), lover = rs.get(p.lover), ... ) }
rs.get
の箇所ですが、このget
メソッドは型パラメータA
を受け取り、それが戻り値となるようになっています。ですので、キーワード引数によって与えられる先の仮引数の型からA
が何か推論されています。受け取る側の型を気にせずget
メソッドを並べれば良いので、すっきりしていますね。
で、さらにTypeBinder[A]
が求まり、という話になってくるのですが、それは型クラスの話なので今回は割愛します。
私がナイスと思った具体例、代入先から代入元を推論するケースばかりですね。
まとめ
実際のところ型推論は便利です。記述がすっきりします。変数の型指定や無名関数の仮引数の型指定、関数が求める型パラメータの指定などを省略できます。
しかしどのケースにおいても重要なのは「型に関する情報が欠けている箇所を補う」ために型推論は働き、その推論の元となる型は必ずどこかに指定があります。推論は既にある事実がないことには始まりませんからね。
型推論は案外付きまとうものですが、人の頭でなぞれば分かるよ、怖くないよという気付きと振り返りでした。