Currying a proc with keyword arguments in Ruby

后端 未结 4 969
刺人心
刺人心 2021-01-17 22:44

Say I have a generic Proc, Lambda or method which takes an optional second argument:

pow = -> (base, exp: 2) { base         


        
相关标签:
4条回答
  • 2021-01-17 23:24

    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.

    0 讨论(0)
  • 2021-01-17 23:31
    • 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.
    0 讨论(0)
  • 2021-01-17 23:32

    Extending @Stefan answer above as per last comment and in line with his snippet:

    def curry(method)
      -> (*args, **kargs) {
        required  = method.parameters.select { |type, _| type == :req }
        krequired = method.parameters.select { |type, _| type == :keyreq }
        all_args = (required.length <= args.length)
        all_keys = krequired.all? { |_, name| kargs.has_key?(name) }
        if all_args && all_keys
          final_args = (args + kargs.map {|k,v| {k => v} })
          method.call(*final_args)
        else
          -> (*args_, **kargs_) { curry(method)[*args, *args_, **kargs, **kargs_] }
        end
      }
    end
    
    def foo(a1, b1, c1 = 5, a2:, b2:, c2: 50)
      { a1: a1, b1: b1, c1: c1, a2: a2, b2: b2, c2: c2}
    end
    
    puts foz = curry(method(:foo)) #=> #<Proc:0x0000000003a255f0@./training.rb:56 (lambda)>
    puts bar = foz[6,  a2: 60]     #=> #<Proc:0x0000000003a255f0@./training.rb:56 (lambda)>
    puts bar[1, b2: 10]            #=> {:a1=>6, :b1=>1, :c1=>5, :a2=>60, :b2=>10, :c2=>50}
    puts baz = bar[1]              #=> #<Proc:0x0000000003a17540@./training.rb:64 (lambda)>
    puts baz[10, b2: 30, c2: 40]   #=> {:a1=>6, :b1=>1, :c1=>10, :a2=>60, :b2=>30, :c2=>40}
    
    0 讨论(0)
  • 2021-01-17 23:39

    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)
    
    0 讨论(0)
提交回复
热议问题