See the Elephant

1992生まれのプログラマが書くエンジニアブログ

scala 型パラメータ 非変、共変、反変について調べた

お仕事でscalaを使うのでscalaに入門

型パラメータと変位指定 · Scala研修テキスト

型パラメータについて

今日は型パラメータについて学んでいる。

動的型付け言語ばっかり書いてきたので、型パラメータについて学ぶのは初。

何の事は無い、クラスを作る時点では型が不明の場合に役に立つ。

例えば、コレクションクラス(配列とか)を作るときに便利らしい。

rubyphpは代入された物をよしなに動的に判断してくれたり、肩にかかわらず突っ込めたりするが、scalaはそうではない。

例えばIntが入ったシーケンスはこんな感じで定義する

scala>  val seqInt = Seq[Int]();
seqInt: Seq[Int] = List()

replで遊んでみた。

// A が型パラメータ
// Aには任意の型が入る

scala> class Cell[A](var value: A) {
     | def put(newValue: A): Unit = {
     | value = newValue
     | }
     | def get(): A = value
     | }
defined class Cell

// Intで宣言する

scala> val cell = new Cell[Int](1)
cell: Cell[Int] = Cell@47ed3f81

scala> cell.put(2)

scala> cell.get()
res1: Int = 2

// Int で宣言したcellはStringを受け付けない

scala> cell.put("2")
<console>:13: error: type mismatch;
 found   : String("2")
 required: Int
       cell.put("2")
                ^

// Stringで宣言してみる

scala> val cell = new Cell[String]("1")
cell: Cell[String] = Cell@60b68ffb

scala> cell.get()
res3: String = 1

scala> cell.put("2")

scala> cell.get()
res5: String = 2

実用的な使われ方では、戻り値で複数の値を返したいときに使われるらしい。

tapleのような複数個の値をひとまとめにしたクラスを作ることが多いそう。

変位指定(variance)

型パラメータ 自体はわかりやすいがこれがとても難しかった。

というか今もあんまり理解できてない。

variance は 2つのオブジェクト(関数も含む) について型の扱いをどうするか を指定するための物である。

型の継承によって扱われ方が異なる。

ちょっと集合論というか、概念の話が多くなる。

variance には 3種類の特性がある。

  • 非変 (invariant)
  • 共変 (covariant)
  • 反変 (contravariant)

非変

これはめっちゃわかりやすい。 非変は、同じ型じゃないと代入を許さないというものだ。

ドワンゴから引用

型パラメータを持ったクラスG、型パラメータAとBがあったとき、A = Bのときにのみ

val : G[A] = G[B]
というような代入が許されるという性質を表します。

scalaは非変がデフォルト。そらそうだよね、って挙動。 (javaは共変がデフォルトらしい)

共変

ドワンゴ曰く、以下とのこと。わ...わからん...。

共変というのは、型パラメータを持ったクラスG、型パラメータAとBがあったとき、A が B を継承しているときにのみ、

val : G[B] = G[A]
というような代入が許される性質を表します。

この記事がわかりやすかったので引用。

qiita.com

共変:型の制限を強める(対象範囲を狭める) → 継承先の派生クラスに変更すること

らしいです。

例えば、クラスHoge、型パラメータA = String、B = Any(全ての型の親クラス)とすると

val hoge: Hoge[Any] = Hoge[String]

になる。実際にこれを宣言するとコンパイルエラーになる(非変だから)。

共変として扱うときは型パラメータの前に + を宣言する。

val hoge: Hoge[+Any] = Hoge[String]

こうやってみてみると、 型の制限を強める っていうのは妥当な表現に見える。

定義域が広いAnyに対して、より定義域の狭いStringを渡す。

これは、本来扱える空間はすっごく広い(Any)けど、実際に扱えるのはある一部(String)の空間だけという風に型の制限を強めるを意味している。

元記事の図がとてもわかりやすいのでお借りする。

https://camo.qiitausercontent.com/8eb17f1a577a359bff9476a2249f99dcad611e3a/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f33323035372f32343562373035302d616662332d366339622d396266362d3532393939373038323362382e706e67

型の定義域を集合の空間と同じように扱うとわかりやすいのかも。

親クラスが全集合で、子クラスが部分集合みたいな感じで。

子クラスになると自ずと表現できる幅や制約がかかるので、型の制約が厳しくなる。

immutableなら安全に共変できるらしい。なんで危険なのか詳しくは知らない。

反変

qiita.com

の言葉を借りると、

反変:型の制限を弱める(対象範囲を広げる) → 継承元の基底クラスに変更すること

https://camo.qiitausercontent.com/8eb17f1a577a359bff9476a2249f99dcad611e3a/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f33323035372f32343562373035302d616662332d366339622d396266362d3532393939373038323362382e706e67

共変は型の制約を強める働きをした。反変は型の制約を弱めるらしい。

ドワンゴから抜粋。

共変とちょうど対になる性質である反変です。簡単に定義を示しましょう。反変というのは、型パラメータを持ったクラスG、型パラメータAとBがあったとき、A が B を継承しているときにのみ、

val : G[A] = G[B]
というような代入が許される性質を表します。

んーなんだかわからない。

実際のコードをみてみよう。

ちなみにコードリードの前に細く。

=> は関数定義の意味があるらしい。

つまり、argType => returnType と読める。jsでもよくみるよね。

scalaは関数もObjectなのでx1に関数定義する感じになる。

val x1: String => AnyRef = AnyRef => AnyRef型の値

読みづら...ということで補足する。

ここは個人的な考えなのだが、scalaのsyntaxは次のように読み替えてもいいと思う

val 変数名 期待する引数/戻り値のインタフェース = 内部実装(引数/戻り値)

こう読むと、以下のように読むことができる。

// x1はStringを受け取ってAnyRefを返す関数を期待している
// 内部実装はAnyRefを受け取ってAnyRefを返す関数

val x1: String => AnyRef = AnyRef => AnyRef型の値

こうやってみてみると、以下のように考えられた。

1. x1はStringを受け取ってAnyRefを返せる関数
2. 代入された関数はAnyRefを受け取ってAnyRefを返す
3. 引数として取れる型が狭いだけで、関数内ではAnyRef相当の定義域で扱える。
4. つまり, x1が期待するインタフェースの方が型の定義域が厳しく、内部実装が扱える型の定義域が広い

入り口がめっちゃ狭いだけで、実装上扱える型空間が広いときに反変を使えば良さそうである。

AnyRefは全ての型の親クラスなのでこう書き換えることもできる。

val x1: Int => AnyRef = AnyRef => AnyRef型の値

つまり、型の制約を弱めていることになるらしい。

scalaでは反変をこう定義する。

class ClassName[-A]

なんとなく雰囲気を理解したので今日はここまで。

型パラメータの境界 なんて考え方もあるらしいが、難しかったのでまた明日やる。

では。