Ruby short-cut for passing only expected number of arguments to a lambda

86 Views Asked by At

Is there a short-cut for the code below that will pass only the expected number of arguments (the "arity") to the lambda (and ignore the rest)?

args = [:p1, :p2, :p3, :p4, :p5, :p6]
expected_args = args[0...the_lambda.arity]
value = the_lambda.call(*expected_args)

The big picture is that I'm writing a Ruby program where the user can write a Ruby config file that contains lambdas. I'd like the user to be able to write these lambdas using only the parameters needed (without having to do "extra" typing like including parameters that won't be used).

With the code above the user could write

my_override = ->(a) {....}

or

my_override = ->(a, b, c) { ....}

without the compiler complaining. (Part of the idea here is that when teaching new users, we wouldn't even need to mention the fact that there is more than one or two parameters. We could simply "save" those extra features to be revealed to more advanced users.) The end goal is to make the writing of the lambdas as simple as possible -- which means avoiding having elements of the user-written code that do not directly contribute to the functioning of the lambda.

2

There are 2 best solutions below

0
engineersmnky On

Generally speaking I agree with the comments of using a Proc or an options Hash but you can facilitate your request by wrapping the "user lambda" and the argument passing into a method call like so

def lambda_wrapper(user_lambda,*args,**kwargs,&block)
  defined_args = user_lambda.parameters
     .each_with_object(Hash.new {|h,k| h[k] = []}) do |(type,arg),obj| 
       obj[type] << arg
     end
  user_lambda.call(
    *collect_args(args,defined_args),
    **collect_kwargs(kwargs,defined_args),
    &block)
end

def collect_args(args, defined_args)
  return args if defined_args.key?(:rest)
  args.first(defined_args[:req].concat(defined_args[:opt]).size)
end

def collect_kwargs(kwargs, defined_args)
  return kwargs if defined_args.key?(:keyrest)
  kwargs.slice(*defined_args[:keyreq].concat(defined_args[:key]))
end

While I would recommend encapsulatimg this functionality in a Class (Working Example), this will gather the arguments and keyword arguments for the lambda and then invoke the user lambda with those arguments by extracting them from the arguments passed to the wrapper.

Example:

user_lambda = ->(a,b,c,d=57, e:, f:,g:92) {[a,b,c,d,e,f,g]}
lambda_wrapper(user_lambda,1,2,3,e:12,f:17)
#=> [1,2,3,57,12,17,92]
lambda_wrapper(user_lambda,1,2,3,4,e:12,f:17,g:101)
#=> [1,2,3,4,12,17,101]
lambda_wrapper(user_lambda,1,2,3,4,5,6,e:12,f:17,x:88)
#=> [1,2,3,4,12,17,92]
lambda_wrapper(user_lambda,1,2,e:12,f:17,x:88)
#=> wrong number of arguments (given 2, expected 3+; required keywords: e, f) (ArgumentError)
lambda_wrapper(user_lambda,1,2,3,f:17,x:88)
#=> missing keyword: :e (ArgumentError)
0
Todd A. Jacobs On

By definition, Ruby lambdas care about arity but Proc objects don't. You could do something like splat arguments to the lambda (I provide such an example below) but with either a Proc or lambda you need to add logic to handle cases where the caller does something unexpected. That's much easier to do in a method than in a Proc or lambda.

Generally, the correct thing to do to avoid arity issues would be to use a Proc object, since a Proc doesn't care about arity. For example, this works without fuss whether you pass in one Array or multiple Integer objects:

prc = proc { |a, b| pp a, b }

prc.call [1, 2, 3, 4, 5]
#=> [1, 2]

prc.call 1, 2, 3, 4, 5
#=> [1, 2]

Otherwise, you will need to collaborate more closely with your caller (which is generally bad design), or create a splatted argument just to throw the positional arguments away. For example:

# define +prc+ as a lambda and then coerce it
prc = ->(*ary) { pp ary.flatten.take(2) }

prc.call [1, 2, 3, 4, 5]
#=> [1, 2]

prc.call 1, 2, 3, 4, 5
#=> [1, 2]

Either way, you have edge cases. Consider:

prc = proc { |a, b| pp a, b }
prc.call [1, 2, 3, 4, 5], [6, 7]
#=> [[1, 2, 3, 4, 5], [6, 7]]

prc = ->(*ary) { pp ary.flatten.take(2) }
prc.call [1, 2, 3, 4, 5], [6, 7]
# => [1, 2]

If you don't know what the caller is likely to pass in, you might as well just write a method so you can handle arity, keyword options, duck-typing, class validation, or whatever other sanity checks you need to verify unknown inputs with less fuss. Proc objects and lambdas can be useful if you need a closure, but other than closing over the Binding where they're defined I'm not sure what you gain in comparison to the headaches this will cause for your particular use case.