144

You're probably familiar with the following Ruby shorthand (a is an array):

a.map(&:method)

For example, try the following in irb:

>> a=[:a, 'a', 1, 1.0]
=> [:a, "a", 1, 1.0]
>> a.map(&:class)
=> [Symbol, String, Fixnum, Float]

The syntax a.map(&:class) is a shorthand for a.map {|x| x.class}.

Read more about this syntax in "What does map(&:name) mean in Ruby?".

Through the syntax &:class, you're making a method call class for each array element.

My question is: can you supply arguments to the method call? And if so, how?

For example, how do you convert the following syntax

a = [1,3,5,7,9]
a.map {|x| x + 2}

to the &: syntax?

I'm not suggesting that the &: syntax is better. I'm merely interested in the mechanics of using the &: syntax with arguments.

I assume you know that + is a method on Integer class. You can try the following in irb:

>> a=1
=> 1
>> a+(1)
=> 2
>> a.send(:+, 1)
=> 2
Community
  • 1
  • 1
Zack Xu
  • 11,505
  • 9
  • 70
  • 78

9 Answers9

158

You can create a simple patch on Symbol like this:

class Symbol
  def with(*args, &block)
    ->(caller, *rest) { caller.send(self, *rest, *args, &block) }
  end
end

Which will enable you to do not only this:

a = [1,3,5,7,9]
a.map(&:+.with(2))
# => [3, 5, 7, 9, 11] 

But also a lot of other cool stuff, like passing multiple parameters:

arr = ["abc", "babc", "great", "fruit"]
arr.map(&:center.with(20, '*'))
# => ["********abc*********", "********babc********", "*******great********", "*******fruit********"]
arr.map(&:[].with(1, 3))
# => ["bc", "abc", "rea", "rui"]
arr.map(&:[].with(/a(.*)/))
# => ["abc", "abc", "at", nil] 
arr.map(&:[].with(/a(.*)/, 1))
# => ["bc", "bc", "t", nil] 

And even work with inject, which passes two arguments to the block:

%w(abecd ab cd).inject(&:gsub.with('cde'))
# => "cdeeecde" 

Or something super cool as passing [shorthand] blocks to the shorthand block:

[['0', '1'], ['2', '3']].map(&:map.with(&:to_i))
# => [[0, 1], [2, 3]]
[%w(a b), %w(c d)].map(&:inject.with(&:+))
# => ["ab", "cd"] 
[(1..5), (6..10)].map(&:map.with(&:*.with(2)))
# => [[2, 4, 6, 8, 10], [12, 14, 16, 18, 20]] 

Here is a conversation I had with @ArupRakshit explaining it further:
Can you supply arguments to the map(&:method) syntax in Ruby?


As @amcaplan suggested in the comment below, you could create a shorter syntax, if you rename the with method to call. In this case, ruby has a built in shortcut for this special method .().

So you could use the above like this:

class Symbol
  def call(*args, &block)
    ->(caller, *rest) { caller.send(self, *rest, *args, &block) }
  end
end

a = [1,3,5,7,9]
a.map(&:+.(2))
# => [3, 5, 7, 9, 11] 

[(1..5), (6..10)].map(&:map.(&:*.(2)))
# => [[2, 4, 6, 8, 10], [12, 14, 16, 18, 20]] 

Here is a version using Refinements (which is less hacky than globally monkey patching Symbol):

module AmpWithArguments

  refine Symbol do
    def call(*args, &block)
      ->(caller, *rest) { caller.send(self, *rest, *args, &block) }
    end
  end

end

using AmpWithArguments

a = [1,3,5,7,9]
a.map(&:+.(2))
# => [3, 5, 7, 9, 11] 

[(1..5), (6..10)].map(&:map.(&:*.(2)))
# => [[2, 4, 6, 8, 10], [12, 14, 16, 18, 20]] 
Uri Agassi
  • 36,848
  • 14
  • 76
  • 93
  • Anyone considering doing this should think twice. At first glance this seems cool and simple but it isn't just patching the `Symbol` class, it's monkey patching the a core class of the ruby standard library. This affects all other code in the running application which loads this "patch". http://devblog.avdi.org/2008/02/23/why-monkeypatching-is-destroying-ruby/ – rudolph9 May 06 '15 at 12:34
  • 1
    @rudolph9 - "monkey patching" usually means _overwriting_ existing implementation. This solution is more akin to _extending_ a class, like [C#'s extension methods](https://msdn.microsoft.com/en-us/library/bb383977.aspx), and is used heavily in ruby, like Rails's `1.minute.ago`. There is no risk of affecting any other code, as it does not change any existing behavior. It itself is not brittle, since it does not rely on any internal implementation which might change. – Uri Agassi May 06 '15 at 12:51
  • 11
    @UriAgassi Just because a lot of libraries do this does not make it a good practice. While `Symbol#with` may not exist in the core library and defining that method is less destructive than redefining an existing method it is still changing (i.e. overwriting) the implementation of core class of the ruby library. The practice should be done very sparingly and with great caution. \n\n Please consider inheriting from the existing class and modifying the newly created class. This generally achieves comparable results without the negative side effects of changing core ruby classes. – rudolph9 May 06 '15 at 13:27
  • "Monkey patching" probably isn't the right word to describe this though. While technically it may fall under that category, the practical use of the word ["monkey patching"] usually refers to something else. – rudolph9 May 06 '15 at 13:32
  • 3
    @rudolph9 - I beg to differ - the definition of "overwrite" is to write _over_ something, which means that a code that was written is not longer available, and this is clearly not the case. Regarding your suggestion to inherit `Symbol` class - it is not trivial (if even possible), since it is such a core class (it has no `new` method, for example), and its usage will be cumbersome (if even possible), which will defeat the purpose of the enhancement... if you can show an implementation which uses that, and achieves comparable results - please share! – Uri Agassi May 06 '15 at 13:39
  • @rudolph9 - you talked about "monkey patching" - not me – Uri Agassi May 06 '15 at 13:39
  • @UriAgassi my suggestions were speaking in general (not this specific use case), obviously it's very powerful to modify an existing class but it can also be very destructive and should be used with caution. – rudolph9 May 06 '15 at 13:53
  • @rudolph9 - "Anyone considering doing _this_ should think twice" - doesn't sound like generally speaking, and neither does your downvote :-P. I would argue that this is one of these cases, where caution was used, and the benefit is much greater to the (negligible) risk, at least for those who choose to use it. – Uri Agassi May 06 '15 at 14:01
  • @UriAgassi You could consider using rkon solution, it's generally applicable and only effects your code rather than all running code in the application. – rudolph9 May 06 '15 at 14:04
  • @rudolph9 - let me reiterate - my solution _does not_ affect all running code in the application. – Uri Agassi May 06 '15 at 15:02
  • @UriAgassi Calling `class Symbol; def with(*args, &block); ->(caller, *rest) { caller.send(self, *rest, *args, &block) }; end; end` will cause `Symbol#with` to be a method on every symbol created, including symbols created by gems loaded into the application. – rudolph9 May 06 '15 at 15:22
  • @rudolph9 - this is technically true - but it has no impact whatsoever on the code... – Uri Agassi May 06 '15 at 15:43
  • @UriAgassi what if one of those gem you're loading also define `Symbol#with` with a different behavior? – rudolph9 May 06 '15 at 15:57
  • @rudolph9 - the same thing that will happen if some other gem will use the exact same class name and namespace as yours... – Uri Agassi May 06 '15 at 16:15
  • @UriAgassi true but Class namespace conflicts are generally much more obvious. – rudolph9 May 06 '15 at 17:42
  • 2
    @UriAgassi Please note; I think this solution is _very slick_. Although I consider it dangerous to patch the ruby core (any gem for that matter), I think it would be awesome if your solution got incorporated into the ruby core! – rudolph9 May 06 '15 at 17:48
  • 3
    I like this solution, but I think you can have even more fun with it. Instead of defining a `with` method, define `call`. Then you can do things like `a.map(&:+.(2))` since `object.()` uses the `#call` method. And while you're at it, you can write fun things like `:+.(2).(3) #=> 5` - feels sort of LISPy, no? – amcaplan Aug 13 '15 at 09:27
  • @amcaplan - wow, cool! I wasn't aware of this... it looks and reads great. My only concern is that it assumes that `Symbol` is used only in this context. In other contexts it might confuse, since it gives no context of its own. That said - I still like it! – Uri Agassi Aug 13 '15 at 16:13
  • Yeah @UriAgassi that's true - monkey patching these things can be a bit dangerous. But once you're already reopening `Symbol`... – amcaplan Aug 19 '15 at 14:30
  • This should never be done, and I strongly appreciate other programming ecosystems that have decided hijacking core objects is prohibited. I feel sad seeing so many people think this is a great idea in Ruby, because I want Ruby to do better and be modern. – Andy Ray Mar 20 '19 at 17:53
  • should be `public_send` instead of `send` – localhostdotdev Apr 11 '19 at 19:37
  • Personally, I would argue that "monkey patching" includes extending core classes, as well as overwriting them, since it risks collision with other, similar extensions in the wild. In any event, my real purpose in commenting is simply to note that for modern Ruby (2.7+) users, [`Refinements`](https://ruby-doc.org/core-3.1.1/doc/syntax/refinements_rdoc.html) will allow you to accomplish this very cool approach without the dangers of monkey patching. – Damian C. Rossney Apr 07 '22 at 05:00
54

For your example can be done a.map(&2.method(:+)).

Arup-iMac:$ pry
[1] pry(main)> a = [1,3,5,7,9]
=> [1, 3, 5, 7, 9]
[2] pry(main)> a.map(&2.method(:+))
=> [3, 5, 7, 9, 11]
[3] pry(main)> 

Here is how it works :-

[3] pry(main)> 2.method(:+)
=> #<Method: Fixnum#+>
[4] pry(main)> 2.method(:+).to_proc
=> #<Proc:0x000001030cb990 (lambda)>
[5] pry(main)> 2.method(:+).to_proc.call(1)
=> 3

2.method(:+) gives a Method object. Then &, on 2.method(:+), actually a call #to_proc method, which is making it a Proc object. Then follow What do you call the &: operator in Ruby?.

Community
  • 1
  • 1
Arup Rakshit
  • 116,827
  • 30
  • 260
  • 317
  • Clever usage! Does that assume that the method invocation can be applied both ways (i.e. arr[element].method(param) === param.method(arr[element])) or am I confused? – Kostas Rousis May 16 '14 at 13:21
  • @rkon I didn't get your question too. But if you see the `Pry` outputs above, you can get it, how is it working. – Arup Rakshit May 16 '14 at 13:25
  • 7
    @rkon It doesn't work in both ways. It works in this particular case because `+` is commutative. – sawa May 16 '14 at 13:28
  • How can you supply multiple arguments? As in this case: a.map {|x| x.method(1,2,3)} – Zack Xu May 16 '14 at 13:28
  • 1
    that's my point @sawa :) That it makes sense with + but wouldn't for another method or let's say if you wanted to divide each number by X. – Kostas Rousis May 16 '14 at 13:28
  • @ZackXu Please think it closely, you can get the answer. In your case(as per the comment) it can be done also, but if your method takes 3 argument, and input array should be like `[[a,b,c],[d,e,f]..]` – Arup Rakshit May 16 '14 at 13:33
  • "Very clever", sure, but I think this will confuse 99% of developers. `a.map(&2.method(:+))` is "wtf?" code, whereas `a.map { |i| i + 2 }` is obvious. – Tom Lord Mar 05 '21 at 11:07
14

As the post you linked to confirms, a.map(&:class) is not a shorthand for a.map {|x| x.class} but for a.map(&:class.to_proc).

This means that to_proc is called on whatever follows the & operator.

So you could give it directly a Proc instead:

a.map(&(Proc.new {|x| x+2}))

I know that most probably this defeats the purpose of your question but I can't see any other way around it - it's not that you specify which method to be called, you just pass it something that responds to to_proc.

Kostas Rousis
  • 5,918
  • 1
  • 33
  • 38
  • 1
    Also keep in mind you can set procs to local variables and pass them to map. `my_proc = Proc.new{|i| i + 1}`, `[1,2,3,4].map(&my_proc) => [2,3,4,5]` – rudolph9 May 06 '15 at 12:48
11

There is another native option for enumerables which is pretty only for two arguments in my opinion. the class Enumerable has the method with_object which then returns another Enumerable.

So you can call the & operator for a method with each item and the object as arguments.

Example:

a = [1,3,5,7,9]
a.to_enum.with_object(2).map(&:+) # => [3, 5, 7, 9, 11]

In the case you want more arguments you should repeat the proccess but it's ugly in my opinion:

a = [1,3,5,7,9]
a.to_enum.with_object(2).map(&:+).to_enum.with_object(5).map(&:+) # => [8, 10, 12, 14, 16]
Sebastián Palma
  • 32,692
  • 6
  • 40
  • 59
Pedro Augusto
  • 303
  • 3
  • 5
  • 4
    Three things to note with this approach: 1.) it's actually longer (in terms of characters) than just doing `a.map { |n| n + 2 }`, 2.) it's, arguably, less readable, and 3.) it's 5 times slower than running `map()` directly on the array (~19 seconds compared to ~ 3.75 seconds when benchmarked using `10_000_000.times {}`. That being said, I still upvoted because it's definitely a creative solution to the question at hand. :-) – jeffdill2 Nov 25 '20 at 16:26
  • `to_enum.with_object(2)` is equivalent to `each_with_object(2)` – Pere Joan Martorell Jan 21 '22 at 13:21
10

Short answer: No.

Following @rkon's answer, you could also do this:

a = [1,3,5,7,9]
a.map &->(_) { _ + 2 } # => [3, 5, 7, 9, 11]
Agis
  • 32,639
  • 3
  • 73
  • 81
10

if all your method needs as argument is an element from the array, this is probably the simplest way to do it:

def double(x)
  x * 2
end

[1, 2, 3].map(&method(:double))

=> [2, 4, 6]
Camilo Sad
  • 101
  • 1
  • 4
7

Instead of patching core classes yourself, as in the accepted answer, it's shorter and cleaner to use the functionality of the Facets gem:

require 'facets'
a = [1,3,5,7,9]
a.map &:+.(2)
pdoherty926
  • 9,895
  • 4
  • 37
  • 68
kxmh42
  • 3,121
  • 1
  • 25
  • 15
3

I'm surprised no one mentioned using curry yet, which has been in Ruby since Ruby 2.2.9. Here's how it can be done in the way OP wants using the standard Ruby library:

[1,3,5,7,9].map(&:+.to_proc.curry(2).call(11))
# => [12, 14, 16, 18, 20]

You need to supply an arity to curry that matches the call, though. This is because the interpreter doesn't know which object the + method refers to yet. This also means you can only use this when all the objects in map have the same arity. But that's probably not an issue if you're trying to use it this way.

AndrewKS
  • 3,603
  • 2
  • 24
  • 33
1

I'm not sure about the Symbol#with already posted, I simplified it quite a bit and it works well:

class Symbol
  def with(*args, &block)
    lambda { |object| object.public_send(self, *args, &block) }
  end
end

(also uses public_send instead of send to prevent calling private methods, also caller is already used by ruby so this was confusing)

localhostdotdev
  • 1,795
  • 16
  • 21