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"]