From: Brian Candler Date: 2007-02-10T23:53:18+09:00 Subject: Re: Jumping to "the next one" in something#each On Sat, Feb 10, 2007 at 08:11:16AM +0900, Garance A Drosehn wrote: > >Well, if you don't mind trashing the array you're processing you could use > >Array.shift to remove the first element and return it... then just > >re-order your loop to exit once the element returns is nil. > > > That's not a general solution, though. Consider an example of: > > filetree.added_files.keys.sort.each { |fname| > ...whatever... > } > > I'd have to create a temp-array to hold the result from the sort step. Note that Array#sort *does* always generate a temp array. (a.sort is implemented internally as a.dup.sort!) > Besides, I wanted something which works for *any* routine which > takes a code-block, not just objects of type Array. How could you > make that work for the each_pair method of Hash objects? Change h.each_pair to h.to_a (which also creates a temp array, of course) In the most general case, you might need to explicitly construct the array, changing foo.each { |x,y,z| ... } to something like foo.map { |*x| x } But setting this aside: I think it *is* possible to get the pattern you asked for - i.e. being able to 'pop' the next value from an enumerable on demand, without first flattening it into an array. Have a look at the Generator class: require 'generator' a = ["arg", "-f", "one", "-c", "two", "final"] g = Generator.new(a) while g.next? arg = g.next if arg == "-f" file = g.next puts "File: #{file}" elsif arg == "-c" conf = g.next puts "Conf: #{conf}" else puts "Arg: #{arg}" end end Generators are implemented using continuations, which are not very efficient. So you sacrifice speed for convenience. > And to complicate things a bit more, sometimes the option might want > to *check* the next option, but not necessarily use it up. For instance, > if the "-f" option can take one-or-more names, then it would keep taking > the next value for 'arg' until it sees arg =~ /^-/ Your generator then needs some buffering to be able to fetch and remember the next value. But we already have this; Generator#current tells you the 'next' item without actually removing it. So try this for size: require 'generator' module Enumerable def geneach g = Generator.new(self) while g.next? yield g, g.next end end end a = ["arg", "-f", "one", "two", "three", "-c", "conffile", "final"] a.geneach do |g, arg| if arg == "-f" while g.current !~ /^-/ file = g.next puts "File: #{file}" end elsif arg == "-c" conf = g.next puts "Conf: #{conf}" else puts "Arg: #{arg}" end end There are probably some rough edges to sort out in this pattern (esp. what happens if the enumerator yields multiple values) but you get the idea. Now, doing all this with generators is a general-purpose solution; it works with any Enumerable (i.e. anything which implements the 'each' method), but as I said before, it's inefficient. You can optimise this by making a custom geneach method for Arrays, which will be much faster. This gives: class ArrayGenerator def initialize(a) @a = a @index = 0 end def next? @index < @a.size end def next v = @a[@index] @index += 1 v end def current @a[@index] end #can also add other methods to match Generator, e.g. #def rewind # @index = 0 #end end class Array def geneach g = ArrayGenerator.new(self) while g.next? yield g, g.next end end end a = ["arg", "-f", "one", "two", "three", "-c", "conffile", "final"] a.geneach do |g, arg| if arg == "-f" while g.current !~ /^-/ file = g.next puts "File: #{file}" end elsif arg == "-c" conf = g.next puts "Conf: #{conf}" else puts "Arg: #{arg}" end end Notice that the 'main' program which parses the array is unchanged. You can of course have both implementations loaded at once. So Arrays will get the efficient behaviour, and anything else that is Enumerable will get the continuation implementation. HTH, B.