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

Hi!
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
end

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]
    else
        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
    end
    println(a) # Do whatever
end

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?
Thanks!

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;
      a=...,
      b=...,
      c=...)
   # function body
end

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.

7 Likes

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])
6 Likes

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)
    end
    println(a) # Do whatever
end
3 Likes

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
       end

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

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

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


3 Likes