Reputation: 81
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
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