Reputation: 8777
Say I have a generic Proc
, Lambda
or method
which takes an optional second argument:
pow = -> (base, exp: 2) { base**exp }
Now I want to curry this function, giving it an exp
of 3
.
cube = pow.curry.call(exp: 3)
There's an ambiguity here, arising from the keyword arguments and the new hash syntax, where Ruby interprets exp: 3
as a hash being passed as the first argument, base
. This results in the function immediately being invoked, rendering a NoMethodError
when #**
is sent to the hash.
Setting a default value for the first argument will similarly result in the function being immediately invoked when currying, and if I mark the first argument as required, without providing a default:
pow = -> (base:, exp: 2) { base**exp }
the interpreter will complain that I'm missing argument base
when I attempt to curry the Proc
.
How can I curry a function with the second argument?
Upvotes: 8
Views: 2508
Reputation: 752
Extending on @Stefan answer here a solution that includes all the arguments:
# @param meth [Method, Proc, Lambda]
# @param on_missing [Symbol] what should do when there are missing required arguments
# - `:curry` -> it does a curry with the parameters received (default).
# - `:call` -> it calls anyway (will error).
# - `:safe` -> it sets to `nil` the missing required arguments and does a call.
# - `:return` -> it returns `nil` but doesn't call.
def curry(meth, on_missing = :curry)
-> (*args, **kargs, &block) do
params = meth.parameters
kparams = params.select { |type, _| (type == :keyreq) || (type == :key) }
aparams = params.select { |type, _| (type == :req) || (type == :opt) }
kreq_miss = kparams.select { |type, _| type == :keyreq }.map(&:last) - kargs.keys
req_miss = aparams.select { |type, _| type == :req }.count - args.count
req_miss = req_miss >= 0 ? req_miss : 0
ready = kreq_miss.empty? && req_miss == 0
if on_missing == :safe
unless params.find { |type, _| type == :keyrest }
(kargs.keys - kparams.map(&:last)).each { |name| kargs.delete(name) }
end
unless params.find { |type, _| type == :rest }
args = args[0..(aparams.count-1)] if args.count > aparams.count
end
unless ready
kreq_miss.each { |name| kargs[name] = nil }
args = args.dup.push(*Array.new(req_miss, nil))
ready = true
end
end
return meth.call(*args, **kargs, &block) if ready || on_missing == :call
return nil if on_missing == :return
# (default) on_missing == :curry
-> (*oth, **koth, &blk) do
curried = curry(meth, on_missing)
curried[*args, *oth, **kargs, **koth, &(blk || block)]
end
end
end
Usage Example
def foo(a, b = :default, c:, d: :default)
args = { 'a' => a, 'b' => b, c: c, d: d }
yield(args) if block_given?
"foo called!"
end
bar = curry(method(:foo))["bar", d: "override"] {|r| pp r}
bar.call(c: "now")
# {"a"=>"bar", "b"=>:default, :c=>"now", :d=>"override"}
# => "foo called!"
curry(method(:foo), :safe)["bar", d: "override"] {|r| pp r}
# {"a"=>"bar", "b"=>:default, :c=>nil, :d=>"override"}
# => "foo called!"
curry(method(:foo), :return)["bar", d: "override"] {|r| pp r}
# => nil
curry(method(:foo), :call)["bar", d: "override"] {|r| pp r}
# ArgumentError (missing keyword: :c)
Overloading arguments
curry(method(:foo), :safe).call("bar", "baz", "ignored", d: "override", z: "ignored") {|r| pp r}
# {"a"=>"bar", "b"=>"baz", :c=>nil, :d=>"override"}
# => "foo called!"
bar = curry(method(:foo))["bar", d: "override"]
bar = bar["override", z: "bad"] {|r| pp r}
bar.call(c: "call it")
# ArgumentError (unknown keyword: :z)
Upvotes: 0
Reputation: 114178
You could build your own keyword-flavored curry method that collects keyword arguments until the required parameters are present. Something like:
def kw_curry(method)
-> (**kw_args) {
required = method.parameters.select { |type, _| type == :keyreq }
if required.all? { |_, name| kw_args.has_key?(name) }
method.call(**kw_args)
else
-> (**other_kw_args) { kw_curry(method)[**kw_args, **other_kw_args] }
end
}
end
def foo(a:, b:, c: nil)
{ a: a, b: b, c: c }
end
proc = kw_curry(method(:foo))
proc[a: 1] #=> #<Proc:0x007f9a1c0891f8 (lambda)>
proc[b: 1] #=> #<Proc:0x007f9a1c088f28 (lambda)>
proc[a: 1, b: 2] #=> {:a=>1, :b=>2, :c=>nil}
proc[b: 2][a: 1] #=> {:a=>1, :b=>2, :c=>nil}
proc[a: 1, c: 3][b: 2] #=> {:a=>1, :b=>2, :c=>3}
The example above is limited to keyword arguments only, but you can certainly extend it to support both, keyword arguments and positional arguments.
Upvotes: 8
Reputation: 304157
I don't think you can do it with Proc.curry
, but there is always the longhand way
cube = -> (base) {pow.(base, exp: 3)}
You could also create a factory function
pow_factory = -> (exp) {-> (base) {pow.(base, exp: exp)}}
cube = pow_factory.(3)
Upvotes: 7
Reputation: 198314
curry
does not work with keyword arguments. A curried function is getting one parameter at a time, which is conceptually incompatible with "any order is fine" keyword arguments.curry
must know the exact arity. If you just call curry
with no arguments, it will ignore any optionals (in case of pow = -> (base, exp=2) { base**exp }
, same as curry(1)
). Use curry(2)
to force both parameters. A curried function can't know an optional parameter is following, and read the future to determine if it should execute or return a curried continuation.Upvotes: 2