Project

General

Profile

Actions

Feature #5663

closed

Combined map/select method

Added by wycats (Yehuda Katz) almost 13 years ago. Updated over 4 years ago.

Status:
Closed
Target version:
-
[ruby-core:41213]

Description

It is pretty common to want to map over an Enumerable, but only include the elements that match a particular filter. A common idiom is:

enum.map { |i| i + 1 if i.even? }.compact

It is of course also possible to do this with two calls:

enum.select { |i| i.even? }.map { |i| i + 1 }

Both cases are clumsy and require two iterations through the loop. I'd like to propose a combined method:

enum.map_select { |i| i + 1 if i.even? }

The only caveat is that it would be impossible to intentionally return nil here; suggestions welcome. The naming is also a strawman; feel free to propose something better.


Related issues 2 (0 open2 closed)

Related to Ruby master - Feature #13784: Add Enumerable#filter as an alias of Enumerable#selectClosedEregon (Benoit Daloze)Actions
Related to Ruby master - Feature #15323: [PATCH] Proposal: Add Enumerable#filter_mapClosedActions

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) almost 13 years ago

I like the idea but not the name. Maybe something like select_non_nil would be more appropriate. English is not my mother tongue, is there any English way to say this in a shorter way?

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) almost 13 years ago

Or maybe just enum.filter?

Updated by aprescott (Adam Prescott) almost 13 years ago

Instead of looking at it from the map + select approach for a name, what
about the map + compact version? compact_map ?

Updated by wycats (Yehuda Katz) almost 13 years ago

Compact implies two passes, no?

Yehuda Katz
(ph) 718.877.1325

On Tue, Nov 22, 2011 at 1:15 PM, Adam Prescott wrote:

Instead of looking at it from the map + select approach for a name, what
about the map + compact version? compact_map ?

Updated by shugo (Shugo Maeda) almost 13 years ago

  • ruby -v changed from ruby 1.9.3p0 (2011-11-08 revision 33661) [x86_64-darwin11.2.0] to -

Hi,

2011/11/23 Yehuda Katz :

It is pretty common to want to map over an Enumerable, but only include the elements that match a particular filter. A common idiom is:

enum.map { |i| i + 1 if i.even? }.compact

It is of course also possible to do this with two calls:

enum.select { |i| i.even? }.map { |i| i + 1 }

Both cases are clumsy and require two iterations through the loop. I'd like to propose a combined method:

enum.map_select { |i| i + 1 if i.even? }

The only caveat is that it would be impossible to intentionally return nil here; suggestions welcome. The naming is also a strawman; feel free to propose something better.

How about to add list comprehensions or Scala's for expressions instead?
For example, enum.select { |i| i.even? }.map { |i| i + 1 } can be
written as follows:

[ i + 1 for i in enum if i.even? ]

The syntax of list comprehensions needs more considerations.

One benefit is that nested maps can be flattened by list comprehensions.
For example, the following code:

pyths = [ [x, y, z] for z in [1..Float::INFINITY].defer
x in [1..z].defer
y in [x..z].defer
if x2 + y2 == z**2 ]
p pyths.take(3)

is equivalent to the following code:

pyths = (1..Float::INFINITY).defer.flat_map {|z|
(1..z).defer.flat_map {|x|
(x..z).defer.select {|y|
x2 + y2 == z**2
}.map {|y|
[x, y, z]
}
}
}
p pyths.take(3)

Enumerable#defer is proposed in Feature #4890.

BTW, now Ruby has map and reduce as aliases of collect and inject,
but not filter as an alias of select. Why not?

--
Shugo Maeda

Actions #6

Updated by nahi (Hiroshi Nakamura) almost 13 years ago

  • Tracker changed from Bug to Feature

Bug -> Feature

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) almost 13 years ago

Hi Shugo, I also like the idea of supporting list comprehensions like several other languages currently do.

Updated by wycats (Yehuda Katz) almost 13 years ago

I am nervous about list comprehensions because in almost all cases, when
you do something in Ruby, you do it by invoking a named method on an
object. It is clear by looking at a piece of code which named method will
be invoked. In Ruby 1.9, it is even trivial to learn the exact source
location of such a named method.

In contrast, list comprehensions introduce new syntax that invokes some
invisible protocol; understanding which methods are involved requires
figuring out where to look in the documentation.

Python uses protocols like this for everything, so list comprehensions
fit in well there. The only Ruby case I can think of that works like this
is the much maligned for/in syntax, which invokes #each under the hood.
Most people I know find this strange, I suspect because of its
inconsistency with the very strong rule that if a method is directly
invoked by some syntax, you can see it.

If you want to see where this protocol-oriented path leads us, check out
http://docs.python.org/reference/datamodel.html#special-method-names. I
much prefer Ruby's "if you want to do something, invoke a named method"
principle of uniform access[1].

[1] http://en.wikipedia.org/wiki/Uniform_access_principle

Yehuda Katz
(ph) 718.877.1325

On Tue, Nov 22, 2011 at 2:26 PM, Shugo Maeda wrote:

Hi,

2011/11/23 Yehuda Katz :

It is pretty common to want to map over an Enumerable, but only include
the elements that match a particular filter. A common idiom is:

enum.map { |i| i + 1 if i.even? }.compact

It is of course also possible to do this with two calls:

enum.select { |i| i.even? }.map { |i| i + 1 }

Both cases are clumsy and require two iterations through the loop. I'd
like to propose a combined method:

enum.map_select { |i| i + 1 if i.even? }

The only caveat is that it would be impossible to intentionally return
nil here; suggestions welcome. The naming is also a strawman; feel free to
propose something better.

How about to add list comprehensions or Scala's for expressions instead?
For example, enum.select { |i| i.even? }.map { |i| i + 1 } can be
written as follows:

[ i + 1 for i in enum if i.even? ]

The syntax of list comprehensions needs more considerations.

One benefit is that nested maps can be flattened by list comprehensions.
For example, the following code:

pyths = [ [x, y, z] for z in [1..Float::INFINITY].defer
x in [1..z].defer
y in [x..z].defer
if x2 + y2 == z**2 ]
p pyths.take(3)

is equivalent to the following code:

pyths = (1..Float::INFINITY).defer.flat_map {|z|
(1..z).defer.flat_map {|x|
(x..z).defer.select {|y|
x2 + y2 == z**2
}.map {|y|
[x, y, z]
}
}
}
p pyths.take(3)

Enumerable#defer is proposed in Feature #4890.

BTW, now Ruby has map and reduce as aliases of collect and inject,
but not filter as an alias of select. Why not?

--
Shugo Maeda

Updated by alexeymuranov (Alexey Muranov) almost 13 years ago

It seems that in full generality this method needs to accept two blocks: one for selecting and one for mapping, but this would be an unusual syntax.

So how about a lazy #selecting first, which would store a block for selecting inside enum, and to make #map check if a block for selecting is defined, and to use it? Maybe, instead of changing #map, a new variant of #map can be created that would take lazy operations into account. (For the name, i would propose #partial_map or #map_partially ( http://en.wikipedia.org/wiki/Partial_function ), or #map_selected.) Then the code would look like this:

enum.selecting { |i| i.even? }.map_selected { |i| i + 1 }

and only one loop will be needed.

I've read about gems defining lazy methods for Enumerable, but i do not remember if any of them is doing exactly this.
Update Here is a link to a description of Lazing project:
http://blog.gregspurrier.com/articles/lazing-lazy-enumerable-methods-for-ruby-1-9

-Alexey.


Update If the goal is not to solve this in full generality, but to optimize for one use case, then what about #map_and_compact ? I think it is ok to have relatively long names for methods that solve relatively restricted problems.

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) almost 13 years ago

Alexey, what about thread safety in this case? Would the selecting return a new object?

Updated by alexeymuranov (Alexey Muranov) almost 13 years ago

Rodrigo Rosenfeld Rosas wrote:

Alexey, what about thread safety in this case? Would the selecting return a new object?

I do not know much about threads. I think #selecting can return a new object, and #selecting! can add lazy selection to the object on which it is called. Those more knowledgable please comment with something wiser.

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) almost 13 years ago

I was just suggesting that maybe avoiding laziness for this case would be less of a trouble since we wouldn't need to worry about thread safety...

Actions #13

Updated by nobu (Nobuyoshi Nakada) almost 13 years ago

  • Category set to lib
  • Target version set to 2.0.0

=begin
What about:
(1..10).grep(->(i){i.even?}){|i|i+1}
or
(1..10).grep(:even?.to_proc){|i|i+1}
=end

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) almost 13 years ago

Nobuyoshi, wouldn't &:even? be equivalent to :even?.to_proc? I just find that the example reads better this way ;)

(1..10).grep(&:even?){|i|i+1}

But anyway, I don't find that "grep" is self-explanatory, although I like the idea of passing two procs.

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) almost 13 years ago

How can I find the syntax for highlighting the code in this Redmine instance?

Updated by nobu (Nobuyoshi Nakada) almost 13 years ago

Rodrigo Rosenfeld Rosas wrote:

Nobuyoshi, wouldn't &:even? be equivalent to :even?.to_proc? I just find that the example reads better this way ;)

They are different.
&expr calls #to_proc method on the result of expr, to achieve a Proc object.

(1..10).grep(&:even?){|i|i+1}

It's a syntax error.

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) almost 13 years ago

Em 25-11-2011 01:00, Nobuyoshi Nakada escreveu:

Issue #5663 has been updated by Nobuyoshi Nakada.

Rodrigo Rosenfeld Rosas wrote:

Nobuyoshi, wouldn't&:even? be equivalent to :even?.to_proc? I just find that the example reads better this way ;)
They are different.
&expr calls #to_proc method on the result of expr, to achieve a Proc object.

(1..10).grep(&:even?){|i|i+1}
It's a syntax error.

Wow! Thanks! This is unexpected to me. I didn't know about the difference.

Anyway, I still didn't understand why (1..10).grep(&:even?) works, but
(1..10).grep(&:even?){|i| i+1} not.

Actually, I didn't understand your explanation. You said that &expr
calls #to_proc on the result of expr. What is the expr on each example?
Is there any place I could further read about those differences?

Thank you, Rodrigo.

Updated by nobu (Nobuyoshi Nakada) almost 13 years ago

Hi,

(11/11/28 10:05), Rodrigo Rosenfeld Rosas wrote:

&expr calls #to_proc method on the result of expr, to achieve a Proc object.

achieve a Proc object, and convert it into a block.

Anyway, I still didn't understand why (1..10).grep(&:even?) works, but (1..10).grep(&:even?){|i| i+1} not.

The former is equivalent to

(1..10).grep {|i| i.even?}

You can't pass two or more blocks to one method call.

Actually, I didn't understand your explanation. You said that &expr calls #to_proc on the result of expr. What is the expr on each example? Is there any place I could further read about those differences?

expr is :even? symbol literal here.

--
Nobu Nakada

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) almost 13 years ago

Em 27-11-2011 23:59, Nobuyoshi Nakada escreveu:

...

Anyway, I still didn't understand why (1..10).grep(&:even?) works, but (1..10).grep(&:even?){|i| i+1} not.
The former is equivalent to

(1..10).grep {|i| i.even?}

You can't pass two or more blocks to one method call.

Ah, ok, thanks. So this is actually the explanation why the other
example won't work.

Maybe if Ruby accepted blocks as parameters, like Groovy does and as
José Valim has proposed in a talk at RubyConf Brazil, that could be
achievable as

(1..10).grep&:even?, {|i| i+1}

Has this feature already been discussed?

Updated by alexeymuranov (Alexey Muranov) almost 13 years ago

Rodrigo Rosenfeld Rosas wrote:

Em 25-11-2011 01:00, Nobuyoshi Nakada escreveu:

Issue #5663 has been updated by Nobuyoshi Nakada.

Rodrigo Rosenfeld Rosas wrote:

Nobuyoshi, wouldn't&:even? be equivalent to :even?.to_proc? I just find that the example reads better this way ;)
They are different.
&expr calls #to_proc method on the result of expr, to achieve a Proc object.

(1..10).grep(&:even?){|i|i+1}
It's a syntax error.

Wow! Thanks! This is unexpected to me. I didn't know about the difference.

Anyway, I still didn't understand why (1..10).grep(&:even?) works, but
(1..10).grep(&:even?){|i| i+1} not.

Actually, I didn't understand your explanation. You said that &expr
calls #to_proc on the result of expr. What is the expr on each example?
Is there any place I could further read about those differences?

Thank you, Rodrigo.

I am not a specialist, but it seems that you can only use ampersand in a def with the last parameter, and in such case this last parameter becomes the name of a block passed to the method. Similarly, when you call a method, you can either supply a block, or pass a proc as a block by adding it as the last argument prefixed with ampersand, but not both, because you cannot pass two blocks to a method (but you can pass multiple procs). So basically only one ampersand is allowed, and only if you do not supply a block.

Here is a link about blocks and procs, but it does not explain this detail:
http://www.robertsosinski.com/2008/12/21/understanding-ruby-blocks-procs-and-lambdas/

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) almost 13 years ago

Hi Alexey,

Yes, I know about this. What José Valim suggested (well, he actually wondered, since he doesn't believe this could be changed in Ruby due to the large existent code base) was the removal of the block concept. From what I've understood, the block syntax would actually create a proc (or a lambda) that would be passed as a parameter. And I also find this less confusing, with the similar callable approach taken by C, JavaScript, Python and most languages out there I guess.

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) almost 13 years ago

Em 30-11-2011 05:41, Ondřej Bílka escreveu:

On Tue, Nov 29, 2011 at 08:32:01PM +0900, Rodrigo Rosenfeld Rosas wrote:

... What José Valim suggested (well, he actually wondered, since he doesn't believe this could be changed in Ruby due to the large existent code base) was the removal of the block concept. From what I've understood, the block syntax would actually create a proc (or a lambda) that would be passed as a parameter. And I also find this less confusing, with the similar callable approach taken by C, JavaScript, Python and most languages out there I guess.
It already is. For example
def foo(&p)
p.call
end

This is not what I'm talking about. The other callable languages
wouldn't have special treatments for blocks like block_given? or yield,
for instance.

I'm talking about "method(some_param){some_block 'here'}" being exactly
equal to "method some_param, lambda {some_block 'here'}" (or using a
proc instead of a lambda, I'm not sure about which one would be a better
fit).

Actions #24

Updated by kernigh (George Koehler) almost 13 years ago

=begin
Nobuyoshi Nakada wrote:

What about:
(1..10).grep(->(i){i.even?}){|i|i+1}
or
(1..10).grep(:even?.to_proc){|i|i+1}

This looks smart. One can also write it as
(1..10).grep((proc &:even?), &:succ)
but that might be too ugly.

I forgot about #grep, so I would have used #flat_map:
(1..10).flat_map{|i| i.even?? [i + 1] : []}

I like #flat_map, but it might have a disadvantage: it must allocate several new zero-element and one-element arrays.
=end

Updated by Anonymous almost 13 years ago

It is pretty common to want to map over an Enumerable, but only include the elements that match a particular filter. A common idiom is:

enum.map { |i| i + 1 if i.even? }.compact

It is of course also possible to do this with two calls:

enum.select { |i| i.even? }.map { |i| i + 1 }

Both cases are clumsy and require two iterations through the loop. I'd like to propose a combined method:

+1 for a combined map and select. I find myself reaching for it every
so often and wishing it were there.

Updated by trans (Thomas Sawyer) almost 13 years ago

In Facets it's called #compact_map, but that's only b/c I thought better of monkey patching #compact itself.

I think it would be a slick feature for #compact to take a block.

Updated by trans (Thomas Sawyer) almost 13 years ago

In Facets it's called #compact_map, but that's only b/c I though better of
monkey patching #compact itself.

I think it would be a slick addition for #compact to take a block.

Updated by nobu (Nobuyoshi Nakada) almost 13 years ago

Hi,

(12/02/01 11:18), Thomas Sawyer wrote:

In Facets it's called #compact_map, but that's only b/c I thought better of monkey patching #compact itself.

I think it would be a slick feature for #compact to take a block.

Mere "compact" doesn't feel implying "map".

--
Nobu Nakada

Updated by funny_falcon (Yura Sokolov) almost 13 years ago

I often wish to have methods, which likes to inject but do use return value of block for next iteration:

class Enumerable
  def accum(container)
    each{|args| yield container, args}
    container
  end
end

Then I could use it in following ways:
instead of

enum.inject({}){|h, k| h[k] = true; h}
enum.map{|ar| ar.select{|i| i.usefull?}}.flatten(1)
enum.map{|i| i + 1 if i.even?}.compact

I could

enum.accum({}){|h,k| h[k] = true}
enum.accum([]){|res, ar| ar.each{|i| res << i if i.usefull?}}
enum.accum([]){|res, i| res << i + 1 if i.even?}

Well, it is shorter only in case of inject, but I still will prefer such method, cause I don't stress GC with many short living arrays.
And maybe two common container-s could have separate methods:

class Enumerable
  def accum_hash(&block)
    accum({}, &block)
  end
  def accum_ar(&block)
    accum([], &block)
  end
end

enum.accum_hash{|h,k| h[k] = true}
enum.accum_array{|res, ar| ar.each{|i| res << i if i.usefull?}}
enum.accum_array{|res, i| res << i + 1 if i.even?}

Updated by nobu (Nobuyoshi Nakada) almost 13 years ago

Yura Sokolov wrote:

I often wish to have methods, which likes to inject but do use return value of block for next iteration:

Try each_with_object.

Updated by trans (Thomas Sawyer) almost 13 years ago

@Nakada Why not?

"Returns a copy of self with all nil elements removed. If block is given, returns the yield of each member sans those that yield nil."

[1,2,3,4].compact{ |x| x.even? ? x*2 : nil } #=> [4,16]

Seems clear enough to me. Besides, I think we'd be quite hard pressed to find some other term that implies both "map" and "compact".

Updated by mame (Yusuke Endoh) over 12 years ago

  • Status changed from Open to Rejected

In #708, Matz approved a related ticket #4890.
Let's discuss there.

--
Yusuke Endoh

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) over 12 years ago

I don't think #4890 removes the need for this ticket. This is about API, not implementation. Even if we can use lazy maps/collects to implement this API, I still think it should exist.

Updated by mame (Yusuke Endoh) over 12 years ago

  • Status changed from Rejected to Assigned
  • Assignee set to matz (Yukihiro Matsumoto)

Okay reopened.

Do you mean we need map_select as a shorthand?

--
Yusuke Endoh

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) over 12 years ago

Yes, Yusuke, exactly. Thanks for reopening.

Updated by trans (Thomas Sawyer) over 12 years ago

One of the problems with this method is that in becomes impossible to get nil or false as a valid selection/non-compaction.

Couple thoughts on handling this. For starters maybe #compact should be able to take an argument, e.g.

[1,2,3,1].compact(1) => [2,3]

(This in itself seems pretty useful, regardless.)

Then a #compact_map could do the same. If we need to be able to get nil/false results we could then use an alternative.

foo = Object.new

[0,1,-1].compact_map(foo){ |e| e.zero? ? foo : (e > 0 ? true : false) } #=> [true,false]

(Here's a case where some sort of global null instance might be useful, in place of foo.)

But then again, a delayed enumeration is about as concise, and maybe a bit easier to read.

[0,1,-1].denum.select{ |e| !e.zero? }.map{ |e| e > 0 ? true : false }.to_a #=> [true,false]

Something to consider.

Updated by jballanc (Joshua Ballanco) over 12 years ago

What about using "next" for this purpose? Currently:

(1..10).to_a.map { |i| i % 3 == 0 ? next : i**2 }
=> [1, 4, nil, 16, 25, nil, 49, 64, nil, 100]

Is it so common to use "next" with a return value inside of an Enumerable#map call? Even so, could we special case "next" so that instead of returning nil, it skips the element entirely?

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) over 12 years ago

Thomas, I didn't find your example for justifying a "null" instance good enough. Why not just using "nil" instead of "foo"?

But I liked your suggestion on "compact" allowing an argument defaulting to nil.

Joshua, I like the idea of next inside a map doing the compact thing, but I guess this will be hard to get approval for from Ruby Core team.

It would be a surprising feature in Ruby. Ruby is usually a pretty concise language with very little surprises... I'm not sure if they'll want to introduce such different "next" behavior only for the map/collect method.

Updated by trans (Thomas Sawyer) over 12 years ago

@rodrigo

Your right, I could have used nil, so my example wasn't a particularly good one for what I was trying to demonstrate. Here's a better demonstration:

[0,1,-1,2].compact_map(foo) do |e|
case e
when 0 then nil
when 1 then true
when -1 then false
else foo
end
end
#=> [nil,true,false]

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) over 12 years ago

I supposed that was your point, but I waited for your new example, before talking about this.

The point is that usually we want some examples from the real world for considering some feature.

So, I don't find this example likely to actually happen on real software. Could you please provide some real scenario where having nils in the result would be better than any other values?

Updated by funny_falcon (Yura Sokolov) over 12 years ago

2012/2/1 Nobuyoshi Nakada

Issue #5663 has been updated by Nobuyoshi Nakada.

Yura Sokolov wrote:

I often wish to have methods, which likes to inject but do use return
value of block for next iteration:

Try each_with_object.

Yeah, it is good :) though it have a long name.

Yura

Updated by matz (Yukihiro Matsumoto) over 12 years ago

  • Status changed from Assigned to Feedback

I am OK with the original map_select behavior, but I don't like the name #map_select.
It is combination of -ect family name with map.

I prefer the name #filter, but it might be confused by simple alias of select.
Any idea?

Matz.

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) over 12 years ago

I actually prefer map_select as it is self-explanatory. There isn't a single word that is able to clearly provide the description for what it does.

Updated by nobu (Nobuyoshi Nakada) over 12 years ago

Hi,

(12/02/01 18:14), Юрий Соколов wrote:

Try each_with_object.

Yeah, it is good :) though it have a long name. It's pity that 1.9.3 hadn't
it.

It's long indeed, but 1.9.3 had it.

$ ./ruby -v -e 'p((1..3).each_with_object({}){|x,a|a[x]=x+1})'
ruby 1.9.3p0 (2011-10-30 revision 33569) [x86_64-darwin11.3.0]
{1=>2, 2=>3, 3=>4}

--
Nobu Nakada

Updated by trans (Thomas Sawyer) over 12 years ago

Maybe #select_yield, since that what it is doing.

Which reminds me, is there an equivalent #find method to this (e.g. #find_yield), that returns the yield result instead of the element?

Updated by shugo (Shugo Maeda) over 12 years ago

matz (Yukihiro Matsumoto) wrote:

I am OK with the original map_select behavior, but I don't like the name #map_select.
It is combination of -ect family name with map.

I prefer the name #filter, but it might be confused by simple alias of select.
Any idea?

I think filter should be a simple alias of select.

Otherwise, we can't complain about Scala's collect:p

How about filter_map?

I've found that Scheme has filter-map.
From SRFI-1:

filter-map f clist1 clist2 ... -> list
Like map, but only true values are saved.
(filter-map (lambda (x) (and (number? x) (* x x))) '(a 1 b 3 c 7))
=> (1 9 49)
The dynamic order in which the various applications of f are made is not specified.
At least one of the list arguments must be finite.

Updated by alexeymuranov (Alexey Muranov) over 12 years ago

=begin
By the way, is anybody using the (({nil})) returned by (({#compact!})) ?

enum.map { |i| i + 1 if i.even? }.compact

makes un unnecessary copy, but i cannot use

enum.map { |i| i + 1 if i.even? }.compact!

because it will return (({nil})) if no changes were made.

Wouldn't it be better if (({#compact})) and (({#compact!})) returned the same thing?
=end

Updated by rosenfeld (Rodrigo Rosenfeld Rosas) over 12 years ago

Em 19-06-2012 17:32, Roger Pack escreveu:

How about #map_some as the name?
#map_if ?

I loved this one :)

Updated by drKreso (Kresimir Bojcic) about 12 years ago

+1 I also like filter_map

(My other favourite would be map_selected which I think is more expressive for non-lisp-brainwashed)

Updated by trans (Thomas Sawyer) about 12 years ago

Instead of thinking of it as a special type of #map, I suggest thinking about it as a special type of #select. The reason is that we could also use the same type of behavior for #find. Which is why I suggest #select_yield and #find_yield (or #yield_select and #yield_find). The names of both of these need to be considered together.

Updated by Anonymous about 12 years ago

On Tue, Aug 14, 2012 at 1:25 PM, trans (Thomas Sawyer)
wrote:

Instead of thinking of it as a special type of #map, I suggest thinking about it as a special type of #select. The reason is that we could also use the same type of behavior for #find. Which is why I suggest #select_yield and #find_yield (or #yield_select and #yield_find). The names of both of these need to be considered together.

To me it looks like a not-so-special type of inject.

How would map_select differ from inject, please?

Ciao,
Sheldon.

Updated by yhara (Yutaka HARA) about 12 years ago

  • Target version changed from 2.0.0 to 2.6

Updated by shugo (Shugo Maeda) over 10 years ago

  • Status changed from Feedback to Assigned

Yukihiro Matsumoto wrote:

I am OK with the original map_select behavior, but I don't like the name #map_select.
It is combination of -ect family name with map.

I prefer the name #filter, but it might be confused by simple alias of select.
Any idea?

What do you think of the following counter proposals?

  • select_yield
  • filter_map
  • map_if

Updated by sawa (Tsuyoshi Sawada) over 10 years ago

I would like to propose:

  • partial_map

Updated by sawa (Tsuyoshi Sawada) over 10 years ago

Also, regarding Yehuda Katz's concern:

The only caveat is that it would be impossible to intentionally return nil here; suggestions welcome.

I would like to propose that the method takes an optional argument that determines what element is to be removed. By default, this is nil.

[1, 2, 3, 4].partial_map{|i| i + 4 if i.even?} # => [6, 8]

s = "abc"
[0, 1, 2, 3, 4].partial_map(:ignore){|i| i.even? ? s[i] : :ignore} # => ["a", "c", nil]

Updated by sawa (Tsuyoshi Sawada) over 10 years ago

I came to think that, if this feature is to be added, then are other methods to be considered: flat_map and to_h in addition to map. I think it should be either the case that the corresponding counterpart to all of these methods should be added, or none.

Updated by epidemian (Demian Ferreiro) over 10 years ago

I found this issue looking for a single-pass alternative to .map{...}.compact.

The former example uses { |i| i + 1 if i.even? }, which reads almost like a comprehension and has a clear separation between the condition and the transformation. But sometimes it's not so easy to achieve such separation:

# Generate an array of integers from untrusted input.
['', '42', 'nope', :not_even_int_convertible].compact_map { |x| Integer(x) rescue nil } # => [42]

BTW, i think that compact_map sounds quite natural for what this method does, but it could also be an extension to compact as others have mentioned:

['', '42', 'nope', :not_even_int_convertible].compact { |x| Integer(x) rescue nil } # => [42]

As long as this is considered an alternative to map + concat, i think it makes sense not to worry about the edge case of wanting to preserve the nils, as the purpose of compact is to wipe them out. For those cases a combination of select and map can be used, or grep with a proc as its "pattern" argument to avoid having two iterations.

Updated by rokob (Andrew Ledvina) over 9 years ago

The only caveat is that it would be impossible to intentionally return nil here

I don't see why you need that, just have the block return a pair (keep_if_true, mapped_value). I also would second the name filter_map. Ignoring all edge-cases you could just do something like:

module Array
  def filter_map
    result = []
    self.each do |elem|
      keep, value = yield elem
      result << value if keep
    end
    result
  end
end

> [1,32,9,33,2,13].filter_map{|v| [v < 20, v%2==1 ? v*2 : nil]]}
 => [2, 18, nil, 26]

Although to be fair, filter_map is just reduce with a more complicated block

> [1,32,9,33,2,13].reduce([]){|acc, v| v<20 ? v%2==1 ? acc << v*2 : acc << nil : nil; acc}
 => [2, 18, nil, 26]
Actions #60

Updated by shugo (Shugo Maeda) about 7 years ago

  • Related to Feature #13784: Add Enumerable#filter as an alias of Enumerable#select added
Actions #61

Updated by naruse (Yui NARUSE) almost 7 years ago

  • Target version deleted (2.6)
Actions #62

Updated by shugo (Shugo Maeda) over 5 years ago

  • Related to Feature #15323: [PATCH] Proposal: Add Enumerable#filter_map added

Updated by sawa (Tsuyoshi Sawada) over 4 years ago

I think this proposal has been realized under the name Enumerable#filter_map following the duplicate proposal #15323. So it should be closed.

Actions #64

Updated by nobu (Nobuyoshi Nakada) over 4 years ago

  • Description updated (diff)
  • Status changed from Assigned to Closed
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0