2

Let's say I have a function foo:

def foo(A, B, C)
 A + B + C
end

And I call it like this, with only the last parameter changing:

foo("foo", "bar", "123")
foo("foo", "bar", "456")
foo("foo", "bar", "789")

How can I "bake" or "pre-fill" the arguments that do not change? So maybe I would get a new callable foo_baked such that foo_baked("123") is the same as foo("foo", "bar", "123")?

And use it like this:

foo_baked = ...?
foo_baked("123")
foo_baked("456")
foo_baked("789")

Note that I do not want to define a new function using def but want to be able to create foo_baked at runtime on the fly, maybe for an array.

Jabrove
  • 718
  • 5
  • 13

4 Answers4

6

Ruby has function-currying built in with the curry method:

def foo(a, b, c)
  a + b + c
end

foo_baked = method(:foo).curry.call('foo', 'bar')

# this returns "foobar123"
foo_baked.call('123')

# ArgumentError (wrong number of arguments (given 4, expected 3))
foo_baked.call('123', '234')

Basically, Method#curry returns a curried proc: a function, pre-loaded with arguments, that will only execute once enough arguments have been passed to satisfy the method signature.

Note that foo_baked.lambda? returns true, so a curried proc is actually a lambda. This is important since it means that you'll get an ArgumentError if you exceed the maximum number of arguments.

You can allow additional arguments beyond the required three by passing an arity argument to curry.

def foo(*args)
  args.join
end

# does not execute since only 2 arguments have been supplied
foo_baked = method(:foo).curry(3).call('foo', 'bar')

# executes on the third argument
# this returns "foobar123"
foo_baked.call('123')

# this returns "foobar123234"
foo_baked.call('123', '234')
TonyArra
  • 10,607
  • 1
  • 30
  • 46
1

You can define a new method foo_baked that wraps foo and passes the non-changeable arguments as hardcoded values:

def foo_baked(c)
  foo("foo", "bar", c)
end

Then it can be called like so:

foo_baked("123")

Alternatively, you can use Ruby's built-in #curry method, as outlined in TonyArra's answer. This approach is more flexible, returning a callable proc until enough arguments are provided.

Zoran
  • 4,196
  • 2
  • 22
  • 33
0

You have a few options. The first is a currying approach as outlined in Zoran's answer.

The second is to use default positional arguments. But in order to do this, the optional arguments need to be after the required ones:

def foo_baked(a, b="foo", c="bar")
  [a, b, c]
end

foo_baked(123) # => [123, "foo", "bar"]
foo_baked(123, "asd") 

The third option is to use keyword arguments. The benefit of this approach is that the arguments can be in any order. You can pick and choose which ones you provide values for.

def foo_baked(b: "foo", c: "bar", a:)
  [a, b, c]
end

foo_baked(a: 123) # => [123, "foo", "bar"]
foo_baked(a: 123, b: "asd") # => [123, "asd", "bar"]

A final option would be to combine positional and keyword arguments:

def foo_baked(a, b: "foo", c: "bar")
  [a, b, c]
end

foo_baked(123) # => [123, "foo", "bar"]
foo_baked(123, b: "asd") # => [123, "asd", "bar"]

This has the benefit of still letting you use the positional arguments, but the order of the optional args doesn't matter and they will never interfere with the positional ones

max pleaner
  • 26,189
  • 9
  • 66
  • 118
  • 2
    A problem with all these solutions is that they mask errors when `foo`, as originally written, is intended to be called with its three required arguments but, erroneously, fewer than three are provided. In that case we want an exception to be raised, but instead Ruby quietly passes on the error. I say, "leave `foo` alone". Instead do what @Zoran suggests. – Cary Swoveland Apr 21 '22 at 19:21
  • @CarySwoveland is it because the signature makes foo and bar optional values? – Kick Buttowski Apr 21 '22 at 20:40
  • 1
    @KickButtowski, yes. – Cary Swoveland Apr 21 '22 at 21:12
0

You could create a lambda wrapping original method:

foo_baked = ->(value) { foo('foo', 'bar', value) }

foo_baked.(123) # => [123, "foo", "bar"]

Note additional . in the execution though. This is ruby special syntax, equivalent to:

foo_baked.call(123)
BroiSatse
  • 44,031
  • 8
  • 61
  • 86
  • 1
    Did you mean `foo_baked.("123") #=> "foobar123"`? As I expect you know, other ways to express [Proc#call](https://ruby-doc.org/core-2.7.0/Proc.html#method-i-call) are `foo_baked["123"]` and `foo_baked.yield("123")`. – Cary Swoveland Apr 21 '22 at 21:28