おはぽえ〜〜
先日、私の通っている女子高の同級生との会話で、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つにまとめたい気がしてきます。
Burnable
もIncombustible
もGarbage
のサブタイプなので、以下のように書ける気もします。
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]
これは、Burnable
はGarbage
のサブタイプですが、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]
を代入することが可能になります。
ちなみにjavaのArrayListは共変じゃないので、このようなことはできません。
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の変位指定についてまとめてみました。
反変についての理解はもうちょっと深めたいですね〜また何かわかったら加筆していきたいと思います