Ruby Forum Ruby > RCR enumerable extra into core

Posted by Roger Pack (rogerdpack)
on 08.11.2009 01:22
I'm considering suggesting that the base functionality for the
enumerable-extra gem[1] be submitted into core.

In a nut shell, it's that you can do

list.map :name

as an alternative to

list.map &:name

which is arguably less readable.
Thoughts?
-r
[1] http://allgems.ruby-forum.com/docs/enumerable-extra/0.1.2/doc/hanna/
Posted by Tor Erik Linnerud (toreriklinnerud)
on 08.11.2009 01:43
> list.map :name

Makes sense, given that you can already do

list.reduce(:+)

Tor Erik
Posted by David A. Black (Guest)
on 08.11.2009 01:57
(Received via mailing list)
Hi --

On Sun, 8 Nov 2009, Roger Pack wrote:

>
> which is arguably less readable.
> Thoughts?

I'm very much in sympathy with the position that &:name is ugly. It's
always struck me as uncharacteristically line-noise-ish for Ruby
(though I also understand the motivation for it). I have trouble
talking myself into using it.

At the same time, the merit of &:name over :name is that &:name does
tell you what's going on, once you know that the & in front of any
object (symbol or otherwise) in last-argument position means that the
object will be converted to a Proc and play the block role.

Having :name magically understood as &:name is, for my taste, too much
invisible ink. It would also mean that map (and maybe other Enumerable
methods) had one argument syntax and all other iterators had another,
since presumably there's no general way to make method(:sym) know when
it's supposed to actually mean method(&:sym). I don't think that's a
useful special case.

(I know there's inject(:method), but note that that's a different
case; inject(:method) is not shorthand for inject(&:method).)


David

--
The          Ruby training with D. Black, G. Brown, J.McAnally
Compleat     Jan 22-23, 2010, Tampa, FL
Rubyist      http://www.thecompleatrubyist.com

David A. Black/Ruby Power and Light, LLC (http://www.rubypal.com)
Posted by David A. Black (Guest)
on 08.11.2009 02:01
(Received via mailing list)
On Sun, 8 Nov 2009, David A. Black wrote:

> (I know there's inject(:method), but note that that's a different
> case; inject(:method) is not shorthand for inject(&:method).)

I garbled that bit, due to an irb session gone horribly wrong :-) So
forget that sentence. I do have a feeling there's something my brain
is groping toward that differentiates the inject case, but I could be
totally wrong about that.


David

--
The          Ruby training with D. Black, G. Brown, J.McAnally
Compleat     Jan 22-23, 2010, Tampa, FL
Rubyist      http://www.thecompleatrubyist.com

David A. Black/Ruby Power and Light, LLC (http://www.rubypal.com)
Posted by Gavin Sinclair (Guest)
on 08.11.2009 07:05
(Received via mailing list)
On Nov 8, 11:57 am, "David A. Black" <dbl...@rubypal.com> wrote:
>
> Having :name magically understood as &:name is, for my taste, too much
> invisible ink. [...]

I think                          list.map :name
is clearly to be understood as   list.map { |x| x.send(:name) }
not as an abbreviation for       list.map &:name
which I agree is extraordinarily ugly.

I think (don't quote me) allowing #map to take a symbol was debated
and rejected a long time ago.  I'd like to see it implemented in core,
as it's a shortcut for a common idiom, reads nicely and is
unambiguous.  I don't know when list.inject(:+) started working, but
it's a good thing!

One thing occurred to me, though.  Using "send" in the implementation
would allow access to private methods.  A check would need to be made
before pulling the trigger.
Posted by Haoqi Haoqi (haoqi)
on 08.11.2009 10:40
list.map :name
+1
Posted by David A. Black (Guest)
on 08.11.2009 15:40
(Received via mailing list)
Hi --

On Sun, 8 Nov 2009, Gavin Sinclair wrote:

>> object will be converted to a Proc and play the block role.
> and rejected a long time ago.
It was: http://oldrcrs.rubypal.com/rejected.html#rcr50. Needless to
say that doesn't contractually bind Matz to any eternal decision :-)

There's no doubt it looks better than &:sym. My problem with it is
that it adds a special case (or a small subclass of special cases). I
don't think it would be a disaster, but I always dislike things where
it feels like it has to be accounted for sort of inside out (there's
the general case of &object, the common case of &:sym, and now a kind
of escape from &:sym via :sym).

Mind you, I really don't think it would be a calamity, just a little
hard to account for in terms of what's around it.

> I'd like to see it implemented in core, as it's a shortcut for a
> common idiom, reads nicely and is unambiguous.  I don't know when
> list.inject(:+) started working, but it's a good thing!
>
> One thing occurred to me, though.  Using "send" in the implementation
> would allow access to private methods.  A check would need to be made
> before pulling the trigger.

You could use public_send.


David

--
The          Ruby training with D. Black, G. Brown, J.McAnally
Compleat     Jan 22-23, 2010, Tampa, FL
Rubyist      http://www.thecompleatrubyist.com

David A. Black/Ruby Power and Light, LLC (http://www.rubypal.com)
Posted by Rick Denatale (rdenatale)
on 08.11.2009 18:56
(Received via mailing list)
On Sun, Nov 8, 2009 at 9:39 AM, David A. Black <dblack@rubypal.com> 
wrote:

> Mind you, I really don't think it would be a calamity, just a little
> hard to account for in terms of what's around it.

Not completely on-point, but I tend to be rather conservative when
thinking about such proposed changes.

I've recently taken over an existing Rails app, one of the first tasks
was to get it to run with Ruby 1.9.

Most of the changes I needed to make turned out to do with the fact
that 1.9 redefined Array#to_s

Where prior to 1.9 Array#to_s was the same as Array#join so:

  [1, nil, "foo"].to_s => "1foo"

in 1.9 it's been redefined to be the same as Array#inspect so:

 [1, nil, "foo"].to_s => "[1, nil, \"foo\"]"

I was surprised at how many times an array was used in a context which
expected the pre-1.9 behavior.

When I explained to the client that so many changes were caused
because Ruby 1.9 changed Array#to_s  his response was "Why did they do
that?"


Of course the upside of such changes is that they generate billable
hours, but I'd rather generate more value per hour.  But c'est la vie!
--
Rick DeNatale

Blog: http://talklikeaduck.denhaven2.com/
Twitter: http://twitter.com/RickDeNatale
WWR: http://www.workingwithrails.com/person/9021-rick-denatale
LinkedIn: http://www.linkedin.com/in/rickdenatale
Posted by David A. Black (Guest)
on 08.11.2009 22:49
(Received via mailing list)
Hi --

On Mon, 9 Nov 2009, Rick DeNatale wrote:

>
>
> I was surprised at how many times an array was used in a context which
> expected the pre-1.9 behavior.
>
> When I explained to the client that so many changes were caused
> because Ruby 1.9 changed Array#to_s  his response was "Why did they do
> that?"
>
> Of course the upside of such changes is that they generate billable
> hours, but I'd rather generate more value per hour.  But c'est la vie!

I'll take on all comers in the matter of being conservative about
changes to Ruby :-) Part of the case with the examples you're
referring to, though, is the fact that 1.9 is more of a major-number
change, in spirit, than it sounds like.


David

--
The          Ruby training with D. Black, G. Brown, J.McAnally
Compleat     Jan 22-23, 2010, Tampa, FL
Rubyist      http://www.thecompleatrubyist.com

David A. Black/Ruby Power and Light, LLC (http://www.rubypal.com)
Posted by Roger Pack (rogerdpack)
on 09.11.2009 16:36
>> which is arguably less readable.
>> Thoughts?
> 
> At the same time, the merit of &:name over :name is that &:name does
> tell you what's going on, once you know that the & in front of any
> object (symbol or otherwise) in last-argument position means that the
> object will be converted to a Proc and play the block role.

Hmm.  I suppose I'm of a slightly different opinion--to me
list.map &:symbol

is less clear than
list.map(:symbol)


If we were required to write
list.map &:symbol.to_proc

then I would be more easily convinced that list.map(:symbol) is too 
implicit.  So for me it's implicit already.

I'm not too worried about the run time slowdown...so many methods are 
already special cased...it doesn't seem to be a too large concern to the 
core guys...

-r
Posted by Marnen Laibow-Koser (marnen)
on 09.11.2009 16:49
Roger Pack wrote:
> 
>>> which is arguably less readable.
>>> Thoughts?
>> 
>> At the same time, the merit of &:name over :name is that &:name does
>> tell you what's going on, once you know that the & in front of any
>> object (symbol or otherwise) in last-argument position means that the
>> object will be converted to a Proc and play the block role.
> 
> Hmm.  I suppose I'm of a slightly different opinion--to me
> list.map &:symbol
> 
> is less clear than
> list.map(:symbol)
> 
> 
> If we were required to write
> list.map &:symbol.to_proc

But we are.  &:symbol is equivalent to :symbol.to_proc.  So the & form 
is explicit -- and thus arguably clearer.  I would almost expect 
list.map :symbol to return :symbol for each item in list.

> 
> then I would be more easily convinced that list.map(:symbol) is too 
> implicit.  So for me it's implicit already.

Because you're misunderstanding the syntax.

> 
> I'm not too worried about the run time slowdown...so many methods are 
> already special cased...it doesn't seem to be a too large concern to the 
> core guys...
> 
> -r

Best,
--
Marnen Laibow-Koser
http://www.marnen.org
marnen@marnen.org
Posted by Haoqi Haoqi (haoqi)
on 10.11.2009 04:05
list.map(:symbol) is just like object.send(:symbol),except the :symbol 
for list.
so list.map(:symbol) is good~
Posted by David A. Black (Guest)
on 10.11.2009 04:41
(Received via mailing list)
Hi --

On Tue, 10 Nov 2009, Roger Pack wrote:

> list.map &:symbol
>
> is less clear than
> list.map(:symbol)

I don't think that &:symbol is inherently clear, but once you know the
rule, then it's just an example of the rule.

> If we were required to write
> list.map &:symbol.to_proc
>
> then I would be more easily convinced that list.map(:symbol) is too
> implicit.  So for me it's implicit already.

Part of the problem I think is that map(:symbol) wouldn't really be an
abbreviation of map(&:symbol) (which would presumably still work); it
would be a new semantics for map, namely that map would now take an
argument, but it would look a lot like shorthand for &:symbol. In a
way I wish they were more different, so that they wouldn't have to be
explained in terms of each other (which I guarantee is how they'll be
seen).

> I'm not too worried about the run time slowdown...so many methods are
> already special cased...it doesn't seem to be a too large concern to the
> core guys...

Or, looking at it the other way, maybe there are so many special cases
that we've reached the quota :-) That's always the flip-side of the
precedent reasoning.


David

--
The          Ruby training with D. Black, G. Brown, J.McAnally
Compleat     Jan 22-23, 2010, Tampa, FL
Rubyist      http://www.thecompleatrubyist.com

David A. Black/Ruby Power and Light, LLC (http://www.rubypal.com)
Posted by Marnen Laibow-Koser (marnen)
on 10.11.2009 05:57
Haoqi Haoqi wrote:
> list.map(:symbol) is just like object.send(:symbol),except the :symbol 
> for list.

But it isn't.  The semantics of the two are completely different.  You 
can only send a method name, but a naked method name doesn't make any 
sense as an argument to map.

> so list.map(:symbol) is good~

Because you don't understand what's going on?  No thanks.
Best,
--
Marnen Laibow-Koser
http://www.marnen.org
marnen@marnen.org
Posted by Brian Candler (candlerb)
on 10.11.2009 10:36
David A. Black wrote:
> It would also mean that map (and maybe other Enumerable
> methods) had one argument syntax and all other iterators had another,
> since presumably there's no general way to make method(:sym) know when
> it's supposed to actually mean method(&:sym).

As you say, it could be extended to other Enumerable methods which 
currently don't take an argument. Maybe useful for:

    students.max(:age)

whereas currently you would have to write something like

    students.map(&:age).max

or

    students.max { |a,b| a.age <=> b.age }.age

[Note that max(&:age) doesn't work]

Thinking aloud: methods which iterate over a collection would be 
expected to replace each elem with elem.send(*args) if given arguments. 
And this could be simplified by delegating that job to the 'each' 
method.

  class Array
    alias :old_each :each
    def each(*args, &blk)
      if args.empty?
        old_each(&blk)
      else
        old_each { |elem| yield elem.send(*args) }
      end
    end
  end

  module Enumerable
    def max(*args, &cmp)
      cmp ||= Proc.new { |a,b| a<=>b }
      first = true
      res = nil
      each(*args) do |elem|      # << NOTE *args passed down
        if first
          res = elem
          first = false
          next
        end
        res = elem if cmp[elem, res] > 0
      end
      res
    end
  end

  class Person
    attr_accessor :name, :age
    def initialize(name, age)
      @name, @age = name, age
    end
  end

  students = []
  students << Person.new("Alice",35)
  students << Person.new("Bob",33)
  puts students.max(:age)

But I don't feel strongly that this is worthwhile, as IMO the language 
is more than complicated enough already.

In any case, it was already decided that the "right" way to pass 
arguments to an Enumerable was to create a new Enumerator proxy object 
for it. That is, even if 'each' did take arguments, you're supposed to 
write

  students.to_enum(:each, :age).max

rather than max taking arguments and passing them through.
Posted by Roger Pack (rogerdpack)
on 10.11.2009 16:05
>> list.map(:name)

The next question...

we currently have
list.map => Enumerator

so should
list.map(:method)

best return an array or an enumerator? (though I don't really understand 
why enumerators are cool, apparently they've become quite common lately)
Thoughts?


>> list.map(:method, arg1)

This would be a natural extension of being able to do list.map(:method)

but it feels less intuitive than forcing the user to do

>> list.map{|i| i.method(arg) }

Or does it?
Thoughts?
-r
Posted by Marnen Laibow-Koser (marnen)
on 10.11.2009 16:25
Roger Pack wrote:
[...]
> 
> 
>>> list.map(:method, arg1)
> 
> This would be a natural extension of being able to do list.map(:method)
> 
> but it feels less intuitive than forcing the user to do
> 
>>> list.map{|i| i.method(arg) }
> 
> Or does it?
> Thoughts?

Questions like this point to the very flaws that indicate that list.map 
:method is a bad idea.  If you understand :symbol.to_proc, or its 
abbreviation &:symbol, there is no need for the proposed syntax.

> -r

Best,
--
Marnen Laibow-Koser
http://www.marnen.org
marnen@marnen.org
Posted by Brian Candler (candlerb)
on 10.11.2009 16:47
Roger Pack wrote:
> we currently have
> list.map => Enumerator
> 
> so should
> list.map(:method)
> 
> best return an array or an enumerator?

The ultimate conclusion of this: all Enumerable methods should return 
other Enumerators. When you want a real array, then add .to_a to the 
end.

> (though I don't really understand 
> why enumerators are cool, apparently they've become quite common lately)

It's cool because evaluation takes place "left to right" instead of 
generating potentially huge intermediate arrays. You start getting 
answers sooner, you can write results out to disk or a socket without 
buffering, and you can deal with infinite lists, but using the same 
syntax.

e.g.
    a = (1..1/0.0).select{ |i| i % 2 == 0 }.map{ |i| i + 100 
}.take(10).to_a

Implementing this turns out to be easier than you'd think, and is 
efficient. No funky Fibers or Continuations required.

http://github.com/trans/facets/blob/master/lib/core/facets/denumerable.rb
http://github.com/trans/facets/blob/master/lib/core/facets/enumerable/defer.rb
Posted by Rick Denatale (rdenatale)
on 10.11.2009 20:46
(Received via mailing list)
On Tue, Nov 10, 2009 at 10:47 AM, Brian Candler <b.candler@pobox.com> 
wrote:
> other Enumerators. When you want a real array, then add .to_a to the
> end.

And you base this ultimate conclusion on what?

Prior to Ruby 1.9, most Enumerable methods required that a block be
passed and would raise an error if not. There are  few, such as to_a
which did not.

If you wanted an enumerator you needed to do something like

(1..100).enum_for(:inject, 0)

In Ruby 1.9 the enumerable methods got a little more relaxed, they
work as before if a block IS given, and if not they return an
enumerator instead.

So, my ultimate conclusion is that enumerable methods should almost
always use a block passed as an argument to do whatever they do, and
for Ruby 1.9 at least return an enumerator if a block is not passed
which will do whatever the enumerable itself would do with a block.

And a Ruby method can't distinguish wheter it was called with syntax 
like

   some_method { # some block
                        }


  or

   some_method(&something_which_understands_to_proc)

In both cases block_given? inside the method invocation will return 
true.

Now as I understand it, the proposal would be to make

  list.map(:method)

do the same thing as

 list.map(&:method)

One problem with this is what to do with something like

 list.inject(:initial_value)

Where instead of a proc, :initial_value represents the initial value
for an enumerator.  I would argue that only the caller knows which one
he/she wants.

That "&" is pretty important in distinguishing these two cases.



--
Rick DeNatale

Blog: http://talklikeaduck.denhaven2.com/
Twitter: http://twitter.com/RickDeNatale
WWR: http://www.workingwithrails.com/person/9021-rick-denatale
LinkedIn: http://www.linkedin.com/in/rickdenatale
Posted by Brian Candler (candlerb)
on 10.11.2009 21:30
Rick Denatale wrote:
> On Tue, Nov 10, 2009 at 10:47 AM, Brian Candler <b.candler@pobox.com> 
> wrote:
>> other Enumerators. When you want a real array, then add .to_a to the
>> end.
> 
> And you base this ultimate conclusion on what?

The OP was asking whether map(:foo) should return an Array or an 
Enumerator. I was trying to say that if you're ambivalent about this, 
one logical conclusion (or extreme viewpoint) is that you could always 
return an Enumerator, even for

   map { |x| x*x }

I wasn't saying that Ruby does anything like this, and it's a tangent to 
the original thrust of map(:foo).
Posted by Gavin Sinclair (Guest)
on 11.11.2009 06:25
(Received via mailing list)
Brian Candler wrote:
> one logical conclusion (or extreme viewpoint) is that you could always
> return an Enumerator, even for
>
>    map { |x| x*x }
>
> I wasn't saying that Ruby does anything like this, and it's a tangent to
> the original thrust of map(:foo).

That's a pretty yucky option.  I use map all the time, and I expect to
get an array back.  Adding to_a everywhere is not cool.

Lazy evaluation is cool, though.  But it would need a thorough
reconsideration of the language.  And considering that Ruby is doing
pretty well without it...
Posted by David A. Black (Guest)
on 11.11.2009 13:13
(Received via mailing list)
Hi --

On Wed, 11 Nov 2009, Brian Candler wrote:

> one logical conclusion (or extreme viewpoint) is that you could always
> return an Enumerator, even for
>
>   map { |x| x*x }
>
> I wasn't saying that Ruby does anything like this, and it's a tangent to
> the original thrust of map(:foo).

This reminds me of the early 1.9.0 thing, where you could do:

   array = [1,2,3,4]
   enum = array.enum_for(:map, &lambda {|x| x * x })

   enum.next   # 1
   enum.next   # 4

etc. (That's ruby 1.9.0 (2008-03-01 revision 15660)
[i686-darwin9.2.0].) To be honest, when that disappeared, it seemed to
me to do away with a great deal of the usefulness of enumerators.
Given that you can no longer attach a block to an enumerator when you
create the enumerator, I don't think there are any actual use cases
for the fact that map returns an enumerator (or at least very, very
few). You can't do this, for example (following the above example):

   enum.select {|x| x > 1 } # [4, 9, 16]


David

--
The          Ruby training with D. Black, G. Brown, J.McAnally
Compleat     Jan 22-23, 2010, Tampa, FL
Rubyist      http://www.thecompleatrubyist.com

David A. Black/Ruby Power and Light, LLC (http://www.rubypal.com)
Posted by Benoit Daloze (Guest)
on 13.11.2009 17:22
(Received via mailing list)
Hi,

I'm quite conservative or maybe 'wise', seeing this discussion.

In fact, using arr.map(:method, arg1 ... ) is too confusing to me
While the &:method notation is clear, and not that ugly.

And I think it has to be a separation between blocks and arguments; 
block is
another "kind" of argument.
That's why I think we should keep the &:method, and forget about 
:method.
Note also that is not a common use of &:method, because you often need 
to do
ore than one thing with the element. eg:
[1,15,3].map { |e| e.to_s.length }

P.S.:
Why &:method is faster ? (Bad, I wanted to argue &:method is slower but 
I
can't ...)
(requiring and including Benchmark)
irb(main):008:0> realtime { (1..1000000).to_a.map &:to_s  }
=> 0.38111090660095215
irb(main):009:0> realtime { (1..1000000).to_a.map { |e| e.to_s } }
=> 0.445728063583374


2009/11/11 David A. Black <dblack@rubypal.com>
Posted by Roger Pack (rogerdpack)
on 13.11.2009 18:30
> This reminds me of the early 1.9.0 thing, where you could do:
> 
>    array = [1,2,3,4]
>    enum = array.enum_for(:map, &lambda {|x| x * x })
> 
>    enum.next   # 1
>    enum.next   # 4

It is surprising that currently it appears to *ignore* the given block.

>> b = File.open('file', 'r').lines{|line| lines * 2}
>> b.next
=> "xx\n"
>>> b.next
=> "xx\n"

Is this expected?
-r
Posted by Roger Pack (rogerdpack)
on 13.11.2009 19:42
>This reminds me of the early 1.9.0 thing, where you could do:
>
>    array = [1,2,3,4]
>    enum = array.enum_for(:map, &lambda {|x| x * x })
> 
>    enum.next   # 1
>    enum.next   # 4


Appears somewhat possible...

class Enumerator
  def filter(&blk)
    self.class.new do |y|
      each do |*input|
        out = blk.call(*input)
        y << out
        # or y << out if out
      end
    end
  end
end

>> array = [1,2,3,4]
>> enum = array.enum_for(:map).filter {|x| x * x }
>> enum.next
=> 1
>> enum.next
=> 4

>> enum.select {|x| x > 1 } # [4, 9, 16]
=> [4, 9, 16]

[Thanks to Brian Candler for the snippet--though perhaps it could be 
improved...]

This should be the default, though--I don't know why blocks are ignored 
currently...

-r

http://www.ruby-forum.com/topic/169844#new