For one of my projects, I have multiple implementations of the same analytical computations. These implementations offer different speed / precision trade-offs, and involve mostly a two step process where the entries and outputs are the same, but not the intermediate results. The final result is a matrix.
struct myintermediateresult1
...
end
struct myintermediateresult2
...
end
function myfirstmethod1(args...)::myintermediateresult1
...
end
function myfirstmethod2(args...)::myintermediateresult2
...
end
function mysecondmethod1(arg::myintermediateresult1)::Matrix{Float64}
...
end
function mysecondmethod2(arg::myintermediateresult2)::Matrix{Float64}
...
end
When I call this in my script, I use a symbol as argument of the calling function and wrap these methods in an if then else
construct e.g.
if arg_symbol === :method1
intermediateresult = myfirstmethod(args...)
else
intermediateresult = mysecondmethod(args...)
end
Therefore, the variable intermediateresult
is not always of the same type.
JeffreySarnoff explained that one could also declare a single function with multiple methods using for instance
function myfirstmethod(args...,::Val{:method1})::myintermediateresult1
...
end
function myfirstmethod(args...,::Val{:method2})::myintermediateresult2
...
end
and avoid the if then else
altogether.
What are the differences between these implementations, especially in a situation where I often compare the different implementations ? Is there a difference in terms of precompilation or efficiency ? This is probably related to the multiple dispatch (so I guess the second option is more julianic) but can someone highlight some major differences ?
If I understand you correctly, you have some distinct structs and some sets of methods, one method set for each struct type.
struct StructA
value::Int32
end
struct StructB
value::Float64
end
function f1_a(x::StructA) ...
function f1_b(x::StructB) ...
function f2_a(x::StructA) ...
function f2_b(x::StructB) ...
that will work without any ifelse-ing
1 Like
Well actually, if you look at myfirstmethod1
and myfirstmethod2
, both share the same input arguments, so I cannot use directly multiple dispatch without specifying an additional argument (as proposed in the second implementation) or use an if the else
construct.
We use Val
for that sort of disambiguation
(note the curly braces in the function definition and the parens in the call).
function f(::Val{:asymbol}, args..) ...
function f(::Val{:bsymbol}, args..) ...
f(Val(:asymbol), ..)
or f(Val(:bsymbol), ..)
as you require
So you mean there is a difference between
function f(::Val{:asymbol}, args..) ...
and
function f(:asymbol, args..) ...
?
absolutely – the form with Val does what you want, the other is not Julia code.
Indeed ! That’s my bad. That is actually what I meant in my first post. I will edit accordingly.
However, my initial question remains… what are the main differences between these two implementations ?
In my opinion, one is cleaner, easier to maintain over time (particularly if you add more cases)
In this particular case, performance differences should be inconsequential unless you do this a huge number of times [in that case benchmark each approach].
I’d rather follow the first reply in this topic and wrap input arguments in some sort of a buffer:
struct Method1Input
...
end
struct Method2Input
...
end
function first_step(input::Method1Input)::IntermediateResult1
...
end
function first_step(input::Method2Input)::IntermediateResult2
...
end
Then, instead of
first_step(Val(:method1), args...)
just call
first_step(Method1Input(args...))
Another way is to pass a function as an extra argument:
function first_step(method::Function, args...)
method(args...)
end
function method1(args...)
...
end
function method2(args...)
...
end