Drenmi
Drenmi

Reputation: 8777

Currying a proc with keyword arguments in Ruby

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

Answers (4)

rellampec
rellampec

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

Stefan
Stefan

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

John La Rooy
John La Rooy

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

Amadan
Amadan

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

Related Questions