Brian Candler
2/10/2007 2:53:00 PM
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.