From: Akinori MUSHA Date: 2009-05-10T18:24:57+09:00 Subject: [ruby-dev:38420] Re: Enumerable#gather_each --pgp-sign-Multipart_Sun_May_10_18:25:19_2009-1 Content-Type: text/plain; charset=ISO-2022-JP At Sun, 10 May 2009 15:57:33 +0900, Tanaka Akira wrote: > In article <86ocu132gq.knu@iDaemons.org>, > "Akinori MUSHA" writes: (snip) > >  最初の例というのが > >> arg = lambda {|l| /\A\=~ l ? true : nil } > > で読めなかったのですが、 l == "\n" でしたか。 > > あぁ、すいません。そこは /\A\s/ =~ l です。 > > パラグラフの例は最初のメール [ruby-dev:38392] のもうちょっと > 下に出てきます。  なるほど。空行は文脈依存と思ったのですが、 /\A\s/ が空行にも マッチしてくれるので都合がいいわけですね。 > > これは > > > > prev, s.status = s.status, (e == "\n") > > b.flush if prev != b.status > > b << e > > > > くらいで悪くはないと思います。b.status != nil のところは、上記の > > 「空なら flush しない」で手当てするとして。 > > それでも gather_each のほうがずっと短いですよね。  はい、この例はまさに gather_each の守備範囲にぴったりです。  しかし、 ChangeLog のような場合は gather_each も単純さを維持 できず複数行になると思います。buffer は、そうした少しの違いには 少しの違いで対応できます。 > >  やりすぎかもしれませんが、 status/status= を提供するのなら、 > > prev_status や status_changed? も用意するという手はあります。 > > 私としては、状態変化をユーザが意識する必要はない用途は充分に > 多いと考えています。 > > 状態変化をそうやってサポートするのは、状態変化をユーザが意識 > する必要がある用途には有用でしょう。しかし、まずそういう用途 > が充分に多いのかという点について議論が必要ではないでしょうか。  はい。私の方は gather_each がカバーできる範囲の広さについて 疑問を持っていますが、二択という話ではないと思っています。 > >  区切るだけなら確かに1行で済みますが、実際にはサンプルコード辺か > > どうかを判定したり、前後の空行を除いたりと最終結果までの道のりは > > 長いので何とも言えません。要らない部分まで集めて(gather)いますが、 > > 本当はもっと複雑な処理が必要なので buffer のようなものがあれば、 > > 取捨や加工についても引き受けることができると思いました。 > > 後でやればいいんじゃないでしょうか。gather はまとめるだけで > 情報を捨てませんし。  情報を捨てないとすると、カバー範囲はとても狭くなると思います。 たとえばコード片の摘出の例は実用的な処理として完結しないので、 gather の有用な使用例としては弱いのではないでしょうか。 > >  上記の通り、実際に考えるべきことが後ろに残ると思うので、 gather > > 単体の提供する機能が中途半端に思えたのです。すなわち、インデント > > レベル等、分類の基準として計算した値(ブロックの評価値)を捨てて > > しまっていますが、この例でも、後段でまた必要になりそうですよね。 > > インデントの深さは、むしろ後から計算するほうがいいんじゃない > でしょうか。インデントされたブロックでは、複数の行のうち、もっ > とも浅いインデントが欲しそうですよね。  たとえばですが、 RD や markdown のような文書を処理する場合を 考えていました。考えてみると gather の守備範囲外かもしれませんね。 > もし、どうしても分類の値が必要だということであれば、yield す > る配列にインスタンス変数としてつけておく (必要ならそれを参照 > するアクセサも) のにはやぶさかではありません。  その場合のAPIはどうなるのでしょう。gather 本来の用途には邪魔な データがくっついてしまうので、別メソッドですかね。 > > ということであれば、 buffer を使って gather 等を実装するのは容易 > > なので、複数のメソッドを用意するのなら、実装を共有するためにも > > buffer のような汎用のものを持つメリットがあるということになるの > > ではないでしょうか。 > > 実装してみましたが、そういうものがなくてもそれほど面倒ではあ > りませんでした。  なるほど。実は map の Enumerator 版(配列を生成しない)を手元で 作っていたのですが、 Yielder があれば簡単なんですよね。 > 汎用のものを用意するのは、汎用のものが必要な例が出てきてから > でいいんじゃないでしょうか。 > > >  buffer は1回のイテレーションで複数の値を push したり複数回 > > flush したり(あるいはしなかったり)でき、またイテレータ引数で > > なく任意の値を push できるので、 lexer などを実装できます。 > > > >  というか、 scanf.rb をパースする例を見て、実際の延長上には > > lexer のようなものがあるんじゃないかと推測しました。 > > 複雑なことができるからいろんな用途があるに違いない、という話 > ではなく、具体的な例はありませんか?  むしろ、田中さんの出された2問を具体例としたときの私なりの解が Buffer なんですよ。scanf.rbからのコード片の切り出しという最初の 例は、コード片でない部分も含めて pp するというのが最終目的では ないですよね。削除するという機能が欠けているので不要な部分まで 得られてわずらわしい。従って取捨選択の機能が必要ではないか、と 思いました。  次にパラグラフごとに切るという例は gather_each がもっとも短く 書ける例でした。しかしながら、ヘッダを区切りとする構造を扱う際は 短くなくなる。従って恣意的なタイミングで yield できる必要がある、 あるいは状態の管理を支援するといいのではないか、と思いました。  これらを合わせた考えたのが Bufferです。当たり前ですが複雑なこと 自体を目指しているのではなくて、 gather_each では不十分と感じ、 一部のケースを除けば最適な解とは思えなかったのが対案の動機です。  いちおう、私からもいくつか具体例を挙げてみます。なお、空だったら yield しないという改変を前提としています。Gist に上げました。 http://gist.github.com/109550 Unix mbox の切り分け: file.lines.buffer { |line, buf| buf.flush if /\AFrom / =~ line buf << line } ChangeLog の各エントリの切り出し: open("ChangeLog") { |f| header = nil f.lines.buffer { |line, buf| case line when /\A\S/ # 頭が空白でなければヘッダ header = line when /\A\s*\z/ # 空行は区切り buf.flush when /\A\s+\*/ # * で始まる行はエントリの始まり buf.flush if header buf << header buf << line end when /\A\s+\S/ # 空白で始まる行は、エントリが始まっていればエントリの継続行 buf << line unless buf.empty? end }.each { |header, *entry_lines| puts header, *entry_lines puts '----' } } 簡単な電卓: STDIN.lines.buffer { |line, buf| until line.empty? line.sub!(/\A\s+/, '') line.sub!(/\A(\d*\.?\d+)/) { buf << [:NUM, $1.to_f]; '' } line.sub!(/\A([+\-*\/])/) { buf << [:OP, $1.intern]; '' } end buf.flush }.tap { |expr| result = 0 calc = proc { |x, op, y| case op when :+ x + y when :- x - y when :* x * y when :/ x / y end } expr.each { |tokens| op = num = nil tokens.each { |token| type, data = token case type when :OP if op && num result = calc[result, op, num] end op = data when :NUM num = data result = num if !op end } if op && num result = calc[result, op, num] end puts "> %g" % result } } -- Akinori MUSHA / http://akinori.org/ --pgp-sign-Multipart_Sun_May_10_18:25:19_2009-1 Content-Type: application/pgp-signature Content-Transfer-Encoding: 7bit -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.9 (FreeBSD) iEYEABECAAYFAkoGnX8ACgkQkgvvx5/Z4e5HuACfZlSJEVHRuelgbbo3AyNsy+Uo 5REAoNwm2kde32fCByVnhQGL/2msG6xQ =chY5 -----END PGP SIGNATURE----- --pgp-sign-Multipart_Sun_May_10_18:25:19_2009-1--