[#38392] Enumerable#gather_each — Tanaka Akira <akr@...>

ときに、複数行をまとめて扱いたいことがあります。

47 messages 2009/05/09
[#38394] Re: Enumerable#gather_each — ujihisa <ujihisa@...> 2009/05/09

ujihisaと申します。

[#38400] Re: Enumerable#gather_each — Yukihiro Matsumoto <matz@...> 2009/05/09

まつもと ゆきひろです

[#38399] Re: Enumerable#gather_each — "Akinori MUSHA" <knu@...> 2009/05/09

At Sat, 9 May 2009 15:30:20 +0900,

[#38405] Re: Enumerable#gather_each — Tanaka Akira <akr@...> 2009/05/10

In article <86r5yy2nrg.knu@iDaemons.org>,

[#38417] Re: Enumerable#gather_each — "Akinori MUSHA" <knu@...> 2009/05/10

At Sun, 10 May 2009 10:08:47 +0900,

[#38524] [Bug #1503] -Kuをつけた時、/[#{s}]/n と Regexp.new("[#{s}]",nil,"n") で実行結果が異なる — sinnichi eguchi <redmine@...>

Bug #1503: -Kuをつけた時、/[#{s}]/n と Regexp.new("[#{s}]",nil,"n") で実行結果が異なる

8 messages 2009/05/22

[ruby-dev:38420] Re: Enumerable#gather_each

From: "Akinori MUSHA" <knu@...>
Date: 2009-05-10 09:24:57 UTC
List: ruby-dev #38420
At Sun, 10 May 2009 15:57:33 +0900,
Tanaka Akira wrote:
> In article <86ocu132gq.knu@iDaemons.org>,
>   "Akinori MUSHA" <knu@iDaemons.org> 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/

In This Thread