型付けについてのまつもとさんとみずしまさんのやりとり

動的型付け言語と静的型付け言語の違いに関する言語仕様について
24
Yukihiro Matzmotto @yukihiro_matz

@kmizu 動的言語では議論の余地なく悪いスタイルです。静的型言語では型チェックが効くのでそこまでではないかもしれません。個人的には賛成しませんが。でも、動的言語との比較という文脈に使うのはフェアじゃないですよね。

2010-05-08 19:22:16
Kota Mizushima (on a diet) @kmizu

@yukihiro_matz それは順序が逆で、静的型付けが無い「から」悪いスタイルになってるのでは?もちろん、静的型付け言語でも悪いスタイルであるかもしれませんが。

2010-05-08 20:12:36
Kota Mizushima (on a diet) @kmizu

@yukihiro_matz それは順序が逆で、静的型付けが無い「から」悪いスタイルになってるのでは?もちろん、静的型付け言語でも悪いスタイルであるかもしれませんが。

2010-05-08 20:12:36
Kota Mizushima (on a diet) @kmizu

動的言語では議論の余地なく悪いスタイルである、という点についてちょっと考えてみたのだけど、これはLiskov則を破っている、という点に尽きるのかな。たとえば、Rubyで、実際の中身はHashだけどユーザはEnumerableということしかわからないという場合を考える。

2010-05-08 20:25:55
Kota Mizushima (on a diet) @kmizu

このとき、h.map{|k, v| [v, k]}とかしたときに、mapがHashを返すようになっている場合、(キーの重複によってエントリが削除されることで)、いつの間にかEnumerableで列挙できる要素数が減っているということが考えられる。

2010-05-08 20:28:35
Kota Mizushima (on a diet) @kmizu

Enumerableのユーザはmapしたら要素数が増減するなんて事は普通は予期しないはずで、これは明らかに契約違反。よって、このようなHash#mapがHashを返し得るようなのは悪い仕様だと言えそう。

2010-05-08 20:29:56
Kota Mizushima (on a diet) @kmizu

一方、Scalaの場合だけど、Map(RubyのHash相当)をRubyにおけるEnumerableに相当するTraversable型にアップキャストして、Traversableに対してmapを呼び出した場合、

2010-05-08 20:33:37
Kota Mizushima (on a diet) @kmizu

つまり、val t: Traversable[(String, Int)] = Map("A" -> 2, "B" -> 2, "C" -> 3); t.map{ case (k, v) => (v, k) } とした場合、

2010-05-08 20:34:45
Kota Mizushima (on a diet) @kmizu

Map(2 -> "A", 3 -> "C")みたいに勝手に要素数が減って大弱り、なんてことはなく、ちゃんとList((2, "A"), (2, "B"), (3, "C"))](静的な型はTraversable)が返ってくる。

2010-05-08 20:36:24
Kota Mizushima (on a diet) @kmizu

Map#mapで返すべきオブジェクトの型を推論するに当たって、レシーバの「静的な」型も考慮に入れて推論されるので、これでうまく行くようになっている。MapをそのスーパータイプであるTraversableとして扱って問題無いので、この点ではLiskov則は破っていない、と言える。

2010-05-08 20:39:38
Kota Mizushima (on a diet) @kmizu

というわけで、仮にLiskov則を基準とした場合、Scala 2.8のMap#mapの仕様は別に悪いスタイルではない、という事が言えるのではないか。それ以外の基準についても検証する必要があるけど、たとえばどういう基準があるだろうか?

2010-05-08 20:41:17
Yukihiro Matzmotto @yukihiro_matz

@kmizu 同じ字面で違うものが返るのはLSPに反するでしょう。静的型ではコンパイル時に検出できる(から致命的ではない)でしょうけど、だからといって「良く」はならないと思います。

2010-05-08 21:03:07
Kota Mizushima (on a diet) @kmizu

@yukihiro_matz いや、この場合は反しないでしょう。Traversableが要求されている箇所にMapを渡しても、考えた限りでは問題が発生しないようになっています。map.map{ case ... } で関数の型によって異なるオブジェクトが返るのは

2010-05-08 21:07:24
Kota Mizushima (on a diet) @kmizu

@yukihiro_matz 単なるオーバーローディングの範疇であり、LSPは関係ないです。

2010-05-08 21:07:56
Kota Mizushima (on a diet) @kmizu

@mametter なるほど。ちょっとその変で食い違ってたのかもしれません。ありがとうございます。ただ、Liskovはその辺についてはかなり深く考えてますよ。Liskov則の元になった

2010-05-08 21:13:24
Kota Mizushima (on a diet) @kmizu

@mametter 「A behavioral notion of subtyping」という論文では、LSPと一言で片付けられてしまう原則に関するフォーマルな定義を含めた深い議論を見ることができます。

2010-05-08 21:14:24
Kota Mizushima (on a diet) @kmizu

@mametter implicit conversionみたいな怪しげな機能についてはさすがに考察してないですけど、Liskovの論文ではメソッドオーバーライド時のrenamingがあるような場合のサブタイピングについて考察がなされています。

2010-05-08 21:18:22
Kota Mizushima (on a diet) @kmizu

@mametter で、メソッドオーバーライド時のrenamingがあったりすると、スーパータイプのものを「字面上で」サブタイプのものにそのまま置き換えると問題が発生するケースがあるわけで、今回のケースと似た問題だと思います。

2010-05-08 21:20:31
Kota Mizushima (on a diet) @kmizu

てなわけで、自分はLiskov則について言及するときは、おおむねその論文を意識しているので、その辺でズレが生じていた、のかも。

2010-05-08 21:24:30
Kota Mizushima (on a diet) @kmizu

さっきの話、「字面上の」と強調したのにはわけがあって

2010-05-08 21:25:33
Kota Mizushima (on a diet) @kmizu

val m: Map[String, Int] = Map("A" -> 1, "B" -> 3, "C" -> 2); m.map{ case (k, v) => (v, k) }のとき、呼び出しの型は、map[(Int, String), Map[Int, String]]

2010-05-08 21:31:42
Kota Mizushima (on a diet) @kmizu

一方、val t: Traversable[(String, Int)] = m; t.map{ case (k, v) => (v, k) }のとき、型はmap[(Int, String), Traversable[(Int, String)]] で、#scala

2010-05-08 21:33:25
Kota Mizushima (on a diet) @kmizu

つまり、「字面上は」同じでも、この場合、m.map{ ... }とt.map{ ... }は、実はオーバーロードされた別バージョンを呼び出しているのに等しいので、字面上だけの置換可能性を元に議論されちゃたまらんと思うわけです。

2010-05-08 21:35:03
Yukihiro Matzmotto @yukihiro_matz

@kmizu まあ、確かに型情報の違いによって字面上は同じでも別バージョン、というのはわかるんですが、字面が同じってことは人間に与える印象も同じってことなんで、それで違っちゃうのはつらいなあと。同様の理由でメソッドオーバーロードも乱用は危険だと思ってます。

2010-05-08 21:48:31
Kota Mizushima (on a diet) @kmizu

@yukihiro_matz はい。その点に関する懸念は理解できます。ただ、悪いかどうかについては十分議論の余地がある話だと考えている、といったところです。

2010-05-08 21:56:58