Search code examples
rubycurryingarity

Understanding the arity parameter of the method Proc.curry in Ruby


In the documentation of Ruby's Proc.curry method at https://ruby-doc.org/core-2.5.3/Proc.html#method-i-curry, it says:

curry -> a_proc

curry(arity) -> a_proc

Returns a curried proc. If the optional arity argument is given, it determines the number of arguments. A curried proc receives some arguments. If a sufficient number of arguments are supplied, it passes the supplied arguments to the original proc and returns the result. Otherwise, returns another curried proc that takes the rest of arguments.

I know arity means the number of arguments of a function. However, I'm not quite clear how it works here. Can anyone help to explain, please? I've read the examples on the doc, but still confused.


Solution

  • Perhaps walking through a better example might help. Let us start with a simple proc:

    p = proc { |a, b, c| "a=#{a.inspect}, b=#{b.inspect}, c=#{c.inspect}" }
    p[1,2,3]
    # "a=1, b=2, c=3" 
    

    If we call curry without the arity argument then it is pretty clear what's going on:

    p.curry          # evaluates to a proc
    p.curry[1]       # and a different proc
    p.curry[1][2]    # another different proc
    p.curry[1][2][3] # "a=1, b=2, c=3" 
    p.curry[1,2]     # yet another proc, hooray for procs!
    p.curry[1,2][3]  # "a=1, b=2, c=3"
    p.curry[1,2,3]   # "a=1, b=2, c=3"
    

    So p.curry gives us a succession of Procs by supplying values for the arguments until we have enough to evaluate the original Proc. Now we start adding arity values:

    p.curry(1)          # some proc
    p.curry(1)[]        # some other proc,
    p.curry(1)[1]       # "a=1, b=nil, c=nil" 
    p.curry(1)[1, 2]    # "a=1, b=2, c=nil" 
    p.curry(1)[1, 2, 3] # "a=1, b=2, c=3"
    
    p.curry(2)          # a proc
    p.curry(2)[]        # another proc
    p.curry(2)[1]       # oh look, a proc, a lovely surprise
    p.curry(2)[1][2]    # "a=1, b=2, c=nil" 
    p.curry(2)[1, 2]    # "a=1, b=2, c=nil" 
    p.curry(2)[1, 2, 3] # "a=1, b=2, c=3" 
    

    The arity argument is setting the effective arity of the curried proc; don't bother looking at the real arity – p.curry.arity, p.curry(1).arity, ... – since it will always be -1 (i.e. variadic). The result is that p.curry(1) is sort of like

    proc { |a| p[a] }.curry # "change" p's arity to 1 then curry
    

    and p.curry(2) is sort of like:

    proc { |a, b| p[a, b] }.curry # "change" p's arity to 2 then curry
    

    etc. Keep in mind that just because a (non-lambda) proc has arity n doesn't mean that you have to call it with n arguments. A proc's arity is more of a suggestion than anything else.

    Of course, if you try this chicanery with a lambda then everything goes sideways because lambdas care very much about their arity:

    λ = ->(a, b, c) {  "a=#{a.inspect}, b=#{b.inspect}, c=#{c.inspect}" }
    
    λ[1]             # ArgumentError (wrong number of arguments (given 1, expected 3))
    λ.curry[1]       # a lambda-proc
    λ.curry[1][2][3] # "a=1, b=2, c=3" 
    λ.curry[1][2, 3] # "a=1, b=2, c=3" 
    
    λ.curry(1)       # ArgumentError (wrong number of arguments (given 1, expected 3))
    λ.curry(2)       # ArgumentError (wrong number of arguments (given 2, expected 3))
    λ.curry(3)       # a lambda-proc that's just like λ.curry