読者です 読者をやめる 読者になる 読者になる

Scalaの変位指定をすると、何が嬉しいのか。反変編

こんにちは、ヘルステックチームの近藤です。

今までScalaのコードを読んでて「これってどういうときに使って、何が嬉しいんだろう?」と思ってきたもののひとつに、変位指定があります。言わんとすることは分かるのですが、いつどのときに使えば良いのか自分の中でなかなか落としこめず、分かりそうで分からないというもやもやした感じがずっとありました。特に反変。共変はまあそうだよなあ、と思うのですが、反変は脳が理解を拒む感じです。

しかしですねー、先日ついに分かったんですよ。ということで嬉しさのあまり筆を取りました。

なお反変編と銘打ちましたが、共変編は未定です。

反変についてのおさらい

変位に関してはWikipediaにも載っています。

共変性と反変性 (計算機科学) - Wikipedia

ここで反変(contravariant)は:

狭い型(例:float)から広い型(例:double)へ変換すること

と説明されています。たしかにその通りです。

もう少し具体的にしてみましょう。以下のような継承関係の型があるとします。

// 何かしらの名前を表す型。nameプロパティを持つ
trait Name {
  val name: String
}

// 人物の名前を表す型。givenNameプロパティ、familyNameプロパティと、Nameから継承されたnameプロパティを持つ
trait PersonName extends Name {
  val givenName: String
  val familyName: String
}

この場合、継承関係はName -> PersonNameとなります。難しい話ではないと思います。

そして反変を用いたコードです。反変の指定は[-A]と、マイナスを型パラメーターの先頭に付与します。

class Container[-A]()

これでどうなるかと言うと:

val personNameContainer: Container[PersonName] = new Container[Name]()

この代入ができるようになります。

分かりにくい点とはどこか

(個人的に)反変の理解を妨げているのは:

val personNameContainer: Container[PersonName] = new Container[Name]()

というコードです。よく例として取り上げられていると思いますが、これが出来て何が嬉しいのかよく分からないからです。

これが出来て何が嬉しいのかが分からないと、反変はどういうときに使えば良いのかが分からず、「変位指定って何なんだ……」みたいになります。なってました。

実際に反変を使ったケースを説明

「変位指定って何なんだ……」とは思いつつも、それがないと書けないコードがあることも事実。私も先日そのケースにぶち当たり、試行錯誤してたら出来たコードがあるので、そのときのことを順を追って説明します。

そのときの私は任意の型の値をひとつ受け取り、その値をバリデーションして結果を返すクラスを作ろうとしていました。まずはトレイトを定義します。

trait Validator[A] {
  // 型パラメーターAの値を受け取り、バリデーションした結果、不正ならエラーメッセージをSome(errorMessage)として返す。正常ならNoneを返す
  def validate(value: A): Option[String]
}

そして実際にどうバリデーションを行うかを記述した各バリデーターを実装しました。

object NameCannotBeBlank extends Validator[Name] {
  def validate(value: Name) =
    if (value.name.isEmpty) Some("name cannot be blank") else None
}

case class NameMustBeLessThanOrEqualTo(characters: Int) extends Validator[Name] {
  def validate(value: Name) =
    if (value.name.length > characters) Some(s"name must be less than or equal to $characters characters") else None
}

object FamilyNameMustBeKondo extends Validator[PersonName] {
  def validate(value: PersonName) =
    if (value.familyName != "Kondo") Some("family name must be Kondo") else None
}

次に私はこんなことを考えました。「各バリデーターを組み合わせて大きなバリデーターを組み上げたい」と。つまりバリデーター同士の合成です。以下のように、Validatorトレイトに&&メソッドを追加しました。

trait Validator[A] {
  def validate(value: A): Option[String]

  // 自身と引数に与えられたバリデーターを合成する。評価は短絡的(validator1の結果が不正なら、validator2のバリデーションを実行しない)
  def &&(other: Validator[A]): Validator[A] = {
    val validator1 = this
    val validator2 = other

    new Validator[A] {
      def validate(value: A) =
        validator1.validate(value) orElse validator2.validate(value)
    }
  }
}

これで:

NameCannotBeBlank && NameMustBeLessThanOrEqualTo(10)

とすると、「空っぽではなく、10文字以下の名前であるかを確かめるバリデーター」を作り上げられます。良いですね。

しかし、「空っぽではなく、名字がKondoであるかを確かめるバリデーター」を作りたいと思ったとき、今までのコードではそれが出来ません。なぜならNameCannotBeBlankValidator[Name]であるので、&&メソッドが求める値もValidator[Name]か、それを継承したものでなければいけません。よってValidator[PersonName]を継承したFamilyNameMustBeKondoは与えられないわけです。

これは型パラメーターAが非変のために起こる問題です。ということで変位指定をしましょう。

今回は共変と反変、どちらを指定すれば良いのでしょうか。「空っぽではなく、名字がKondoであるかを確かめるバリデーター」がどの型であれば良いかを考えてみます。

  • Validator[Name]型の場合: 与えられた値をNameとして扱うとfamilyNameが取得できなくてダメな気がする
  • Validator[PersonName]型の場合: familyNameも取得できるし、PersonNameNameを継承しているため、nameも問題なく取得できる

答えが出ました。Validator[Name]Validator[PersonName]&&メソッドで合成したら、Validator[PersonName]が出来上がれば良さそうです。子の型に変換したいわけですから、今回は反変です。以下のようにValidator[A]型を書き換えます。

trait Validator[-A] {
  def validate(value: A): Option[String]

  def &&[B <: A](other: Validator[B]): Validator[B] = new Validator[B] {
    // 省略
  }
}

型パラメーターAに反変を指定しました。そして新たに型パラメーターBを&&メソッドの利用時に取るようにしました。「型パラメーターBは必要なの?型パラメーターAじゃダメなの?」という疑問がありそうですが、結論から言うと型パラメーターAではダメです。型パラメーターAを利用してしまうと、NameCannotBeBlank && FamilyNameMustBeKondoとした場合、与えられたFamilyNameMustBeKondoValidator[PersonName]にはならなさそうと、なんとなく思いませんか?よって型パラメーターAではなく、与えられた値の型に応じなければいけないため、新たに型パラメーターBを取る必要があります。

それと型パラメーターBには上限境界を指定しています。この指定の理由は後ほど説明します。

これでNameCannotBeBlank && FamilyNameMustBeKondoとした場合、Validator[PersonName]なバリデーターが出来るようになりました。

どのように動くのか

『「Validator[PersonName]バリデーターが出来るようになりました」じゃあないんだよ」と言われそうです。実際私も随分投げ遣りだと思います。正直さきほどのコードは「なんかいじくってたら出来ていた」という代物で、「どうしてこれで動くのか」が分かりません。これが変位指定の難しいところかなーと思います。

そこで私はこれで動く理由、原理、理屈をひたすら考えました。そしてついに腹に落ちたのです。

NameCannotBeBlank && FamilyNameMustBeKondoの動き

「分からないときは具体的な例を出せ」が私のスタンスです。よって実際にどう動くのか、具体的な型を当てはめて考えてみました。まずはNameCannotBeBlank && FamilyNameMustBeKondoとしたときです。

この場合、型パラメーターAはNameとなります。NameCannotBeBlankValidator[Name]ですからね。そして型パラメーターBはPersonNameとなります。よって&&メソッドの戻り値はValidator[PersonName]になります。定義ではValidator[B]になっていますからね。上限境界の方はどうでしょうか。型パラメーターB = PersonNameは、型パラメーターA = Nameを継承しているため、こちらも問題ないですね。

あれ、すごく簡単ですね。

FamilyNameMustBeKondo && NameCannotBeBlankの動き

ではレシーバーと引数を入れ替えたケースも考えてみましょう。つまりFamilyNameMustBeKondo && NameCannotBeBlankとする、ということです。&&メソッドは決して交換可能ではありませんが、いずれのケースにせよ、Validator[PersonName]を返す必要があるため、わざと入れ替えた場合も確かめてみます。

この場合、型パラメーターAはPersonNameとなります。そして型パラメーターBはNameとなります。しかし、これでは上限境界に違反しています。そこで反変指定が効いてきます!Validatorの型パラメーターAは反変が指定されているので、型パラメーターB = Nameは、Nameを継承した型のいずれかに変化できるのです。いずれかに変化できるけど、さてどの型に変化すれば良いのでしょうか?B <: Aという上限境界を満たしてNameを継承した型…… PersonNameしかありえないと思いませんか?

つまり、FamilyNameMustBeKondo && NameCannotBeBlankとした場合、NameCannotBeBlankValidator[PersonName]に変化しているのです!その変化を許すための反変指定なのです。代入のときは「何が嬉しいの?」と思いましたが、引数として与えるときに変化する必要があると言われたら「なるほど!」となりました。たしかに変数も仮引数も似たようなものですからね(ちょっと雑な説明)。

そして変化の道筋を作るために上限境界があります。これが上限境界を指定する理由です(と私は理解しています)。

結果FamilyNameMustBeKondo && NameCannotBeBlankValidator[PersonName]同士を組み合わせていることになるので、Validator[PersonName]になって当然です。

最後に

いかがでしたでしょうか?私のように「変位指定分っかんねー」という方がいるかは分かりませんが、もしあなたがこの記事を読む前にはそうであって、今は理解が進んだとあればとっても嬉しいです。この記事が役に立ったとかそういうことではなく、「ねー変位指定ってこういうことなんだよ!すっごいね!」という感情を共有できそうなので。何かが分かるってすごい楽しいことだと思います。この記事を読んでも分からないという方は是非コメントなどいただければと思います。

にしてもこれを得心したときは本当心躍りました。共変については今回触れていませんが、今後同様に得心することもありそうなので、そのときはまた記事にしようと思います。それでは。