Peter Thompson
Peter Thompson

Reputation: 81

How to write a Julia function that passes an arbitrary number of arguments to another function?

I'm new to Julia and much more experienced in R, so for those who are familiar with both, here is a snippet of R code that I'm trying to replicate in Julia.

f = function(a, b = 1, c = 2) a + 2*b = 3*c
g = function(d, ...) 5 + f(d, ...)

With this it would be valid to call g(1), which would evaluate with the default values for f, or you could specify g(1, b = 3) or g(1, c = 4, b = 2), or whatever. The point is of course that you can pass any permutation of optional arguments to f without order mattering as long as the names are specified.

In Julia I've had a bit of trouble doing this because the varargs stuff works a bit differently. I'm aware that you can pass an unlimited list of arguments to a function in the form of a tuple (e.g., function g(d::Number, f_args::NamedTuple), but it seems like it doesn't work with optional keyword arguments. It's almost as if somewhere along the line the f_args tuple loses its names, and f gets confused because it sees 3 arguments without names and doesn't know which method to select?

If it is indeed possible, I'd really appreciate if someone could show me how to do this in Julia. I would like to preserve the optionality of these arguments, as well as their names. I'd also like to be able to keep their types (i.e., I'd like each argument of f to only be acceptable if they fit under the correct type, which is of course usually done in Julia with the :: operator in the header).

PS - for the purposes of my question, rewriting g so it has c and b as optional arguments will not suffice for me. I display the example of f and g for simplicity but in reality am hoping to apply these principles to much more complex functions with many arguments.

Upvotes: 5

Views: 2331

Answers (1)

PaSTE
PaSTE

Reputation: 4548

In julia, optional positional arguments and keyword arguments are treated differently.

If we want to allow arbitrary argument order in a function, we have to specify keyword arguments. Unlike in R, julia insists that we differentiate between positional arguments and keyword arguments using a semicolon in the argument list. We can write your functions f and g in julia to allow this by simply adding appropriate semicolons:

f(a; b=1, c=2) = a + 2*b + 3*c
g(d; kwargs...) = 5 + f(d; kwargs...)

This defines a method f with one required positional argument and two optional keyword arguments, and another method g with one required positional argument and any number of keyword arguments that will be passed on to f. That means we can do:

g(1)             # 14
g(1) == 5 + f(1) # true
g(1, b=3)        # 18
g(1, c=4, b=2)   # 22

Again, unlike in R, keyword arguments are different from positional arguments, so we cannot do this:

f(1, 2, 4)
# ERROR: MethodError: no method matching f(::Int64, ::Int64, ::Int64)
# Closest candidates are:
#   f(::Any; b, c) at REPL[1]:1

This is because the method signature we defined takes exactly one positional argument. If you want a function that can take either positional arguments or keyword arguments, we have to define a new function signature, like this:

f(a, x=1, y=2) = f(a; b=x, c=y)

Here, we are creating some new methods with the same name f as our original method, and all they do is pass on their positional arguments as keyword arguments. Now we can do this:

f(1, 2, 4) == 17  # true

If we want g to function in a similar way, remembering that julia makes a distinction between positional and keyword arguments, we need to specify two varargs: one for the positional arguments and one for the keyword arguments:

g(d, args...; kwargs...) = 5 + f(d, args...; kwargs...)

Now we can do things like this:

g(1, 2) == g(1, b=2)         # true
g(1, 2, 4) == g(1, c=4, b=2) # true

But things get messy when we try to combine these types of signatures:

g(1, 2, c=4)
# ERROR: MethodError: no method matching f(::Int64, ::Int64; c=4)
# Closest candidates are:
#  f(::Any, ::Any) at REPL[18]:1 got unsupported keyword argument "c"
#  f(::Any, ::Any, ::Any) at REPL[18]:1 got unsupported keyword argument "c"
#  f(::Any; b, c) at REPL[18]:1

Why is this not allowed? The error message gives us a hint as to what defining optional positional arguments do under the hood. When we define a method with n optional positional arguments, julia just makes n+1 methods, each with 0 through n arguments tagged on to the end of the signature. Like this:

h(a, b=1, c=2, d=3) = nothing
# h (generic function with 4 methods)

methods(h)
# 4 methods for generic function "h":
#[1] h(a) in Main at REPL[32]:1
#[2] h(a, b) in Main at REPL[32]:1
#[3] h(a, b, c) in Main at REPL[32]:1
#[4] h(a, b, c, d) in Main at REPL[32]:1

That means when we defined the optional positional argument version of f, julia made the signatures f(a, b) and f(a, b, c) for us, but it did not make a method with the signature f(a, b; c).

Upvotes: 9

Related Questions