読者です 読者をやめる 読者になる 読者になる

型推論ふりかえり

型推論、よく聞きますね。よく聞くのでWikipediaにも型推論のページがあります。

この仕組みの恩恵は案外自然と享受しているものですが、機能を提供することライブラリの開発においては知らないと始まらない前提のようなものであると最近思います。が、自然と享受しているので「あーはいはい」となんとなくの理解だったりします。主に私が。

なのでどう考えればいいかを順に辿っていきます。

  • 型推論アルゴリズムの話はしません。人間がどう考えるかをなぞるだけです
  • コードはすべてScalaですし、型推論の考え方も私がScalaを通じて学んだものです。他の言語では保証できません

型推論をしないケース

型推論の話をする前に、まったく型推論しないケースを考えてみます。以下のようなケースはそうです。

val name: String = "takkkun"

変数nameの型はStringで、String型の値である"takkkun"を代入しているので、まあ普通のコードです。ただやたら丁寧なだけですね。

型推論とは「型に関する情報が欠けている箇所を補うもの」である

私は特別型推論に詳しいわけではないのですが、型推論が何をしてくれるものかと言うと、型に関する情報が欠けている箇所を補ってくれるものだと思っています。例えば以下のケース。

val name = "takkkun"

型推論をしないケースと比べると、変数nameの型が指定されていません。つまり型に関する情報が欠けています。ですので型推論が働き、右辺の"takkkun"String型の値であるので、変数nameString型だな、となります。難しい話じゃないですよね。

型の情報が欠けるケースは代入先だけかと言われるとそんなことはありません。代入元(右辺)に型の情報がないこともありえます。型パラメータを使っている場合です。以下はまたまた型推論しないケース。

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と呼び出すと、やはり型の情報が欠けることになるわけです。というわけで型推論の出番。以下のように考えると分かりやすいかなーと思います。

  1. 代入先の変数loverの型はOption[String]である
  2. empty[A]メソッドの戻り値はOption[A]である
  3. empty[A]メソッドの戻り値をOption[String]にすると型が一致するので、AStringとする

おお、AStringと解決できましたね。こんな風に求める側の型(変数の型, 関数の仮引数の型, 関数の戻り値の型, 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]が求まり、という話になってくるのですが、それは型クラスの話なので今回は割愛します。

私がナイスと思った具体例、代入先から代入元を推論するケースばかりですね。

まとめ

実際のところ型推論は便利です。記述がすっきりします。変数の型指定や無名関数の仮引数の型指定、関数が求める型パラメータの指定などを省略できます。

しかしどのケースにおいても重要なのは「型に関する情報が欠けている箇所を補う」ために型推論は働き、その推論の元となる型は必ずどこかに指定があります。推論は既にある事実がないことには始まりませんからね。

型推論は案外付きまとうものですが、人の頭でなぞれば分かるよ、怖くないよという気付きと振り返りでした。

*1:ちなみにこのケースだと、変位指定のおかげで代入先と代入元、どちらにも型の情報が無くてもコンパイルは通りますが、それはまた別のお話なので今回は端折ります