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

Model[A <: Model[A]] が意味するところを説明するよ

リラクヘルステック室の近藤です。このブログに書くのは今回が初。ということでよろしくお願いします。

一応軽く自己紹介でも。リラクでは主にサーバーサイドのコードをScalaで書いています。元々はRuby on Railsでよくアプリケーションを作っていましたが、Scalaに触れるのがおもしろく、趣味でScala、ひいてはErlangなどで適当なコードを書いていました。そんなところを丸山に声掛けてもらった感じです。

いやー、Scalaおもしろいです。まだまだ不慣れで、さらに実践的なコードを書くのは現職がほぼ初。型の扱いが難しく、そんなところにハマりつつも、その魅力にもハマったりで、二重の意味でハマっています。

そんな中で「なるほど!」と思ったコードがあるので、それを紹介します。

trait Model

class Person(val name: String) extends Model

こんな風にモデル(Model)という概念があり、それのひとつである人物(Person)というモデルを定義したコードです。何ら変哲のないコードですが、これに「Modelトレイトは自身と別のModelオブジェクト(Modelトレイトを継承したクラスのインスタンス)が同値であるか確認するためのメソッドを持つ」ように変更していきます。このメソッドを持たせられるようにするのが今回の目的です。

まずその「同値であるか確認するためのメソッド」はequalという名前にしましょう。

trait Model {
  def equal

そして「別のModelオブジェクト」を「自身」と比較するため、「別のModelオブジェクト」を受け取らなければなりません。よって続けて引数リストを書きます。

trait Model {
  def equal(other: 

ここで問題があります。この引数otherの型は何になるのでしょうか?Modelでしょうか?たしかにModelなのですが、正確には「Modelトレイトを継承したクラス」のはずです。ではPersonでしょうか?でもModelはPerson以外にも定義されていくはずです。ですので、Modelは型パラメーターを受け取らなければなりません。

trait Model[A] {
  def equal(other: A

こうなります。あとは戻り値の型を書けばいいですね。今回は「同値であるかを確認する」ことが目的なので、戻り値はBooleanで良さそうです。

trait Model[A] {
  def equal(other: A): Boolean
}

これで無事にModelトレイトは「自身と別のModelオブジェクト(Modelトレイトを継承したクラスのインスタンス)が同値であるか確認するためのメソッドを持つ」ことが約束されました。実際:

class Person(val name: String) extends Model[Person] {
  override def equal(other: Person): Boolean = {
    this.name == other.name
  }
}

と書けます。良い感じです。

しかし、まだ問題があります。この型パラメーターAはどんな型でも指定できます。ですので:

class Person(val name: String) extends Model[Int] {
  override def equal(other: Int): Boolean = {
    // ?
  }
}

ということも出来てしまいます。コンパイルエラーにはなりません。しかしPersonとIntが同値であるかを確認するのは本意ではないはずです。

つまり型パラメーターAはModel[A]を継承した型でなければいけません。その制約を書き加えていきましょう。

trait Model[A <: Model

<:を用い、型パラメーターAに制約を加えます。この後は]を書いてしまいたいところですが、今はModelではなくModel[A]型なので、]を書くと型パラメーターがないとコンパイルエラーになってしまいます。よって型パラメーターを指定する必要があります。ではどの型を指定すれば良いのでしょう?とりあえずここでは:

trait Model[A <: Model[_]] {
  def equal(other: M): Boolean
}

_でワイルドカード指定します。こうすると型パラメーターAはModel[A]を継承した型であれば良いが、Aの型は問わない、ということになります。

「Aの型は問わない」のですが、本当にこれで良いでしょうか?試してみましょう。

class Calendar(val year: Int, val month: Int) extends Model[Calendar] {
  override def equal(other: Calendar): Boolean = {
    this.year == other.year && this.month == other.month
  }
}

こんな風にPersonとは別のモデルであるCalendarモデルを定義します。CalendarはModel[A]を継承した型です。ですので:

class Person(val name: String) extends Model[Calendar] {
  override def equal(other: Calendar): Boolean = {
    // ?
  }
}

こう書けてしまいます。もちろんコンパイルエラーにはなりません。しかししかし、型パラメーターAにIntを与えたときにも言いましたが、これは本意ではないはずです。

ではどうすれば良いのでしょうか。こう書きます。

trait Model[A <: Model[A]] {
  def equal(other: A): Boolean
}

_ではなく、Aとしました。A、つまり型パラメーターAです。なんだか不思議な感じですね。私はこの書き方を見てそう思いました。だってAの制約にAが現れるんですよ?頭がこんがらがります。でも実際PersonがModel[Person]を継承した場合、A <: Model[A]にあてはまりますが、PersonがModel[Calendar]を継承した場合、 A <: Model[A]にはあてはまりません。つまり制約として正しいわけです。

実はこの書き方、よくある書き方なんです。F-bounded quantificationなんて呼ばれます。ScalaではF-bounded polymorphismと呼ぶのか、そんな名前が見付かります。またgithubにあるコードや書籍でもよく見掛けます。私はScalaプログラミング入門で見掛けました。その書籍を読んだ当時の私は「え?」となりましたが、何ら不思議ではない話のようです。

こうして無事に型パラメーターAに制約を加えることが出来ました。昔書籍で見た「え?」となるような書き方ですが、たしかに制約としては正しく、その制約にはF-bounded quantificationという名前まで与えられていると知り、「なるほど!」と思った次第です。

とりとめのないことですが、こんなことを書いていけたらいいな、って思います。どうぞよろしくです。