Scalaの変位指定をすると、何が嬉しいのか。反変編
こんにちは、ヘルステックチームの近藤です。
今までScalaのコードを読んでて「これってどういうときに使って、何が嬉しいんだろう?」と思ってきたもののひとつに、変位指定があります。言わんとすることは分かるのですが、いつどのときに使えば良いのか自分の中でなかなか落としこめず、分かりそうで分からないというもやもやした感じがずっとありました。特に反変。共変はまあそうだよなあ、と思うのですが、反変は脳が理解を拒む感じです。
しかしですねー、先日ついに分かったんですよ。ということで嬉しさのあまり筆を取りました。
なお反変編と銘打ちましたが、共変編は未定です。
反変についてのおさらい
変位に関しては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であるかを確かめるバリデーター」を作りたいと思ったとき、今までのコードではそれが出来ません。なぜならNameCannotBeBlank
はValidator[Name]
であるので、&&
メソッドが求める値もValidator[Name]
か、それを継承したものでなければいけません。よってValidator[PersonName]
を継承したFamilyNameMustBeKondo
は与えられないわけです。
これは型パラメーターAが非変のために起こる問題です。ということで変位指定をしましょう。
今回は共変と反変、どちらを指定すれば良いのでしょうか。「空っぽではなく、名字がKondoであるかを確かめるバリデーター」がどの型であれば良いかを考えてみます。
Validator[Name]
型の場合: 与えられた値をName
として扱うとfamilyName
が取得できなくてダメな気がするValidator[PersonName]
型の場合:familyName
も取得できるし、PersonName
はName
を継承しているため、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
とした場合、与えられたFamilyNameMustBeKondo
のValidator[PersonName]
にはならなさそうと、なんとなく思いませんか?よって型パラメーターAではなく、与えられた値の型に応じなければいけないため、新たに型パラメーターBを取る必要があります。
それと型パラメーターBには上限境界を指定しています。この指定の理由は後ほど説明します。
これでNameCannotBeBlank && FamilyNameMustBeKondo
とした場合、Validator[PersonName]
なバリデーターが出来るようになりました。
どのように動くのか
『「Validator[PersonName]
バリデーターが出来るようになりました」じゃあないんだよ」と言われそうです。実際私も随分投げ遣りだと思います。正直さきほどのコードは「なんかいじくってたら出来ていた」という代物で、「どうしてこれで動くのか」が分かりません。これが変位指定の難しいところかなーと思います。
そこで私はこれで動く理由、原理、理屈をひたすら考えました。そしてついに腹に落ちたのです。
NameCannotBeBlank && FamilyNameMustBeKondo
の動き
「分からないときは具体的な例を出せ」が私のスタンスです。よって実際にどう動くのか、具体的な型を当てはめて考えてみました。まずはNameCannotBeBlank && FamilyNameMustBeKondo
としたときです。
この場合、型パラメーターAはName
となります。NameCannotBeBlank
はValidator[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
とした場合、NameCannotBeBlank
はValidator[PersonName]
に変化しているのです!その変化を許すための反変指定なのです。代入のときは「何が嬉しいの?」と思いましたが、引数として与えるときに変化する必要があると言われたら「なるほど!」となりました。たしかに変数も仮引数も似たようなものですからね(ちょっと雑な説明)。
そして変化の道筋を作るために上限境界があります。これが上限境界を指定する理由です(と私は理解しています)。
結果FamilyNameMustBeKondo && NameCannotBeBlank
はValidator[PersonName]
同士を組み合わせていることになるので、Validator[PersonName]
になって当然です。
最後に
いかがでしたでしょうか?私のように「変位指定分っかんねー」という方がいるかは分かりませんが、もしあなたがこの記事を読む前にはそうであって、今は理解が進んだとあればとっても嬉しいです。この記事が役に立ったとかそういうことではなく、「ねー変位指定ってこういうことなんだよ!すっごいね!」という感情を共有できそうなので。何かが分かるってすごい楽しいことだと思います。この記事を読んでも分からないという方は是非コメントなどいただければと思います。
にしてもこれを得心したときは本当心躍りました。共変については今回触れていませんが、今後同様に得心することもありそうなので、そのときはまた記事にしようと思います。それでは。