Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby: provide an argument while turning proc to a block

Tags:

ruby

We can easily define a method and turn it into block with unary ampersand.

def my_method(arg)
  puts arg*2
end

['foo', 'bar'].each(&method(:my_method))

# foofoo
# barbar

# or
my_method = ->(arg) { puts arg*2 }
['foo', 'bar'].each(&my_method)
# same output

As we see the first argument is passed automatically when we work with aggregates. But what if we need to pass 2 or even more arguments?

my_method = ->(arg,num) { puts arg*num }
['foo', 'bar'].each(&my_method)
# ArgumentError: wrong number of arguments (1 for 2)
['foo', 'bar'].each(&my_method(3))
# NoMethodError: undefined method `foo' for main:Object
['foo','bar'].each do |i, &my_method|
  yield i, 3
end
# LocalJumpError: no block given (yield)

Is that possible to pass additional arguments while turning proc to a block?

like image 714
DreamWalker Avatar asked Jan 15 '16 09:01

DreamWalker


People also ask

How do you pass a block as argument in Ruby?

We can explicitly accept a block in a method by adding it as an argument using an ampersand parameter (usually called &block ). Since the block is now explicit, we can use the #call method directly on the resulting object instead of relying on yield .

What is a block argument in Ruby?

A ruby block is one or more lines of code that you put inside the do and end keywords (or { and } for inline blocks). It allows you to group code into a standalone unit that you can use as a method argument.

What is * args in Ruby?

In the code you posted, *args simply indicates that the method accepts a variable number of arguments in an array called args . It could have been called anything you want (following the Ruby naming rules, of course).


2 Answers

@sawa is right. You can do that with curry.

Proc version:

mult = proc {|a, b| a * b} # => #<Proc:0x00000002af1098@(irb):32>
[1, 2].map(&mult.curry[2])  # => [2, 4]

Method version:

def mult(a, b)
  a*b
end

[1, 2].map(&method(:mult).to_proc.curry[2])  # => [2, 4]
like image 190
fylooi Avatar answered Sep 20 '22 16:09

fylooi


Regarding your comment:

Strange, but it swaps arguments during the performance

Actually, the argument order is preserved.

curry returns a new proc that effectively collects arguments until there are enough arguments to invoke the original method / proc (based on its arity). This is achieved by returning intermediate procs:

def foo(a, b, c)
  { a: a, b: b, c: c }
end

curried_proc = foo.curry  #=> #<Proc:0x007fd09b84e018 (lambda)>
curried_proc[1]           #=> #<Proc:0x007fd09b83e320 (lambda)>
curried_proc[1][2]        #=> #<Proc:0x007fd09b82cfd0 (lambda)>
curried_proc[1][2][3]     #=> {:a=>1, :b=>2, :c=>3}

You can pass any number of arguments at once to a curried proc:

curried_proc[1][2][3]     #=> {:a=>1, :b=>2, :c=>3}
curried_proc[1, 2][3]     #=> {:a=>1, :b=>2, :c=>3}
curried_proc[1][2, 3]     #=> {:a=>1, :b=>2, :c=>3}
curried_proc[1, 2, 3]     #=> {:a=>1, :b=>2, :c=>3}

Empty arguments are ignored:

curried_proc[1][][2][][3] #=> {:a=>1, :b=>2, :c=>3}

However, you obviously can't alter the argument order.


An alternative to currying is partial application which returns a new proc with lower arity by fixing one or more arguments. Unlike curry, there's no built-in method for partial application, but you can easily write your own:

my_proc = -> (arg, num) { arg * num }

def fix_first(proc, arg)
  -> (*args) { proc[arg, *args] }
end

fixed_proc = fix_first(my_proc, 'foo')  #=> #<Proc:0x007fa31c2070d0 (lambda)>
fixed_proc[2]  #=> "foofoo"
fixed_proc[3]  #=> "foofoofoo"

[2, 3].map(&fixed_proc) #=> ["foofoo", "foofoofoo"]

Or fixing the last argument:

def fix_last(proc, arg)
  -> (*args) { proc[*args, arg] }
end

fixed_proc = fix_last(my_proc, 2)  #=> #<Proc:0x007fa31c2070d0 (lambda)>
fixed_proc['foo']  #=> "foofoo"
fixed_proc['bar']  #=> "barbar"

['foo', 'bar'].map(&fixed_proc) #=> ["foofoo", "barbar"]

Of course, you are not limited to fixing single arguments. You could for example return a proc that takes an array and converts it to an argument list:

def splat_args(proc)
  -> (array) { proc[*array] }
end

splatting_proc = splat_args(my_proc)
[['foo', 1], ['bar', 2], ['baz', 3]].map(&splatting_proc)
#=> ["foo", "barbar", "bazbazbaz"]
like image 39
Stefan Avatar answered Sep 20 '22 16:09

Stefan