JKになりたい

何か書きたいことを書きます。主にWeb方面の技術系記事が多いかも。

変位指定パラメータ「非変・共変・反変」と反変のつかいどころ

おはぽえ〜〜

先日、私の通っている女子高の同級生との会話で、Scalaの変位指定パラメータの話題になりまして。(イマドキのJKはScalaが流行中!)

変位指定パラメータって、どういうものかの解説はたくさんあるんだけど、どういう時に使うか?みたいな話が全然ないよね、という話をしてました(特に反変)
なので、そのあたりの自分の解釈をまとめられたらな〜と思います

非変

型パラメータに変位指定アノテーションを指定しないと、非変になります。


例として、燃えるゴミと燃えないゴミがある世界を考えます。

trait Garbage {
  def name: String
}
trait Burnable extends Garbage
trait Incombustible extends Garbage

で、ゴミ箱の定義があります。

trait Trash[A] {
  def put(a: A): Unit
}

ゴミ箱を作りましょう。 燃えるゴミは燃やして、燃えないゴミは潰します。

implicit val trashForBurnable = new Trash[Burnable] {
  override def put(a: Burnable): Unit = println(s"burn! ${a.name}")
}

implicit val trashForIncombustible = new Trash[Incombustible] {
  override def put(a: Incombustible): Unit = println(s"crush! ${a.name}")
}

ゴミ箱を作ったので、ゴミも作っておきます。 燃えるゴミは紙で、燃えないゴミは缶にしときます。

val paper = new Burnable {
  override def name: String = "paper"
}
val can = new Incombustible {
  override def name: String = "can"
}

最後に、ゴミを渡すとゴミ箱に入れる関数を作ってみます

def throwAway(a: Burnable)(implicit trash: Trash[Burnable]): Unit =
    trash.put(a)

def throwAway(a: Incombustible)(implicit trash: Trash[Incombustible]): Unit =
    trash.put(a)

これで、ゴミを捨てることができるようになりました!やったーー!

throwAway(paper) //burn! paper
throwAway(can) //crush! can

ここまでくると、throwAwayが2つあるので、抽象化して1つにまとめたい気がしてきます。

BurnableIncombustibleGarbageのサブタイプなので、以下のように書ける気もします。

def throwAway(a: Garbage)(implicit trash: Trash[Garbage]): Unit = trash.put(a)

しかし、これはTrash[Garbage]インスタンスが存在しないということで、使えません。

throwAway(paper) //Error! could not find implicit value for parameter trash: Trash[Garbage]

これは、BurnableGarbageのサブタイプですが、Trash[Garbage]Trash[Burnable]のサブタイプではないことを意味しています。

この関係が「非変」の関係です。Scalaでは変位指定パラメータを指定しないと、非変となります。

共変

ScalaのListやOptionの定義には共変の変位指定パラメータが指定されています

trait List[+A]
trait Option[+A]

共変パラメータを指定すると、BがAのサブタイプだったとき、F[B]F[A]のサブタイプとなります。

var garbage = List.empty[Garbage]
val burnables = List(paper, paper, paper)
garbage = burnables

上記のように、Listが共変なのでList[Burnable]List[Garbage]を代入することが可能になります。

ちなみにjavaArrayListは共変じゃないので、このようなことはできません。

var garbage = new java.util.ArrayList[Garbage]()
val burnables =
  new java.util.ArrayList[Burnable](java.util.Arrays.asList(paper, paper, paper))
garbage = burnables //Error! type miss match.


と、いうことで、先程の例に戻ります。

def throwAway(a: Garbage)(implicit trash: Trash[Garbage]): Unit = trash.put(a)

この関数を使うためには、Trash[Garbage]インスタンスが要求されました。
そこで、Trashに共変パラメータを付与し、Trash[Burnable]がサブタイプになるようにしてやればTrash[Garbage]を作らなくても良いのでは?という話になります。
理屈上はそのとおりですが、今度は別の問題が発生します。

trait Trash[+A] {
  def put(a: A): Unit //Error! Covariant type A occurs in contravariant position in type A of value a
}

Covariant type A occurs in contravariant position in type A of value aと怒られてしまいました。
そう、共変の型パラメータをメソッドの引数に使うことはできないんです・・・

なぜ共変のパラメータを引数に使えないの?

まず、Tashに共変パラメータを指定すると以下のような定義が可能になります。

implicit val trashForBurnable: Trash[Garbage] = new Trash[Burnable] {...}

で、以下のような関数も、当然定義ができます。

この定義自体は問題ないようにみえます。

def throwAway(a: Garbage)(implicit trash: Trash[Garbage]): Unit = trash.put(a)


が・・しかし、上記の例だと、この関数にTrash[Garbage]の実態としてサブタイプのTrash[Burnable]が渡ってくるわけです。

にも関わらず、第一引数のa: Garbageには当然、GarbageやサブタイプであるIncombustibleを渡せることになってしまいます。

この時、Trashの実態はTrash[Burnable]なので、Incombustibleのゴミを入れられても、燃えないゴミの処理方法はわからないので困ってしまいます!

・・ということで、引数に共変の型パラメータを指定することはできません。

一方、戻り値の型であることは問題ないので、こちらは共変パラメータを利用可能です。

反変

反変は、BがAのサブタイプだったときにF[A]F[B]のサブタイプになります

関係が逆転するんですね。

つまり、Trashに反変パラメータを指定してやると、Trash[Garbage]Trash[Burnable]のサブタイプになります。

trait Trash[-A] {
  def put(a: A): Unit
}

こうしておくと、以下のような関数があったときに、Trash[Garbage]インスタンスもこの関数に渡すことができるようになります

def throwAway(a: Burnable)(implicit trash: Trash[Burnable]): Unit =
  trash.put(a)

以下はこれまで通り、もちろん問題なく動作します。

implicit val trashForBurnable: Trash[Burnable] = new Trash[Burnable] {
  override def put(a: Burnable): Unit = println(s"burn! ${a.name}")
}
throwAway(paper)

反変指定により、Trash[Garbage]Trash[Burnable]のサブタイプとみなせるようになりましたので、以下が動くようになります。

//  implicit val trashForBurnable: Trash[Burnable] = new Trash[Burnable] {
//    override def put(a: Burnable): Unit = println(s"burn! ${a.name}")
//  }
implicit val trashForAnything: Trash[Garbage] = new Trash[Garbage] {
  override def put(a: Garbage): Unit = println(s"eat! ${a.name}")
}
throwAway(paper) // eat! paper

で、これ何に使うの?

一言でいうと、上記の例ような「燃えるゴミ箱か、何でも入れていいゴミ箱どっちかに入れてね」という気持ちを表現するのに使えます。
もし、これが反変じゃなく共変の形、つまりTrash[Burnable]Trash[Garbage]のサブタイプだった時の世界と比較すると、良さがわかります。
この前提の場合、「燃えるゴミ箱か、何でもいいゴミ箱どっちかに入れてね」という気持ちを表現することができなくなります。

def これは燃えるゴミを捨てる関数(a: Burnable)(implicit trash: Trash[Burnable]): Unit =
  trash.put(a)


上記の関数には、燃えるゴミ用のゴミ箱が渡ってくるかもしれませんし、何でも入れていいゴミ箱が渡ってくるかもしれませんし、燃えないゴミ用のゴミ箱が渡ってくるかもしれません・・つまり、型で気持ちを伝えることができません。
反変を使うと、この気持ちが表現可能になります。

def throwAway(a: Burnable)(implicit trash: Trash[Burnable]): Unit =
  trash.put(a)


この他、よく例に挙げられるのがオブジェクトの文字列表現を得るTraitなどでしょうか。

trait Printable[A] {
  def format(value: A): String
}
def print[A](input: A)(implicit p: Printable[A]): Unit =
  println(p.format(input))


上記のtraitは以下のように使うことができます。

implicit val intPrintable = new Printable[Int] {
  def format(input: Int) = s"number: ${input.toString}"
}
print(1) //number: 1


しかし、AのPrintableが提供されていない場合は、当然、printを使うことができなくなります

print(List(1,2,3)) //Error! no implicit parameter


これでは不便です。特別なフォーマットで表示は不可能でも、汎用的な表現でprintできるようにしたいですね。
そこで、Printableを反変にします。

trait Printable[-A] {
  def format(value: A): String
}


そして、AnyのPrintableを作っておきます。

implicit val anythingPrintable = new Printable[Any] {
  def format(input: Any) = input.toString
}


Scalaでは全てのオブジェクトはAnyのサブタイプなので、これで全ての型でPrintableが使用できるようになります。

print(List(1,2,3)) //List(1, 2, 3)



と、いうわけでScalaの変位指定についてまとめてみました。
反変についての理解はもうちょっと深めたいですね〜また何かわかったら加筆していきたいと思います