[Feature] Symbol to_proc coercions: Enumerable#map can take several symbols

59 views
Skip to first unread message

Alberto Almagro

unread,
May 11, 2018, 1:10:12 PM5/11/18
to Ruby on Rails: Core
These days I have been comparing records in my daily job lots of times, which made me think about a better way to retrieve and compare them. When I want to navigate through several relations in a collection I often see myself writing code like the following:

Given orders as a collection of Order:
> orders.map(&:order_option).map(&:item).map(&:title)
=> ['Foo', 'Bar', 'Baz']

That is, chaining maps with Symbol to Proc coercions one after each other. Sharing my thoughts with my company's architect we came up with the alternative:
> orders.map { |order| order&.order_option&.item&.title }

But we agreed that the notation was awful and didn't improve what we had before. With this, I proposed what I think it is more like what we would expect Ruby to have. I would like to add a notation similar to the one we can find at Array#dig or Hash#dig in the following manner:
> orders.map(&:order_option, &:item, &:title)
The method doesn't necessarily need to be named map or collect, we can agree on a different name for it if you want. Please share your thoughts with me. If you like this, I would be very happy to write a PR to include it in Rails.

Cheers,
Alberto Almagro

Rafael Mendonça França

unread,
May 11, 2018, 2:01:27 PM5/11/18
to rubyonra...@googlegroups.com
If you think that method would be useful there is no reason why it should be Rails specific. Please send a feature request to the Ruby issue tracker here https://bugs.ruby-lang.org/.

Rafael França
--
You received this message because you are subscribed to the Google Groups "Ruby on Rails: Core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rubyonrails-co...@googlegroups.com.
To post to this group, send email to rubyonra...@googlegroups.com.
Visit this group at https://groups.google.com/group/rubyonrails-core.
For more options, visit https://groups.google.com/d/optout.

Alberto Almagro

unread,
May 11, 2018, 2:20:03 PM5/11/18
to Ruby on Rails: Core
Hi Rafael,

I'm glad to see you here. The reason I see its place here at first is because I think its use case fits better with the framework than with the language. When thinking on the Ruby language I would expect more to deal with collections which contain objets like String, Integer, and the likes, while in Ruby on Rails you are much likely to have related collections because of Active Record navigations that require this kind of map chaining.

Cheers,
Alberto Almagro

Anthony Bailey

unread,
May 11, 2018, 4:10:49 PM5/11/18
to rubyonra...@googlegroups.com
On reading my first thought was "can you do that by defining to_proc on Array".

A brief play suggests one can:

class Array
  def to_proc
    Proc.new { |arg| self.inject(arg) { |memo, f| f.to_proc.call(memo) } }
  end
end

Example using primitives:

% [1, 2, 3].map &[:succ, :odd?, :to_s, :length]
=> [5, 4, 5]

Your example would I guess become

% orders.map &[:order_option, :item, :title]

Seems OK?

 --Anthony.

Alberto Almagro

unread,
May 11, 2018, 5:57:31 PM5/11/18
to Ruby on Rails: Core
Hi Anthony,

thanks for your response. Great that you mention this. Yes I have seen other Rubyists on the internet proposing a similar solution, but for me it feels like a workaround, which could work in some cases, but it's not ideal. My main objection for that is that it implies defining to_proc, not only for Array, but for Enumerable, and you may need to define the method for other purposes. For example, I have also seen other people defining Array#to_proc using the first position of the provided array to hold the method that will be sent to the collection object and the remaining positions for the attributes that would be then passed to the method.

In the other hand, having the ability to pass more than one Symbol to the map method only implies extending that method capabilities, thus limiting the scope of the change to the method that we want to extend.

Cheers,
Alberto Almagro

Matt Jones

unread,
May 16, 2018, 9:45:41 AM5/16/18
to rubyonra...@googlegroups.com
On May 11, 2018, at 1:10 PM, Alberto Almagro <alberto...@gmail.com> wrote:

These days I have been comparing records in my daily job lots of times, which made me think about a better way to retrieve and compare them. When I want to navigate through several relations in a collection I often see myself writing code like the following:

Given orders as a collection of Order:
> orders.map(&:order_option).map(&:item).map(&:title)
=> ['Foo', 'Bar', 'Baz']

That is, chaining maps with Symbol to Proc coercions one after each other. Sharing my thoughts with my company's architect we came up with the alternative:
> orders.map { |order| order&.order_option&.item&.title }
Very small nitpick: the code above (which tolerates `nil`) isn’t equivalent to the chained map (which doesn’t). But anyways...


But we agreed that the notation was awful and didn't improve what we had before. With this, I proposed what I think it is more like what we would expect Ruby to have. I would like to add a notation similar to the one we can find at Array#dig or Hash#dig in the following manner:
> orders.map(&:order_option, &:item, &:title)
The method doesn't necessarily need to be named map or collect, we can agree on a different name for it if you want. Please share your thoughts with me. If you like this, I would be very happy to write a PR to include it in Rails.

This reminds me of the Elixir function `get_in` and the associated functions in `Elixir.Access`. I’m not sure if any of the existing methods would make sense to extend with the behavior, though:

* `dig` is called on a collection and returns one element with many levels of nesting
* `pluck` is called on a collection and returns a collection, but only at one level of nesting
* the proposed function is called on a collection and returns a collection, with many levels of nesting

Neither function can guarantee that its arguments are scalars (or even that they aren’t Procs, for that matter) so extending them is tricky. Probably better to pick a new name.

You might also consider “Proc#*” from Facets:


I haven’t tried it, but in principle this should work if the operator precedence goes correctly:

    orders.map(&:order_options * &:item * &:title)

—Matt Jones

Abdel Latif

unread,
May 16, 2018, 12:50:50 PM5/16/18
to rubyonra...@googlegroups.com
Hi, 
I am new to Ruby, can you please give me an example of the orders collection ?
Thanks.

--
You received this message because you are subscribed to the Google Groups "Ruby on Rails: Core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rubyonrails-core+unsubscribe@googlegroups.com.
To post to this group, send email to rubyonrails-core@googlegroups.com.

Alberto Almagro

unread,
May 18, 2018, 11:51:01 AM5/18/18
to rubyonra...@googlegroups.com
Hi Matt,

thanks a lot for all your points and suggestions. 

Having thought about naming, I'm also starting to think that picking a new name would be better. At least it would have the same mental effect as when you compare `dig` vs `[ ]` methods, `dig` predisposes you to think on nested access.

Cheers,
Alberto

--
You received this message because you are subscribed to a topic in the Google Groups "Ruby on Rails: Core" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/rubyonrails-core/22K3pcKEZZU/unsubscribe.
To unsubscribe from this group and all its topics, send an email to rubyonrails-core+unsubscribe@googlegroups.com.

Alberto Almagro

unread,
May 18, 2018, 11:54:12 AM5/18/18
to rubyonra...@googlegroups.com
Hi Abdel,

sorry for the confusion, I guess I didn't explain myself properly. `orders` would be a collection composed by instances of Order model. For example, imagine you do `Order.last(5)` to get the last 5 orders. I hope this clarifies the example.

Cheers,
Alberto

--
You received this message because you are subscribed to a topic in the Google Groups "Ruby on Rails: Core" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/rubyonrails-core/22K3pcKEZZU/unsubscribe.
To unsubscribe from this group and all its topics, send an email to rubyonrails-core+unsubscribe@googlegroups.com.

Alberto Almagro

unread,
Jun 19, 2018, 12:47:56 PM6/19/18
to rubyonra...@googlegroups.com
From the answers I got, I guess this isn't a desired feature at the moment.

Anyway, thanks to everyone involved!

To unsubscribe from this group and stop receiving emails from it, send an email to rubyonrails-co...@googlegroups.com.
To post to this group, send email to rubyonra...@googlegroups.com.

--
You received this message because you are subscribed to a topic in the Google Groups "Ruby on Rails: Core" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/rubyonrails-core/22K3pcKEZZU/unsubscribe.
To unsubscribe from this group and all its topics, send an email to rubyonrails-co...@googlegroups.com.
To post to this group, send email to rubyonra...@googlegroups.com.



--
Alberto Almagro
Senior Ruby on Rails engineer | www.albertoalmagro.com
Reply all
Reply to author
Forward
0 new messages