2

I am getting my head around the functional model in Ruby and ran into a problem. I am able to successfully pass any given number of arguments to an arbitrary function as follows:

add = ->(x, y) { return x + y }
mul = ->(x, y) { return x * y }

def call_binop(a, b, &func)
  return func.call(a, b)
end

res = call_binop(2, 3, &add)
print("#{res}\n")   #5

res = call_binop(3, 4, &mul)
print("#{res}\n")   #12

However, I am not able to pass an arbitrary number of functions:

dbl = ->(x) { return 2 * x }
sqr = ->(x) { return x * x }

def call_funccomp(a, &func1, &func2)
  return func2.call(func1.call(a))
end

res = call_funccomp(3, &dbl, &sqr)
print("#{res}\n")   #Expect 36 but compiler error

The compiler error is syntax error, unexpected ',', expecting ')'

I have already added both lambdas and procs to an array and then executed elements of the array, so I know I can get around this by passing such an array as an argument, but for simple cases this seems to be a contortion for something (I hope) is legal in the language. Does Ruby actually limit the number or lambdas one can pass in the argument list? It seems to have a reasonably modern, flexible functional model (the notation is a little weird) where things can just execute via a call method.

pjs
  • 18,696
  • 4
  • 27
  • 56
  • @alf I disagree with the closure, the linked answer is hardwired to use two procs/lambdas. There is a more general solution (which I was in the middle of posting when this got closed). `def call_funccomp(a, *funcs); funcs.each { |f| a = f.call(a) }; a; end;` which, for example, gives 36 for `call_funccomp(3, dbl, sqr)` and 72 for `call_funccomp(3, dbl, sqr, dbl)`. – pjs May 25 '21 at 19:29
  • My question is not the same as the one linked, but I can use the posted solution there. Additionally (I'll try it out), I can use the one you have posted here. Thanks. The functional aspect of Ruby does not seem to follow "the principle or least surprise" to me. E.g., everything I have seen used the &func and func.call() syntax, which fails for this example. I had not yet seen func and func.() documented anywhere, and even worse, it works, which means it behaves differently. Might you know where I can get unambiguous Ruby documentation? Not the "hey look how simple programming is" kind? – Poisson Aerohead May 25 '21 at 19:52
  • 1
    @PoissonAerohead the `&block` syntax is rather an anomaly among programming languages. The language is indeed hardwired to only accept one block per method. You can pass a block even if the method does not expect it (the method won't do anything with it, but it's not a syntax error). If you need to pass more than one executable chunk of code into a method, use regular procs/lambdas, as outlined by pjs. – Sergio Tulentsev May 25 '21 at 20:01
  • 1
    @PoissonAerohead While searching, I found [this blog](https://thoughtbot.com/blog/proc-composition-in-ruby). It looks like functional composition was explicitly added to Ruby 2.6. Based on that, another solution you might like better is: `dbl = ->(x) { 2 * x }; sqr = ->(x) { x * x }; composition = dbl >> sqr; puts composition.call(3)`. The `>>`'s can be chained, so `composition = dbl >> sqr >> dbl` will yield 72 when called with 3 as the argument. – pjs May 25 '21 at 20:38
  • In the previous example, you can also sidestep assigning the composition to a variable: `dbl = ->(x) { 2 * x }; sqr = ->(x) { x * x }; puts (dbl >> sqr >> dbl).call(3)` – pjs May 25 '21 at 20:50
  • @SergioTulentsev It is very surprising because passing the block outside of the argument list uses `yield` while passing a proc or lambda with `&block` uses `block.call()` and also the formalism of defining a variable with some kind of key word (such as `proc`). This makes you think you are passing it like an ordinary argument, but you are not. It is also confusing because Perl uses the `&` sigil for sub types, so it looks like that is what Ruby is trying to do but it definitely is not. The syntax linked by the mod who closed this is the closest to "function as a first class object." – Poisson Aerohead May 25 '21 at 21:19
  • 1
    @PoissonAerohead You can `yield` to a `&block`, btw. "This makes you think you are passing it like an ordinary argument, but you are not" - not if you pass it with `&`, you are not. You're not decorating other arguments with this sigil, so it obviously is not just an ordinary argument. – Sergio Tulentsev May 25 '21 at 21:24

1 Answers1

3

Does Ruby actually limit the number or lambdas one can pass in the argument list?

No, you can pass as many procs / lambdas as you like. You just cannot pass them as block arguments.

Prepending the proc with & triggers Ruby's proc to block conversion, i.e. your proc becomes a block argument. And Ruby only allows at most one block argument.

Attempting to call call_funccomp(3, &dbl, &sqr) is equivalent to passing two blocks:

call_funccomp(3) { 2 * x } { x * x }

something that Ruby doesn't allow.

The fix is to omit &, i.e. to pass the procs / lambdas as positional arguments:

dbl = ->(x) { 2 * x }
sqr = ->(x) { x * x }

def call_funccomp(a, func1, func2)
  func2.call(func1.call(a))
end

res = call_funccomp(3, dbl, sqr)
print("#{res}\n") 

There's also Proc#>> which combines two procs:

def call_funccomp(a, func1, func2)
  (func1 >> func2).call(a)
end
Stefan
  • 109,145
  • 14
  • 143
  • 218