ひらめの日常

日常のメモをつらつらと

【Scala】リスコフの置換原則と共変反変

SOLID原則の一つであるリスコフの置換原則と、それに関係の深いScalaにおける共変反変について理解が浅かったのでまとめてみました。まとめてみたものの、いまだにわかりづらいのでまたアップデートします。

リスコフの置換原則

リスコフの置換原則 (LSP) とは、全てのクラスやサブクラスを確実に使用できるような継承階層を作成するための決まり。これに従っていないと、派生クラスを実装する際に、基底クラスやインターフェースの変更が生じる可能性がある。

SがTの派生型であるとすれば、T型のオブジェクトをS型のオブジェクトと置き換えたとしても、プログラムは動作し続けるはずである。

置換原則に従うためのルール

  • 事前条件を派生型で強化することはできない。
    • これはメソッドの引数に関係している。
    • メソッドの引数は反変である必要がある。
  • 事後条件を派生型で緩和することはできない
    • これはメソッドの返り値に関係している。
    • メソッドの返り値は共変である必要がある。
  • 基底型の不変条件は、派生型でも維持されなければならない

Scalaにおける変位指定アノテーション

共変 (covariant)

A extends Bの時のみ

val G[B] = G[A]

という代入が許される性質を表す。

class G[+A]

のように型パラメータの前に + をつけることで共変になる。型パラメータを指定する必要のある派生型を、基底型のインスタンスのように扱うことができる点で、ポリモーフィズムとも関連が深い。

たとえば、Scala では List が List[+A] で定義されている。この性質のおかげで、以下のような代入が可能になる。

scala> :paste
// Entering paste mode (ctrl-D to finish)

abstract class Animal
class Cat extends Animal
class Dog extends Animal

// Exiting paste mode, now interpreting.

defined class Animal
defined class Cat
defined class Dog

scala> val cats = List(new Cat())
cats: List[Cat] = List(Cat@66ee9a20)

scala> val dogs = List(new Dog())
dogs: List[Dog] = List(Dog@4934cf32)

scala> cats ++ dogs
res1: List[Animal] = List(Cat@66ee9a20, Dog@4934cf32)

scala> val animals: List[Animal] = cats
animals: List[Animal] = List(Cat@66ee9a20)

反変 (contravariant)

A extends Bの時のみ

val G[A] = G[B]

という代入が許される性質を表す。

class G[-A]

のように型パラメータの前に - をつけることで反変になる。

たとえば、Scala では Function1 が Function1[-T1, +R] で定義されている。この性質のおかげで、以下のような代入が可能になる。

scala> val x1: String => AnyRef = (x: AnyRef) => x
x1: String => AnyRef = $Lambda$6473/1201460973@326f3807

ここで、StringAnyRef を継承している。AnyRef を扱える関数は当然その派生型である String も扱うことができるということ。(しかし現実的に関数の引数以外で反変を意識することがあるのだろうかと思っている。)

参考