170
141

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

第21章:Scalaの型パラメータ

Posted at

皆、元気してた?久々の投稿をさせてもらうね。
今回から 応用編 に突入するよ!

今回のテーマは、 型パラメータ だ!
こんな感じで語っていくからね。

型パラメータとは

型パラメータってなんだろ? 意味分かんないぞ!
と思っていたら、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.ttの値を返すメソッドです。

使う側

使う側はシンプルだよ!

  def main(args: Array[String]): Unit = {
    val stringTypeParam = new TypeParam[String]("test")
    println(stringTypeParam.get)

    val intTypeParam = new TypeParam[Int](1)
    println(intTypeParam.get)
  }

ここではStringIntの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で、以下のどちらかの制約になるんだ。

  • ABのサブ型
  • ABと同一型

上限境界を定義した新しいクラスを定義してみる。

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で、以下のどちらかの制約になるんだ。

  • ABのスーパ型
  • ABと同一型

今度は、下限境界を定義した新しいクラスを定義してみる。

このクラス達を使ってみてみよう!

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の場合、StringObjectのサブクラスだからといって、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とする。

それぞれ見てみましょう。
ここでは、型境界で登場したAnimalPersonStudentクラスに再登場していもらいます。

共変

  • 型の特化を許容する

サブクラスを許容します。制限を付けたいときは、下限境界が使えるぞ。

ソースを見てみよう!

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を今までよりももっと読めるようになるよ!

ところで応用編に突入してみたけど、今までのノリと変わってないだろ?

今回も
体で感じてくれたかな?

170
141
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
170
141

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?