Scalaの変位指定をすると、何が嬉しいのか。blog2016-06-12Scalaの変位指定をすると、何が嬉しいのか。反変編こんにちは、ヘルステックチームの近藤です。今までScalaのコードを読んでて「これってどういうときに使って、何が嬉しいんだろう?」と思ってきたもののひとつに、変位指定があります。言わんとすることは分かるのですが、いつどのときに使えば良いのか自分の中でなかなか落としこめず、分かりそうで分からないというもやもやした感じがずっとありました。共変はまあそうだよなあ、と思うのですが、反変は脳が理解を拒む感じです。先日ついに分かったんですよ。いうことで嬉しさのあまり筆を取りました。反変編と銘打ちましたが、共変編は未定です。反変についてのおさらい変位に関してはWikipediaにも載っています。Wikipediaここで反変(contravariant)は:狭い型(例:float)から広い型(例:double)へ変換することと説明されています。たしかにその通りです。具体的にしてみましょう。以下のような継承関係の型があるとします。何かしらの名前を表す型。PersonNameとなります。難しい話ではないと思います。反変を用いたコードです。反変の指定は[-A]と、マイナスを型パラメーターの先頭に付与します。Container[Name]()この代入ができるようになります。Container[Name]()というコードです。例として取り上げられていると思いますが、これが出来て何が嬉しいのかよく分からないからです。これが出来て何が嬉しいのかが分からないと、反変はどういうときに使えば良いのかが分からず、「変位指定って何なんだ......」みたいになります。「変位指定って何なんだ......」とは思いつつも、それがないと書けないコードがあることも事実。私も先日そのケースにぶち当たり、試行錯誤してたら出来たコードがあるので、そのときのことを順を追って説明します。そのときの私は任意の型の値をひとつ受け取り、その値をバリデーションして結果を返すクラスを作ろうとしていました。トレイトを定義します。型パラメーターAの値を受け取り、バリデーションした結果、不正ならエラーメッセージをSome(errorMessage)として返す。Option[String]}そして実際にどうバリデーションを行うかを記述した各バリデーターを実装しました。None}次に私はこんなことを考えました。「各バリデーターを組み合わせて大きなバリデーターを組み上げたい」と。バリデーター同士の合成です。以下のように、Validatorトレイトに&&メソッドを追加しました。自身と引数に与えられたバリデーターを合成する。NameMustBeLessThanOrEqualTo(10)とすると、「空っぽではなく、10文字以下の名前であるかを確かめるバリデーター」を作り上げられます。「空っぽではなく、名字がKondoであるかを確かめるバリデーター」を作りたいと思ったとき、今までのコードではそれが出来ません。NameCannotBeBlankはValidator[Name]であるので、&&メソッドが求める値もValidator[Name]か、それを継承したものでなければいけません。よってValidator[PersonName]を継承したFamilyNameMustBeKondoは与えられないわけです。これは型パラメーターAが非変のために起こる問題です。いうことで変位指定をしましょう。今回は共変と反変、どちらを指定すれば良いのでしょうか。「空っぽではなく、名字がKondoであるかを確かめるバリデーター」がどの型であれば良いかを考えてみます。familyNameも取得できるし、PersonNameはNameを継承しているため、nameも問題なく取得できる答えが出ました。Validator[Name]とValidator[PersonName]を&&メソッドで合成したら、Validator[PersonName]が出来上がれば良さそうです。子の型に変換したいわけですから、今回は反変です。以下のようにValidator[A]型を書き換えます。}}型パラメーターAに反変を指定しました。新たに型パラメーターBを&&メソッドの利用時に取るようにしました。「型パラメーターBは必要なの?型パラメーターAじゃダメなの?」という疑問がありそうですが、結論から言うと型パラメーターAではダメです。FamilyNameMustBeKondoとした場合、与えられたFamilyNameMustBeKondoのValidator[PersonName]にはならなさそうと、なんとなく思いませんか?よって型パラメーターAではなく、与えられた値の型に応じなければいけないため、新たに型パラメーターBを取る必要があります。それと型パラメーターBには上限境界を指定しています。この指定の理由は後ほど説明します。FamilyNameMustBeKondoとした場合、Validator[PersonName]なバリデーターが出来るようになりました。どのように動くのか『「Validator[PersonName]バリデーターが出来るようになりました」じゃあないんだよ」と言われそうです。実際私も随分投げ遣りだと思います。さきほどのコードは「なんかいじくってたら出来ていた」という代物で、「どうしてこれで動くのか」が分かりません。これが変位指定の難しいところかなーと思います。そこで私はこれで動く理由、原理、理屈をひたすら考えました。FamilyNameMustBeKondoの動き「分からないときは具体的な例を出せ」が私のスタンスです。よって実際にどう動くのか、具体的な型を当てはめて考えてみました。FamilyNameMustBeKondoとしたときです。この場合、型パラメーターAはNameとなります。NameCannotBeBlankはValidator[Name]ですからね。型パラメーターBはPersonNameとなります。よって&&メソッドの戻り値はValidator[PersonName]になります。定義ではValidator[B]になっていますからね。上限境界の方はどうでしょうか。Nameを継承しているため、こちらも問題ないですね。あれ、すごく簡単ですね。NameCannotBeBlankの動きではレシーバーと引数を入れ替えたケースも考えてみましょう。NameCannotBeBlankとする、ということです。&&メソッドは決して交換可能ではありませんが、いずれのケースにせよ、Validator[PersonName]を返す必要があるため、わざと入れ替えた場合も確かめてみます。この場合、型パラメーターAはPersonNameとなります。型パラメーターBはNameとなります。これでは上限境界に違反しています。Nameは、Nameを継承した型のいずれかに変化できるのです。NameCannotBeBlankとした場合、NameCannotBeBlankはValidator[PersonName]に変化しているのです!その変化を許すための反変指定なのです。代入のときは「何が嬉しいの?」と思いましたが、引数として与えるときに変化する必要があると言われたら「なるほど!」となりました。たしかに変数も仮引数も似たようなものですからね(ちょっと雑な説明)。変化の道筋を作るために上限境界があります。これが上限境界を指定する理由です(と私は理解しています)。NameCannotBeBlankはValidator[PersonName]同士を組み合わせていることになるので、Validator[PersonName]になって当然です。最後にいかがでしたでしょうか?私のように「変位指定分っかんねー」という方がいるかは分かりませんが、もしあなたがこの記事を読む前にはそうであって、今は理解が進んだとあればとっても嬉しいです。この記事が役に立ったとかそういうことではなく、「ねー変位指定ってこういうことなんだよ!すっごいね!」という感情を共有できそうなので。何かが分かるってすごい楽しいことだと思います。この記事を読んでも分からないという方は是非コメントなどいただければと思います。してもこれを得心したときは本当心躍りました。共変については今回触れていませんが、今後同様に得心することもありそうなので、そのときはまた記事にしようと思います。13:54Scalaの変位指定をすると、何が嬉しいのか。