共変と反変とPECS規則と

Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)
羽生田 栄一

インプレスジャパン 2009-08-21
売り上げランキング : 5861

Amazonで詳しく見る
by G-Tools

最近Scala始めました。

で、共変と反変の扱いで混乱してしまった。

共変と反変

Scalaスケーラブルプログラミング』のp364にこんなサンプルがある。

S => T

これは次のように展開されるという。

trait Function1[-S, +T] {
  def apply(x: S): T
}

変異指定アノテーションをつけているので、applyメソッドには引数にSのスーパークラス(反変)、結果値にTのサブクラス(共変)を指定できることになる。

PECS規則

ところで、Effective Java 第2版の項目28にて、PECS規則というものが紹介されている。

曰く、生産者(Producer) = 値を渡してくる側は、extendsであるべき。

void putAll(List<? extends E> list) {
    for(E e in list) {
        innerList.add(e);
    }
}

そして、消費者(Consumer) = 値を受け取る側は、superであるべき。

void getAll(List<? super E> list) {
    for(E e in innerList) {
        list.add(e);
    }
}

こうすることで、柔軟性を最大限確保できる。

ちなみに、PSCEだと無理が出てくる。実際に上記コードのextendsとsuperを入れ替えてみるとわかると思う。

混乱

ここで、もう一度Scalaの例を見てみる。

trait Function1[-S, +T] {
  def apply(x: S): T
}

通常、メソッドは引数から値を受け取って、処理した結果を結果値として返すのだから、引数がProducerで結果値がConsumerだろう。しかし、そう考えるとproducer-super, consumer-extendsになってしまい、PSCE(PECS規則の逆)になってしまう。trait Function1[+S, -T]になるべきなのか?

@kmizuさんからつっこみ頂戴しました。ありがとうございます。

そうだと仮定してみます。すると、次のような代入がOKになりますよね? val f: String => Any = (x => x); val g: Any => String = f;

http://twitter.com/kmizu/status/7300161407

gの型はAny => Stringなのでどんな値も渡せるはずです(g(1)など)。しかし、gと同じインスタンスを指しているfはStringを引数に取るので、fにString以外の値(1とか)が渡されるのはマズい。型安全性が壊れるわけです。

http://twitter.com/kmizu/status/7300260871

ごもっともです。というか考え無しに適当なこと言ってすみません。

いくつかサンプルコードを書いてみると、確かにFunction1[-S, +T]で全てうまくいくのがわかる。しかし、結果的にそうなることが理解できても、その理解を抽象的なところに持って行けていない。

自分なりの理解

S => Tは、実際使われる場合は、次のようになるだろう。

def doSomething(func: S => T) : T =  func(param)

つまり、高階関数で使われるんだろう。

このとき、doSomethingから見てProducerになるのはfuncの結果値である。また、doSomethingから見てConsumerになるのはfuncの引数である。よって、PECS規則に則るならば、funcの結果値がextendsで、funcの引数はsuperでなければならない。これはtrait Function1[-S, +T]と合致する。

つまり、funcから見て引数=Producer, 結果値=Consumerと考えることが間違っていて、funcを使用する高階関数から見て引数=Consumer, 結果値=Producerと考えるべきであるわけだ。

思えば、『Scalaスケーラブルプログラミング』のこのあたりが同じことを言っている気がする。

引数は必要とされるものであるのに対し、結果値は与えられるものなので、これはリスコフ置換原則を満足させる。(p364)

関数主体で見ると、言っていることが逆に見える。引数が与えられて、結果値が必要とされるんじゃないの?と。しかし、その外側の高階関数から見ていると考えれば、さもありなんといった内容だ。

しかし今考えてみると、共変も反変も「型」の話なのだから、それを利用する側が主体になるのは当たり前である。それなのに型の先にある実体にばかり気が向くのが敗因か。