2007-06-19
■ Xtalの多値3  
 
			http://www.rubyist.net/~matz/20070611.html#p05
Xtalの多値について。Xtalでは多値と配列では(効率以外は)同じ意味を持つようにしたのだそうだ。「じゃあ多値要らないじゃん」という印象も持つが、純粋に最適化手法としてとらえれば良いのだろう、
はい、多値は効率の問題が無ければ要りません。
ただ、多重代入で
a,b = [1,2,3]
が
a = 1
b = [2,3]
になるのは正直いかがなものかと思う。単純に切り落として
a = 1
b = 2
にした方が良いのではないかな。
a,b = [1,[2,3] ]
と区別が付かないし。たぶん、切り落とすことで情報を失うことを嫌ったのだろう。気持ちはわかる。
いえ、情報を切り落とすことは特に問題に思っておりません。実際昔の実装では切り捨てていました。
このような仕様となった理由は「ある観点で見た時の統一性を持たせるため」「ある仕様の絡みから区別してはいけないため」です。
ある観点
ある観点とは、「左辺の数が一つのときの挙動、複数のときの挙動に統一性があるか」です。
この観点で見たとき、もし「a,b = [1,2,3] が a=1, b=2」となるのであれば、「a = [1,2,3] は a=1」であるべきです。左辺が一つのときは纏められ、二つ以上のときは切り捨てる、と挙動が異なっていますから。
Rubyもこの観点で見たとき、やはり統一性がありません。
Xtalは「右辺の値の数が左辺より多い場合は、余った右辺の値を、左辺最後の変数に配列として纏めて代入する」と定義し、この観点での統一性を持たせているわけです。
もちろん、Xtalのやり方も、他の観点で見た場合に統一性が欠けています。しかし、イテレータ周りがこの観点で見た仕様だと割と上手く行ったため、これを重視しています。
「a,b = [1,2,3]」と「a,b = [1, [2, 3] ]の区別
Xtalでは、次のように「多値の追加」が出来ます。
foo: fun(){ // 5と6、多値を返す return 5, 6; } bar: fun(){ // 1と2と、fooで返す全ての値を返す return 1, 2, foo(); } a, b, c, d: bar(); [a, b, c, d].p; //=> [1, 2, 5, 6]
この時、「多値と配列は同じ」という前提があるため、fooの実装が「fun(){ return [5, 6]; }」であっても、同じように動作しなければなりません。よって、[1, 2, 5, 6]と[1, 2, [5, 6] ]は同じである必要があり、多重代入の際「区別してはならない」のです。
この「区別できない」特性のため、「意図していない、配列と多値の混同が起きる場合」があります。
// この関数の戻り値は「多値のつもりではない配列」とする。 foo: fun(){ return [10, 20]; } bar: fun(){ // 5とfoo()の戻り値を返す。 return 5, foo(); } a, b, c: bar(); [a, b, c].p;
これで、[5, 10, 20] と出力されますが、本当は [5, [10, 20], null]となることを望んでいた。のような場合ですね。
これは、barが三つの多値を返すかのように受取ったのが原因です。正確に「a,b = bar()」とすればa=5, b=[10, 20]」と代入されます。
関数の仕様を決める人は「関数(またはイテレータが)がいくつ多値を返すかを正確に予測できる仕様」にする必要があります。
それさえされれば、特に問題は無いと考えています。
多値の追加の具体例
多値の追加がちゃんと活用されている例として、Iterator::with_indexがあります。
// with_indexの定義は次のような感じでされている /* Iterator::with_index: method(index: 0){ return fiber{ i: index; // 自身をイテレートする。 // ブロックパラメータ部分を省略すると、 // this{ |it| と書いたのと同じ意味となる。 // ブロックパラメータは多重代入と同じ挙動で代入されるため、 // もしブロックパラメータが多値の場合、配列に変換される。 this{ // インデックスと自身のイテレートで取得した要素をyieldする。 yield i, it; i++; } } } */ // 配列のeachは単値をブロックパラメータとして渡す [1,2].each.with_index{ |index, value| %f(index=%s, value=%s)(index, value).p; } //=> index=0, value=1 //=> index=1, value=2 // 連想配列のeachはキーと値の多値をブロックパラメータとして渡す // 連想配列.each.with_indexは、ブロックパラメータとして、 // 本来「index, [key, value]」を渡してくるが、 // ブロックパラメータを三つにすると、多重代入のルールにより // それぞれに値が代入される。 ["a":"ka", "i":"ki"].each.with_index{ |index, key, value| %f(index=%s, key=%s, value=%s)(index, key, value).p; } //=> index=0, key="a", value="ka" //=> index=1, key="i", value="ki" // ブロックパラメータを二つにすると、当然keyvalueには配列で代入される。 ["a":"ka", "i":"ki"].each.with_index{ |index, keyvalue| %f(index=%s, keyvalue=%s)(index, keyvalue).p; } //=> index=0, keyvalue=["a","ka"] //=> index=1, keyvalue=["i","ki"]
■ Xtalの昔の多値の説明と、何故それを止めたのかの理由  
 
			Xtalは昔は「多値と配列は違うもの。余った多値は完全に切り捨てる」という実装で、多値を配列に纏める記法 (a,*b = 1,2,3)」、「配列を多値に展開する記法 (a,b = ary.values)」が存在していました。
つまり、「a,b = 1,2,3」は「a=1, b=2」であり、「a = 1,2,3」は「a=1」、そして「*a = 1,2,3」は「a=[1,2,3]」でした。
実に統一性がある、と、一時は満足したのですが、実装した後冷静にこれで何が嬉しいのか考えてみました。これで嬉しい場面は「よく使う値と、ほとんど使わない値を同時に返す関数がある。その関数を使うときにほとんど使わない二つ目以降の値を気にしなくてもいい」ぐらいしかありません。
はて、そんなの「「よく使う値だけを返す関数」と「よく使う値とほとんど使わない値を多値で返す関数」と別々の関数として用意すれば利便性は同じではないのか」、と思いました。後者で前者は簡単に実装出来ます。
それより「a = foo()」で fooが多値を返そうが単値を返そうが気にせずに受取れて、それを「return a」または「yield a」で全部返せた方が便利ではないかと思いました。*2
もう一つ、仮想マシンの実装が簡潔になることもありました。他の仮想マシン上*3に載せる時も、こっちの仕様の方がが楽で効率も良くなるんではないか、と予測し、この仕様を止め、今の仕様に変更しました。