Functions with many arguments

#1

I’m currently running into a situation where my functions need to operate on many potentially huge matrices of slightly different sizes. I preallocate all memory first, such that all functions take elements from some of the input arguments, do a computation and store it in one of the others, e.g.

function rhs!(U,V,A,B,C,D,E,F,G)
    func1!(U,V,A,B)
    func2!(U,V,C,D)
    func3!(U,V,D,E)
    func4!(U,V,F,G)
end

If I have more like 30+ arguments I can’t even be bothered to write ::AbstractMatrix for readability. Inside rhs! many of the matrices are read&write, some are read-only (U,V in this example). In almost all cases I am also totally fine if the variable names are the same inside the function and in the outer scope:

U,V = preallocate1(...)
A,B,C,D,E,F,G,H = preallocate2(...)

for i in 1:n
    ...
    rhs!(U,V,A,B,C,D,E,F,G,H)
    ...
end

Is there a recommended way how to maintain readability whilst declaring everything correctly and avoiding zipping into tuples and unpacking the variables? I guess, technically I could set A,B,C,… as constants, and not pass them on as an argument but then everytime I store some new entries in them I would get a redefining constant warning.

0 Likes

#2

Sorry in advance if I am totally out of scope.
If some of your arguments are semantically related it may be a good idea to group them inside a struct (immutable). In your example, is it possible that some matrix arguments happen to be blocks of a larger block matrix ?

3 Likes

#3

I would say that more than 3 positional arguments are likely to be code smell, and more than 5 definitely is a very bad idea.

As @LaurentPlagne suggested, wrap them in something: a struct, a NamedTuple, or similar. preallocate1 should return this object instead, etc.

2 Likes

#4

I wouldn’t recommend that, for exactly the reason you noted and also because it obscures what your function is doing. As others have stated, there is almost certain to be a better way to organize your arguments than a flat collection of dozens of matrices. Putting those matrices into a struct is an improvement, but I would suggest that you consider whether there is even more structure to your data than that. Can your function be broken up into pieces that operate on just some of those matrices? Then perhaps those matrices form a logical unit. Can you give that group a name? Then perhaps that group should be its own struct inside your outer struct.

4 Likes

#5

I see so what’s recommend is something like that

mutable struct Group1
    A::AbstractMatrix
    B::AbstractMatrix
    C::AbstractMatrix
end

function rhs!(Gr1,Gr2,Gr3)
    func1!(Gr1.A,Gr2.B)
    func2!(Gr1.D,Gr2.E)
    ...
end

# preallocation
Gr1 = Group1(zeros(30,30),zeros(32,30),zeros(30,32))

for i in 1:n  
    ...
    rhs!(Gr1,Gr2,Gr3)
    ...
end

which would only require to rewrite some of the arguments inside the rhs! function with the dot notation Gr1.A etc. I guess I could live with that!

0 Likes

#6

That is what I had in mind. It makes sense if Group1 really means something and have a significant name.
In addition you may define a “Ctor”/function with the same name to build your struct.

In addition I would not recommend to store abstract types in struct. Either you now the concrete type or you can use parametric types. Abstract types are OK with functions but cause performance problem inside structs.

2 Likes

#7

Parameters.jl is very useful for that, because inside rhs!(Gr1,Gr2,Gr3) you could just put

@unpack U,V,A,B = Gr1
@unpack C,D,E,F = Gr2
@unpack G,H = Gr3

or however it works, and then you don’t have to change the rest of your implementation of rhs!(U,V,A,B,C,D,E,F,G,H) when converting it to use structs.

2 Likes

#8

Just one more suggestion: when writing a struct definition, it is important to use concrete types for the fields of the struct (see https://docs.julialang.org/en/v1/manual/performance-tips/index.html#Avoid-fields-with-abstract-type-1 ). For example,

struct Foo
  x::Matrix{Float64}
end

instead of:

struct Foo
   x::AbstractMatrix
end

Note that if you still want to handle any kind of scalar, you can use a parametric type:

struct Foo{T}
  x::Matrix{T}
end

and if you want to handle any kind of matrix without any performance penalty, you can put the entire matrix type into the parameters of your struct:

struct Foo{M <: AbstractMatrix}
  x::M 
end
7 Likes