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という名前まで与えられていると知り、「なるほど!」と思った次第です。
とりとめのないことですが、こんなことを書いていけたらいいな、って思います。どうぞよろしくです。