[Julia usage] Passing "complex" arguments to a function with default values

This is a quite general question on the correct way to pass arguments with default values in Julia. I am concerned about the facts that 1) my function’s signature could get quite unreadable if I have many arguments with “complex” default values 2) if my default values require several lines of code to initialize the argument, it may be hard to write it in the signature.

Here is an example where the argument a is what I mean by “complex”:

function foo(n::Int64; a::Array{Int64}=Array{Int64}(undef, n, 2*n))
    println(a) # Do whatever

n = 2
foo(n)  # use default value
foo(n, a=Array{Int64}(undef, 3))  # use specific value

So in this example, i see two drawbacks:

  1. if I have many “complex” arguments like a, it may get harder to read;
  2. if my initialization of a would require several lines of code, then I believe I cannot use this syntax.

I see one alternative using kwargs:

function foo(n::Int64; kwargs...)
    kwargs_dict = Dict(kwargs)
    if haskey(kwargs_dict, :a)
        a = kwargs_dict[:a]
        a = Array{Int64}(undef, n, 2*n)  # default value of `a`
        # Notice that here, setting `a` to its default value could be done in several lines of code
    println(a) # Do whatever

n = 2
foo(n)  # use default value
foo(n, a=Array{Int64}(undef, 3))  # use specific value

Is this alternative a good solution in terms of code readability / performance (is parsing kwargs expensive?). Do you have suggestions?

1 Like

A few things:

  1. Type annotation in function signatures do nothing in terms of code optimization, so they’re not necessary here.
  2. Array{Int64} is an abstract type, since it’s missing the dimensionality. Use Array{Int64, 2} or equivalently Matrix{Int64} if you want a 2-dimensional array.
  3. foo(n; a=...) is already using keyword arguments: The ; in front of a= makes a into a keyword (in contrast to n, which is a positional argument).

Your foo could just as well be written like foo(n, a=Matrix{Int64}(undef, n, 2*n).

If your function has multiple arguments with default values, the following is an option:

function foo(n, m, p;
   # function body

But if you find to require a lot of arguments that you pass around to a lot of functions, it’s usually a good idea to start thinking about putting them into a struct and passing that around instead.

Another option would be to create a bar function that only takes the required arguments (e.g. n), takes arbitrary default arguments as keyword arguments, constructs remaining ones and passes that into the fully fledged foo (which takes no keyword arguments). In that case, you’d expose bar as your “public” interface.


It’s also possible to define helper functions to create your default values:

julia> default_a(n) = Matrix{Int64}(undef, n, 2*n);

julia> foo(n, a=default_a(n)) = (n, a);

julia> foo(2)
(2, [0 0 0 0; 0 0 0 0])

A common pattern is to use nothing as “not specified”, for example

# function foo(n::Int64; a::Union{<:Array{Int64}, Nothing}=nothing)
function foo(n::Int64; a = nothing)
    if a === nothing
        a = Array{Int64}(undef, n, 2*n)
    println(a) # Do whatever

Lighter than using kwargs (which is desired in the simple example I gave) I guess? Thanks for the tip!

This is one clean alternative, I think:

julia> using Parameters

       @with_kw struct DefaultArgs
         a::Vector{Int} = zeros(Int,2)
         b::Int = 1

       foo(n;args...) = foo(n,DefaultArgs(;args...))
       function foo(n,args::DefaultArgs) 
         @unpack a, b = args 
         n*b .* a
foo (generic function with 2 methods)

julia> foo(2,a=[1,2])
2-element Vector{Int64}:

julia> foo(3,a=[1,2],b=5)
2-element Vector{Int64}: