Currying a Proc with Keyword Arguments in Ruby

Currying a proc with keyword arguments in Ruby

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.

Currying out of order in Ruby

Currently standard library of Ruby doesn't provide such option.

However you could easily implement a custom method which allows you to change the order of the arguments of Procs and lambdas. For example I'll present an imitation of Haskell's flip function:

flip f takes its (first) two arguments in the reverse order of f

How would it look like in Ruby?

def flip
lambda do |function|
->(first, second, *tail) { function.call(second, first, *tail) }.curry
end
end

And now we can use this method to change the order of our lambdas.

concat = ->(x, y) { x + y }
concat.call("flip", "flop") # => "flipflop"

flipped_concat = flip.call(concat)
flipped_concat.call("flip", "flop") # => "flopflip"

Ruby: provide an argument while turning proc to a block

Regarding your comment:

Strange, but it swaps arguments during the performance

Actually, the argument order is preserved.

curry returns a new proc that effectively collects arguments until there are enough arguments to invoke the original method / proc (based on its arity). This is achieved by returning intermediate procs:

def foo(a, b, c)
{ a: a, b: b, c: c }
end

curried_proc = foo.curry #=> #<Proc:0x007fd09b84e018 (lambda)>
curried_proc[1] #=> #<Proc:0x007fd09b83e320 (lambda)>
curried_proc[1][2] #=> #<Proc:0x007fd09b82cfd0 (lambda)>
curried_proc[1][2][3] #=> {:a=>1, :b=>2, :c=>3}

You can pass any number of arguments at once to a curried proc:

curried_proc[1][2][3]     #=> {:a=>1, :b=>2, :c=>3}
curried_proc[1, 2][3] #=> {:a=>1, :b=>2, :c=>3}
curried_proc[1][2, 3] #=> {:a=>1, :b=>2, :c=>3}
curried_proc[1, 2, 3] #=> {:a=>1, :b=>2, :c=>3}

Empty arguments are ignored:

curried_proc[1][][2][][3] #=> {:a=>1, :b=>2, :c=>3}

However, you obviously can't alter the argument order.


An alternative to currying is partial application which returns a new proc with lower arity by fixing one or more arguments. Unlike curry, there's no built-in method for partial application, but you can easily write your own:

my_proc = -> (arg, num) { arg * num }

def fix_first(proc, arg)
-> (*args) { proc[arg, *args] }
end

fixed_proc = fix_first(my_proc, 'foo') #=> #<Proc:0x007fa31c2070d0 (lambda)>
fixed_proc[2] #=> "foofoo"
fixed_proc[3] #=> "foofoofoo"

[2, 3].map(&fixed_proc) #=> ["foofoo", "foofoofoo"]

Or fixing the last argument:

def fix_last(proc, arg)
-> (*args) { proc[*args, arg] }
end

fixed_proc = fix_last(my_proc, 2) #=> #<Proc:0x007fa31c2070d0 (lambda)>
fixed_proc['foo'] #=> "foofoo"
fixed_proc['bar'] #=> "barbar"

['foo', 'bar'].map(&fixed_proc) #=> ["foofoo", "barbar"]

Of course, you are not limited to fixing single arguments. You could for example return a proc that takes an array and converts it to an argument list:

def splat_args(proc)
-> (array) { proc[*array] }
end

splatting_proc = splat_args(my_proc)
[['foo', 1], ['bar', 2], ['baz', 3]].map(&splatting_proc)
#=> ["foo", "barbar", "bazbazbaz"]

Collecting keyword arguments in Ruby

It's because the first parameter i, is a required parameter (no default value), so, the first value passed to the method (in your first example, this is the hash {a: 7, b: 8}) is stored into it.

Then, since everything else is optional, the remaining values (if any, in this example, there are none) are filled in, as applicable. IE, the second parameter will go to j, unless it is a named parameter, then it goes to kargs (or k). The third parameter, and any remaining--up until the first keyword argument, go into args, and then any keyword args go to kargs

def foo(i, j= 9, *args, k: 11, **kargs)
puts "i: #{i}; args: #{args}; kargs: #{kargs}"
end

foo(a: 7, b: 8)
# i: {:a=>7, :b=>8}; args: []; kargs: {}

How to pre-fill arguments to a function in Ruby?

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')

Mixing keyword argument and arguments with default values duplicates the hash?

TL;DR ruby allows passing hash as a keyword argument as well as “expanded inplace hash.” Since change_hash(rand: :om) must be routed to keyword argument, so should change_hash({rand: :om}) and, hence, change_hash({}).


Since ruby allows default arguments in any position, the parser takes care of default arguments in the first place. That means, that the default arguments are greedy and the most amount of defaults will take a place.

On the other hand, since ruby lacks pattern-matching feature for function clauses, parsing the given argument to decide whether it should be passed as double-splat or not would lead to huge performance penalties. Since the call with an explicit keyword argument (change_hash(rand: :om)) should definitely pass :om to keyword argument, and we are allowed to pass an explicit hash {rand: :om} as a keyword argument, Ruby has nothing to do but to accept any hash as a keyword argument.


Ruby will split the single hash argument between hash and rand:

k = {"a" => 42, rand: 42}
def change_hash(h={}, rand: :om)
h[:foo] = 42
puts h.inspect
end
change_hash(k);
puts k.inspect

#⇒ {"a"=>42, :foo=>42}
#⇒ {"a"=>42, :rand=>42}

That split feature requires the argument being cloned before passing. That is why the original hash is not being modified.

How to get around curry evaluating its arguments?

Simple answer

The syntax would be,

(define (check-io-data data) 
((curry equal?) (get-io-data) data))

Discussion

Because check-io-data is a function of one argument, it must be defined as such. Because equal? is a function with arity = 2, it must be passed two arguments when curried.

In schematic form, this:

> (define f (lambda (x) (equal? 'curried-argument x)))
> (f 'curried-argument)
#t
> (f 1)
#f

Is equivalent to:

> (define (f x) ((curry equal?) 'curried-argument x))
> (f 'curried-argument)
#t
> (f 1)
#f

Illustrative example

#lang racket

;;; Use a generator to simulate
;;; a non-idempotent procedure
(require racket/generator)
(define get-io-data
(infinite-generator
(yield 1)
(yield 2)
(yield 3)))

(define (check-io-data x)
((curry equal?) (get-io-data) x))

(check-io-data 1) ; #t
(check-io-data 1) ; #f
(check-io-data 1) ; #f
(check-io-data 1) ; #t

Conclusion

A curried function has to define itself as a function [i.e. (define (name arg...)(body)) ] to establish the arity of the lambda created by curry.

Ruby automatically expands Hash into keyword arguments without double splat

Since x is optional, hash moves over to kwarg argument.
Unspecified keywords raise error in that case:

def foo(name:)
p name
end

foo # raises "ArgumentError: missing keyword: name" as expected
foo({name: 'Joe', age: 10}) # raises "ArgumentError: unknown keyword: age"

Check out this article



Related Topics



Leave a reply



Submit