From: Brian Candler Date: 2008-11-03T17:55:40+09:00 Subject: Re: ruby1.9: lazy versions of Enumerator#select and friends? David A. Black wrote: > This is the feature that disappeared -- definitely not identical to > what you're doing, but kind of germane: > > a = [1,2,3,4].enum_for(:map, &lambda {|x| x * 10}) > a.select {|x| x > 20 } # [30,40] OK, I think I see what you're saying now, except I'm not sure exactly how ':map' interacts with this. If this code calls the existing 'map' method, then does that mean that 'map' also behaved differently when invoked via an Enumerator? (Because otherwise, 'map' normally builds the entire result array up-front). How did 'map' know whether it was being called in this context or not? Using current ruby-1.9 you would write something like a = [1,2,3,4] Enumerator.new do |y| a.each { |e| y << e * 10 } end.select { |x| x > 20 } but you can see I've had to write the 'map' logic in-line. > True, but that doesn't take newly-written enumerable classes, and > there are a few overrides here and there (like Hash#select returning a > hash, in 1.9). Ah, well that's a bit messy. Taking that mess a bit further, I guess you could arrange for Array#select similarly to be hardcoded always to return an Array? Hmm. > I'm still not sure I'd like to have to do a kind of > enumerator-resolving last call to every map, select, inject, etc. It's > different with each, because it exists only for its block side-effects > in the first place. The ones that return result sets that you care > about would all have to be "capped" with to_a or something, unless the > last method in the chain somehow knew not to return an enumerator. Maybe we're actually discussing my option #3 - which was to change Enumerable#select to return an Enumerator. My option #2 was that Enumerator#select return an Enumerator, in the same way that Hash#select returns a Hash as you just described. That is: class Enumerator def select ... etc end end Then foo.select { ... } would always return an Array (via Enumerable#select), unless foo was an Enumerator (in which case it would return an Enumerator), or a Hash (in which case it would return a Hash) The 'capping' would only be needed if you create an Enumerator in the chain. > I wonder what role what I have dubbed the "un-overriding" of > enumerable methods would play. Here's an example: > >>> hash = { 1 => 2 } > => {1=>2} >>> hash.select { true } > => {1=>2} >>> hash.each.select { true } > => [[1, 2]] > > Since Enumerator doesn't override select (which of course it can't, in > a general way), calling select on an enumerator for the hash has a > different effect from calling select on the hash. Yes, I see what you're saying. But actually I'm proposing that hash.each.select return another Enumerator. So you can cap it with a 'to_hash' method at the end to get the same result: require 'lazy' class Enumerator def to_hash each_with_object({}) { |(k,v),o| o[k] = v } end end hash = {1=>2} p hash.lselect { true } #=> # p hash.lselect { true }.to_hash #=> {1=>2} As you say, arguably it's tedious to have to stick .to_a or .to_hash at the end, but once you have an Enumerator, you have (a) lost the memory of what it is you created it from, and (b) quite probably don't want to create the same thing again anyway - for example, sticking each { ... } at the end of the chain so as to consume the items rather than build a new object. So I think I'm warming to the middle ground: Enumerator#foo returns an Enumerator, so Enumerators chain together, and if at the end of it you really want an array (or a hash or whatever) rather than an Enumerator, then you add .to_a or .to_hash But equally, if you call Array#select or Hash#select directly, without first "priming" the chain with an Enumerator object, then you get back another Array or Hash immediately. It makes sense when you understand it, although someone who didn't understand this might have difficulty distinguishing [10,20,30].select { ... }.map { ... }.each { ... } from [10,20,30].each.select { ... }.map { ... }.each { ... } I think that 'each without a block' is not necessarily a good way of flagging 'make me an enumerator'; writing to_enum would be clearer. Actually, I have to say that I can't really see the purpose of 'map without a block', 'select without a block'. I saw a posting showing how you could chain foo.select.with_index { |x,i| ... } but surely that would be clearer as foo.with_index.select { |x,i| ... } (where 'with_index' creates an Enumerator which adds the index for you) Unless there's a good use for this which I've overlooked, I'd be happy for Ruby to go back to 1.8.6 behaviour of each/map/select *requiring* a block. In that case, my earlier example would have to be written [10,20,30].to_enum.select { ... }.map { ... }.each { ... } which is pretty clear (to me :-) Regards, Brian. -- Posted via http://www.ruby-forum.com/.