皆、元気してた?久々の投稿をさせてもらうね。
今回から 応用編 に突入するよ!
今回のテーマは、 型パラメータ だ!
こんな感じで語っていくからね。
型パラメータとは
型パラメータってなんだろ? 意味分かんないぞ!
と思っていたら、Javaで言う ジェネリックス(総称型) のことを、Scalaでは 型パラメータ と呼ぶみたいなんだ。
型自体をパラメータとし使うことができるんだ。
クラスの定義自体はジェネリックスにしておいて、インスタンス化するときに型を決定する。
なんてことができるようになるんだ。
例えば、JavaのArrayListの定義を抜粋するとこんな感じです。
public class ArrayList<E> {...}
<E>でジェネリックスだぜって言っておいて
実際に使うときは、
final List<String> arrays = new ArrayList<String>()
として、EだったところにStringとして、ArrayListのインスタンスを生成します。
これでこのListには、String型の要素しか入れれないぜ!てことができるんだ。
同じことを、もちろんScalaでもできるのです。
型パラメータのメリット
Scalaでの例を見る前に、ちょっとメリット的なものを。
僕は具体的な型パラメータを決めることを、 型縛り と呼ぶんだけど
型縛りできるおかげで
- 違う型を使うとコンパイルエラーになる
- キャストが必要なくなる
なんて恩恵を得られるようになるんだ。
例えばコレクション系のMapやSeqで型縛りすれば、 縛った型 の要素だけを入れるようにすることができる。
あと抽象度が高いモノが作れるんだ。
さっき見たJavaのArrayListがまさにそうで、ArrayList自身は何かの要素を入れれる箱として定義している。
箱に何を入れるかはインスタンス化する時に決めれる。
型パラメータを使ってみる
では、Scalaでの型パラメータを使ってみよう!
こんなの作ってみたぜ!
object TypeParamSample {
class TypeParam[T](val t: T) {
def get: T = this.t
}
def main(args: Array[String]): Unit = {
val stringTypeParam = new TypeParam[String]("test")
println(stringTypeParam.get)
val intTypeParam = new TypeParam[Int](1)
println(intTypeParam.get)
}
}
説明するぞ
型パラメータのクラスの定義
class TypeParam[T](val t: T) {
def get: T = this.t
}
class TypeParam[T]で型パラメータを使うクラスを宣言したぞ。
Javaだと<...>なんだけど、Scalaだと[...]になるんだ。
何がTなんだ?ということで、インスタンス変数tの型を型パラメータで解決することにする。
def get: T = this.tはtの値を返すメソッドです。
使う側
使う側はシンプルだよ!
def main(args: Array[String]): Unit = {
val stringTypeParam = new TypeParam[String]("test")
println(stringTypeParam.get)
val intTypeParam = new TypeParam[Int](1)
println(intTypeParam.get)
}
ここではStringとIntの2つの型で、TypeParamを使ってみました。
動かす
$ scalac TypeParamSample.scala
$ scala TypeParamSample
test
1
ちゃんとgetできているね!
型境界
次に 型境界 だ!
型境界とは型の範囲を決めることです。
ある程度ゆるい範囲を型パラメータとして使えるようになるのがメリットかな。
パラメータ化した型の範囲を定義します。
Scalaの型は、Anyを頂点として継承の階層構造となっているので、範囲を決めることができるのです。
Javaの<? extends A>と似ていますね。
- 上限境界
- 下限境界
があります。
実際に見て行く前に、以下の型を定義しておきます。
scala> class Animal
defined class Animal
scala> class Person extends Animal
defined class Person
scala> class Student extends Person
defined class Student
上限境界
スーパー型の上限を決めるのです。
A <: Bで、以下のどちらかの制約になるんだ。
-
AはBのサブ型 -
AはBと同一型
上限境界を定義した新しいクラスを定義してみる。
scala> class School[A <: Person]
defined class School
型パラメータを与えてインスタンスを作ってみよう!
サブ型の場合
scala> new School[Student]
res0: School[Student] = School@7de3ec13
問題なしですね。
同一型の場合
scala> new School[Person]
res1: School[Person] = School@35203079
これも問題なし。
境界外の型の場合
scala> new School[Animal]
<console>:10: error: type arguments [Animal] do not conform to class School's type parameter bounds [A <: Person]
val res2 =
^
<console>:11: error: type arguments [Animal] do not conform to class School's type parameter bounds [A <: Person]
new School[Animal]
^
Animalは 上限境界 を超えているので、エラーになりました!
下限境界
サブ型の下限を決めるのです。
まぁ上限の逆だよね。
A >: Bで、以下のどちらかの制約になるんだ。
-
AはBのスーパ型 -
AはBと同一型
今度は、下限境界を定義した新しいクラスを定義してみる。
このクラス達を使ってみてみよう!
scala> class School[A >: Person]
defined class School
下限境界でも、型パラメータを与えてインスタンスを作ってみよう!
スーパー型の場合
scala> new School[Animal]
res0: School[Animal] = School@4d832421
問題なしですね。
同一型の場合
scala> new School[Person]
res1: School[Person] = School@4d032cfc
これも問題なし。
境界外の型の場合
scala> new School[Student]
<console>:11: error: type arguments [Student] do not conform to class School's type parameter bounds [A >: Person]
val res2 =
^
<console>:12: error: type arguments [Student] do not conform to class School's type parameter bounds [A >: Person]
new School[Student]
^
Studentは 下限境界 を超えているので、エラーになりました!
上限・下限境界
上限と下限を同時に設定することもできるんだ。
scala> class School[A >: Student <: Animal]
defined class School
これはAnimal Person StudentをOKにしたんだ。
変位指定アノテーション
これはJavaの世界ではない考え方です。
Javaの場合、StringがObjectのサブクラスだからといって、List<String>がList<Object>のサブクラスとは言えないですよね。
つまり
final List<String> list = new ArrayList<String>();
final List<Object> objList = list;
とすると、2行目がコンパイルエラーになります。
でもScalaではこれが可能となります!
そして、それを可能とするのが変位指定アノテーションです。
Scaladocを覗いてみます。ここではListクラスを取り上げます。
sealed abstract class List[+A] extends AbstractSeq[A] with LinearSeq[A] with Product with GenericTraversableTemplate[A, List] with LinearSeqOptimized[A, List[A]]
ちょっと例としてはかなり複雑ではあるですけどね。。。
なのでちょっと抜粋します。
class List[+A]
これでわかりやすくなったでしょ。削り過ぎとかは言わないでね!
このListクラスは型パラメータです。
そして型パラメータの+Aとなっています。
この中の+が、 変位指定アノテーションの世界 となります。
実は、+なしの場合も、立派に変位指定アノテーションなのです。
この変位指定アノテーションは、型の許容性を明示します。
型の方向性 と考えるとわかりやすいかも。
- 汎化の方向性で許容するのか
- 特化の方向性で許容するか
更に方向性を持たない場合の3つがあります。用語があるので纏めると
- 共変
- 型の特化を許容する。型パラメータで
+Aとする。
- 型の特化を許容する。型パラメータで
- 反変
- 型の汎化を許容する。型パラメータで
-Aとする。
- 型の汎化を許容する。型パラメータで
- 非変
- その型のみを許容する。型パラメータで
Aとする。
- その型のみを許容する。型パラメータで
それぞれ見てみましょう。
ここでは、型境界で登場したAnimal、Person、Studentクラスに再登場していもらいます。
共変
- 型の特化を許容する
サブクラスを許容します。制限を付けたいときは、下限境界が使えるぞ。
ソースを見てみよう!
scala> class School[+T]
defined class School
scala> def receive(args: School[Person]) = println("success!!!")
receive: (args: School[Person])Unit
ここでは共変なSchoolクラスを定義しています。
そしてSchool[Person]を受け取れる関数receiveを定義した。
ではこの関数を使ってみよう!
scala> receive(new School[Person])
success!!!
receiveの引数の型そのモノを渡しているのでOKですね。
scala> receive(new School[Student])
success!!!
共変なため、School[Person]のサブクラスのSchool[Student]もOKです。
scala> receive(new School[Animal])
<console>:12: error: type mismatch;
found : School[Animal]
required: School[Person]
receive(new School[Animal])
^
School[Person]のスーパークラスのSchool[Animal]の場合は、コンパイルエラーになりました。
反変
- 型の汎化を許容する
スーパクラスを許容します。制限を付けたいときは、上限境界が使えるぞ。
同様に見てみます。
scala> class School[-T]
defined class School
scala> def receive(args: School[Person]) = println("success!!!")
receive: (args: School[Person])Unit
scala> receive(new School[Person])
success!!!
scala> receive(new School[Animal])
success!!!
scala> receive(new School[Student])
<console>:13: error: type mismatch;
found : School[Student]
required: School[Person]
receive(new School[Student])
^
今度はSchool[Student]の場合に、コンパイルエラーが発生しました。
非変
- その型のみを許容する
こちらも同様に。
scala> class School[T]
defined class School
scala> def receive(args: School[Person]) = println("success!!!")
receive: (args: School[Person])Unit
scala> receive(new School[Person])
success!!!
scala> receive(new School[Animal])
<console>:12: error: type mismatch;
found : School[Animal]
required: School[Person]
Note: Animal >: Person, but class School is invariant in type T.
You may wish to define T as -T instead. (SLS 4.5)
receive(new School[Animal])
^
scala> receive(new School[Student])
<console>:13: error: type mismatch;
found : School[Student]
required: School[Person]
Note: Student <: Person, but class School is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
receive(new School[Student])
^
サブクラス・スーパークラスのどちらも許容しないことが確認できました。
ちなみに __Javaの世界__では、 全て非変 となっています。
ただし、 配列は共変 です。これは知らなかったな。。。
まとめ
今回は書き方もちょっと変えてみたぞ!内部リンクを付けてみたけどどうかな?必要ないって?
型の世界は奥が深いだろ。
でもこの辺がわかってくると、Scaladocを今までよりももっと読めるようになるよ!
ところで応用編に突入してみたけど、今までのノリと変わってないだろ?
今回も
体で感じてくれたかな?