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