From: Tanaka Akira Date: 2009-07-15T20:31:35+09:00 Subject: [ruby-dev:38795] Re: 多重代入やメソッド引数の展開でto_aが呼ばれます In article <704d5db90907141754p285e6e51xdd3208b27d556906@mail.gmail.com>, Shugo Maeda writes: > 1.9では、他にも*x = [1]とかx = *[1]とかの挙動が1.6の時に戻っているようですが、 > このあたりのまとまった議論ってありましたっけ。 > ブロックパラメータと多重代入が明確に分離されたことで、多重代入の方は > [ruby-dev:13493]の時のまつもとさんの拘りがなくなって、田中さんの主張 > に沿った修正がされたのかなと推測しているのですが。 以下のような文章を書いたことがあります。 | 2007-07-04 (Wed) | | #1 | | Ruby の多重代入において、配列の分解には丸括弧 "(", ")" を使うが、これがなぜ | 角括弧 "[", "]" でないのかという疑問がある。 | | また、多重代入の左辺におけるカンマの扱いもかなり謎である。 | | 配列の分解は ML や Haskell のパターンマッチのしょぼいやつと思えば、生成と分 | 解には同じ形式を使用するのが自然である。具体的には、配列の生成には角括弧を | つかうのだから、分解にも角括弧を使うほうが明らかに自然である。 | | では、角括弧を使えない理由があるかと考えると、パーサの都合はそれなりにある。 | | 仮に、生成時の配列の文法をそのまま代入の左辺に使えるとしてみよう。そうする | と、[a, b, c] = [1, 2, 3] と記述できることになる。 | | ここで、[a, b, c] は左辺であるが、単独で [a, b, c] が表れたときには、配列を | 生成する式とみなせる。つまり、パーサが左から読んでいったとき、[a, b, c] が | 左辺であるかどうかは = を見つけるまではわからない。したがって、a, のところ | まで読んで、a に対応する構文木を生成する場合、これは左辺の一部かどうかはわ | からず、後でどうにかする必要がある。 | | これに対し丸括弧を使うと、同様な文は (a, b, c) = [1, 2, 3] と記述することに | なる。 | | ここで、(a, b, c) は左辺であるが、角括弧の場合と異なり、普通の式とはみなせ | ない。(C と違い Ruby にはカンマオペレータはない) つまり、パーサが左から読ん | でいったとき、最初のカンマを見つけた時点で、普通の式ではないことがわかり、 | 左辺と判断できる。したがって、a, のところまで読んで a に対応する構文木を生 | 成する場合、これは左辺の一部であることがわかっているので、左辺用のノードを | 生成することができる。 | | さて、丸括弧ならばいつも区別できるかというと、そうでもない。配列の角括弧を | 丸括弧にすると、カンマという手がかりが使えるようになるが、配列にはカンマを | 使わない場合がある。カンマを使うのは 2要素以上で、それ未満であれば使わない | のである。 | | とすると (a) = val などという場合は、a が左辺の一部であることを判断するには | = まで待たなければならない。ここで、ネストすると ((((((((((a)))))))))) = val | とかなるわけで、a と = の間には任意個のトークンが入りうる。ということは | 有限個のトークンの先読みで判断するのは無理である。そして、Ruby が使っている | yacc は LALR(1) なので無理である。 | | これに対して Ruby がどう対処しているかというと、1要素でもカンマを必須にする | というものである。つまり、((((((((((a,)))))))))) = val と記述するのである。 | こうすればたしかに、変数 (や他の代入対象) の直後にカンマが表れるので、先読 | みひとつで判断ができる。 | | これで、1要素以上の配列に対応する記法が用意できたことになる。 | | というように、パーサの都合をみてとることは可能である。しかしこれはネガティ | ブな理由であり、丸括弧が素晴らしいという理由ではない。 | | 角括弧よりも丸括弧のほうが良いという理由はあげられるだろうか? | | 2007-08-16 (Thu) | | #1 | | Ruby のブロックパラメータの挙動は長いあいだ謎であった。 | | だが、ついに、もはや謎ではない、と言うことができる。 | | この「謎」はメソッド呼び出しと微妙に動作が異なり、さらにその違いが理解でき | ない、ということである。 | | この謎に挑戦したひとは (私を含め) 過去に何人もいて、だいたいにおいてメソッ | ド呼び出しに挙動をあわせようとして議論をするのだが、全面的に受け入れられる | ことはなく、しかしながらそのたびに微妙に動作が変わることもあって、謎の成長 | に貢献してきた。 | | しかし、現在 (1.9) ではメソッド呼び出しとの違いは以下に集約され、かつ、その | 理由がわかる。 | | □ lambda でないブロックを呼び出したとき | ☆ 引数の数が違ってもエラーにならない | ☆ 配列をひとつ渡したときにそれが引数の並びとして展開されることがある | | なお、lambda な場合はメソッド呼び出しとまったく同じである。(lambda なブロッ | クというのは lambda および define_method についたブロックである。1.9 では | Proc オブジェクトを inspect や p してわかる) | | 上記の違いには理由がある。忘れても後から調べられるよう以下にまとめておく。 | | 引数の数の違いを無視する理由は 10.times { puts "hello" } などを許容するため | である。Integer#times はブロックにひとつ引数を渡すが、このブロックでは受け | 取っていない。このような用法は非常に多く、現時点で ArgumentError にすること | は非現実的である。(ただ、ここはメソッド呼び出しにさらに近づける余地があるか | もしれない) | | 配列の展開の理由は、大きな原因は標準添付されているライブラリの rss である。 | rss には以下のようなコードが大量にある。 | | [ | ['title', nil, :text], | ['link', nil, :text], | ['description', nil, :text], | ['image', '?', :have_child], | ['items', nil, :have_child], | ['textinput', '?', :have_child], | ].each do |tag, occurs, type| | ... | end | | 配列の配列を each して内側の配列を引数の並びとして受け取るというコードであ | る。このような用法は rss だけにみられるものではないが、rss にはとくに大量に | ある。 | | ここで、Array#each が配列の要素をひとつ yield するとすると、yield されたひ | とつの実引数に対し 3つの仮引数で数があわない。もしブロックの呼び出しがメソ | ッド呼び出しと同じ挙動であるとすれば、これは ArgumentError である。 | ArgumentError を避けるためには、以下のようにブロックパラメータを括弧で括り、 | ひとつの引数として受け取ってそれを配列として 3つに分解することができる。 | | [ | ['title', nil, :text], | ... | ['textinput', '?', :have_child], | ].each do |(tag, occurs, type)| | ... | end | | [ruby-dev:29616] を実装したとき、最初は上のように括弧で括ればいいと考えて、 | 配列の展開は行わなかった。しかし、テストを通すために必要なところに括弧を挿 | 入していったところ、最終的に rss でめげたのである。多すぎてあきらめたのであ | る。めげてしまい、また、配列の展開は使い勝手がいいということを認めざるを得 | なかったため、配列の展開は行うことにした。 | | ここで、配列の展開は呼び出し側でなく呼び出される側で行われるが、引数が配列 | ひとつであっても必ず行われるわけではない。 | | たとえば、{|*r|} では決して展開が行われず、引数の並びが配列としてそのまま得 | られるので、ブロックの引数の並びをそのまま転送することができる、つまりラッ | パーを実現することができる。 | | では、展開が行われる条件はなにかというと、引数が配列ひとつであったときで、 | さらにブロックパラメータが以下の形式の場合である。 | | □ 普通の引数がふたつ以上ある場合: {|x,y|}, {|x,y,z|}, {|x,y,*r|}, {|x,y,&b|} など | □ 普通の引数がひとつで最後に余計なカンマがある場合: {|x,|} | □ 普通の引数と残り引数が両方ある場合: {|x,*r|}, {|x,*r,&b|} など | | なお、展開が行われないブロックパラメータの形式は何があるかというと、以下が | ある。 | | □ 普通の引数がひとつだけで残り引数および余計なカンマがない場合: {|x|}, {|x,&b|} | □ 残り引数だけの場合: {|*r|}, {|*r,&b|} | | 一般に、ブロックに対するブロック引数 (上記の &b) は展開の有無には関係しない | が、構文上、&b があると {|x,|} という余計なカンマがある形式は記述できないと | いうことだけには関係する。 | | あと、上記の「普通の引数」は括弧で括られた配列分解の指定でもよい。つまり | {|x|} の x は (y,z) でもよくて、{|(y,z)|} でも展開は行われない。 | | ブロックパラメータの形式によって展開されるかどうかが異なるのにはそれぞれに | 理由がある。 | | □ {|*r|} で展開を行わない理由はラッパーを記述可能にするためである。 | □ {|x|} で展開を行わない理由は Array#each で配列の要素がなんであっても要 | 素を得られるようにするためである。 (もし、ここで展開を行うと「100%くら | いの人には影響する」[ruby-dev:28710] の二の舞になる) | □ {|x,y|} などで展開を行う理由は、rss でめげたからである。 | □ {|x,|} で展開を行う理由は、配列の先頭だけ欲しいときには最後にカンマをつ | ける、という用法を尊重したためである。「配列の先頭だけ得るにはどうした | らいいか」というのは FAQ であり、用法として確立している。 | | あと、{|x,*r|} でも展開を行うが、これについては十分に用法が確立しているかど | うかは自信がない。ただ、配列ひとつを展開せずに {|x,*r|} に適用した場合、x | にその配列が束縛され、r に空配列が束縛されることになる。空配列に固定される | 引数というのは意味がないので、イテレータが常に配列ひとつを yield する場合は | 展開されるほうがまだ有用であろう。意味がないものに比べて有用である、という | のはずいぶんと弱い根拠ではあるが。展開されないほうが有用である場合があると | すれば、イテレータが引数ひとつの配列を yield することもあれば、ふたつ以上の | 引数で yield することもある、という引数の数が可変の場合である。 | | なお、{|x,*r|} で展開を行うことにより、{|x,|} は {|x,*|} と等価となる。(つ | いでにいえば、{|x,|} は、lambda では {|x,*|} でなく {|x|} と等価になる。) | | 上記の展開を行わない場合の無用さに関する議論は {|x,y|} にも適用できる。配列 | を展開しなければ、x にその配列が束縛され、y には nil が束縛される。nil 固定 | というのは意味がない。 | | {|x,|} についても、無用さは説明できる。配列を展開しなければ、{|x,|} は | {|x|} と等価であるが、そうであるならば {|x,|} を使う意味がない。 | | 2007-08-17 (Fri) | | #1 | | もはや謎ではない、と言えば、多重代入もそうである。 | | 「Rubyで一番複雑な仕様はどこだ、と問われれば筆者は即座に多重代入と答える」(*) | とまでいわれた多重代入も 1.9 での挙動は難しくない。 | | (*) 青木峰郎, Ruby ソースコード完全解説, p.414 | | まず、1.9 では多重代入とブロックパラメータは別物である。以前はそうではなく、 | 上記の発言は以前のものであり、両方を含めた話に対するものともとれる。現在 | は別物なので、少なくともブロックパラメータの部分の複雑さは多重代入には及ば | ない。 | | さて、多重代入は、代入の左辺および右辺にカンマとアスタリスクを使った配列の | 中身を記述できるものである。 | | 一般に代入 (単純代入・多重代入) の左辺・右辺は以下の形式である。 | | 左辺: | | □ x = 右辺 (ひとつの代入先を左辺とすることができる) | □ x, y, z = 右辺 (カンマで区切って代入先を複数記述できる) | □ x, y, *z = 右辺 (代入先のひとつに * をつけられる) | □ x, *y, z = 右辺 (* をつける要素はどれでもよい) | □ x, = 右辺 (左辺の最後にはカンマをつけられる) | | 右辺: | | □ 左辺 = u (ひとつの式を右辺とすることができる) | □ 左辺 = u, v, w (カンマで区切って式を複数記述できる) | □ 左辺 = u, v, *w (式に * をつけられる) | □ 左辺 = *u, *v, w (左辺と異なり右辺では複数個の * をつけてもよい) | | これらの左辺・右辺は任意に組み合わせられるが、x = u の形を単純代入といい、 | それ以外を多重代入という。 | | また、左辺・右辺は両方ともネストできる。 | | 右辺のネストは、u, v, w が任意の式であることから、それらに配列も書けるとい | うだけの事である。 | | 左辺のネストは、以前述べたように、角括弧でなく丸括弧を用いカンマの扱いが異 | なるという文法で行える。具体的には x とかのところに (y,z) などと配列を分解 | する指定を記述できる。右辺の配列と記法が揃っていないが、文法が自然でないの | はパーサの都合ということで、謎ではない。(謎ではないが、望ましいという話でも | ないので、修正する方向に持っていける可能性もあるとは思う。) | | なお、x = u で x が (y,z) の場合、つまり (y,z) = u のように、左辺が丸括弧で | 括られた単一の要素からなるものは、慣習的には多重代入の一種と認識されている | が、ここでは説明の都合上単純代入として扱う。 | | 代入では、右辺と左辺を対応づけるわけであるが、左辺と右辺が両方とも単純な場 | 合、つまり x = u の形式であれば自明に x と u を対応づければ良い。(x が | (y,z) とかで、u が配列でなかったときにどうするか、という話はある。これは後 | 述する) | | また、左辺と右辺が両方とも単純でない場合、つまり、x, y, *z = *u, *v, w など | の場合でも悩むことはない。これは (x, y, *z) = [*u, *v, w] というように、両 | 辺それぞれを括弧で括って解釈すれば、単純代入に帰着できる。 | | 問題は、左辺・右辺のどちらかだけが単純でない場合である。たとえば x, y = u | とか x = *u とか x, = u とかである。この場合、左右の対応が自明でないのでな | んらかの細工が必要になる。 | | 1.9 におけるここでの細工は簡単なものである。左辺・右辺それぞれについて単純 | でない場合は括弧で括ったものとして解釈するのである。つまり、(x, y) = u とか | x = [*u] とか (x,) = u とかである。これにより両辺が単純になるので全ての代入 | は単純代入に帰着できることになる。 | | したがって、あとは配列の分解の話を定義すれば、代入の意味を定義できる。 | | 配列の分解を配列に対して行うやりかたは明らかである。配列でないものに対して | 行うときは、事前にそれ自身を唯一の要素とする配列に変換してから行う。これが | 良い挙動であるかどうかは議論があるだろうが、謎というほどではない。 メール用に書いたものではないので文体は気にしないでください。 -- [田中 哲][たなか あきら][Tanaka Akira]