I’m not sure what the exact name of this phenomenon is, so I’ll change the title if necessary, but my question involves the following situation: I have some structs that determine how certain data x is transformed:
abstract type FooType end
struct Foo1{T<:Real} <: FooType
a::T
end
struct Foo2{T<:Real} <: FooType
b::T
end
transform(x, a::Foo1) = x .* a.a
transform(x, b::Foo2) = x ./ b.b
now, I know that it is possible to also write the following (which I describe as “applying a struct as a function” in the title:
(a::Foo1)(x) = x .* a.a
(b::Foo2)(x) = x ./ b.b
The latter leads to more readable code in my case (in my case, my function name would e.g. be something like interpolate while the name which I’ll attach to the instance will be something like interpolation, in which case you’d get long expressions like interpolate(interpolation, x). Of course you can abbreviate things, but at some point that increases cognitive overhead for me).
Before I embark on a journey of slowly making a transition to this latter way of writing this throughout my code-base, are there are performance pitfalls I must be aware of? Would implementing interpolation(x) in the obvious way just amount to nicer syntax compared to interpolate(interpolation), or is there a difference in how julia treats this?
For what it is worth: I will not be defining only one such method per struct, I need to dispatch based on input types quite often.
Aside: I would tend to just define (a::Foo1)(x) = x * a.a. If you want to vectorize the application of a function, do it at the call site as a.(x). This way it will fuse with other “dot calls”, not to mention being clearer.
Just one thing. If you have vectors of the abstract FooType, i.e. some entries are Foo1, some are Foo2, Foo3 etc. This dispatch thing may not be the most efficient. In that case it might be faster to do manual dispatch based on a stored Int or something.
will give an error, but is intuitively kind of what I’m after. Is the simplest approach really to add an additional argument to dispatch on so I can get the in-place version, such as:
Yes, if the in-place logic is simple enough that it doesn’t actually need to be in its own function and everything can be thought of elementwise that’s definitely nicer. Sometimes the real cases are a lot messier than that though and one does want it wrapped up in a function.
These are all good points, I’ve written a slightly better MWE. If I’m going with the fourth version (call syntax, in-place), then I’d probably use a different symbol to avoid accidentally triggering it if I made a mistake while typing a negation…
using BenchmarkTools
struct Foo1 a::Float64 end
# Apply function
apply_foo(foo::Foo1, x::AbstractVector) = x .- (foo.a * mean(x))
# Apply function in-place
apply_foo!(foo::Foo1, x::AbstractVector) = (x .-= foo.a * mean(x); x)
# Apply function using call syntax
(foo::Foo1)(x::AbstractVector) = x .- (foo.a * mean(x))
# Apply function using call syntax, in-place
(::typeof(!))(foo::Foo1, x::AbstractVector) = (x .-= foo.a * mean(x); x)
function test()
foo = Foo1(0.1)
x = [1.,2.,3.]
# Benchmarking different ways to apply the function
apply_foo(foo, x); apply_foo!(foo, x); foo(x); !(foo, x) # Warm up
display(@benchmark apply_foo($foo, $x))
display(@benchmark apply_foo!($foo, $x))
display(@benchmark ($foo)($x))
display(@benchmark !($foo, $x))
end
test()
For an in-place/mutating method I would just use a different function name with a !, e.g. evalfoo!(foo, x). This way you follow the standard Julia convention for naming mutating functions.
Yes, it’s a little more verbose, but it’s worth it IMO to be explicit about the side effect. Code that uses mutating functions is usually lower-level performance-critical code anyway, which can afford to be more verbose.
But at this point we’re down to bikeshedding, not performance questions as in the original post.
One way to avoid bikeshedding for the mutating function name is to instead implement the MutableArithmetics.jl interface (operate!) for the non-mutating version, providing the mutating version.