Handling of single object vs. 1-element array of object

See for example

mutable struct Coords
    x::Float64
    y::Float64
    z::Float64
end
Coords() = Coords(rand(), rand(), rand())
c5 = [Coords() for i in 1:5]
c1a = [Coords() for i in 1:1]
c1b = Coords()

fieldnames(typeof(c1b))    # (:x, :y, :z)
fieldnames(c1a)            # ()
fieldnames(typeof(c1a[1])) # (:x, :y, :z)

length(c5)  # 5
length(c1a) # 1
length(c1b) # throwing error

Before I clobber some code using many if typeof(x)==y, is there a more elegant way to handle or distinguish single objects vs. arrays of objects?

I am not sure I understand what you want but I think in this case multiple dispatch is your friend:

# First method
function myfun(x::Coords)
# Do something with a single element
end

# Second method
function myfun(x::Vector{Coords})
# Do something with an array
end

OK for the example.

I am dealing with an unknown number of types (which could be higher dimensional as well).
So I would prefer to have a test like is_single/is_vector/is_matrix.

You can dispatch on the dimension as well

myfun(x::Cords) = ...
myfun(x::Vector{Coords})
myfun(x::Matrix{Coods})

etc.

Maybe the best bet is to choose a type you think is most natural, Vector, Matrix etc. and then for the other methods, just convert it to that type.

1 Like

Right, but still complicating my code…
I finally came up with this:

julia> is_single(x) = !hasmethod(length, tuple(typeof(x)))
is_single (generic function with 1 method)

julia> is_single(c1a)
false

julia> is_single(c1b)
true

If that works for you, then that’s fine.

One final thing to note is that you can dispatch on AbstractArray, which will catch all arrays of any dimension.

myfun(x::AbstractArray{Coords}) = ...
myfun(x::Coords) = myfun([x])
2 Likes

Note that is_single(1) == is_single([1]) == false, and that this is quite slow. Dispatch on f(x::AbstractArray) vs. f(x) will I think do what you want. Or x isa AbstractArray if you want a test within a function:

julia> @btime is_single(1)
  323.328 ns (3 allocations: 144 bytes)
false

julia> @btime 1 isa AbstractArray
  0.001 ns (0 allocations: 0 bytes)
false
7 Likes

Cool, so

julia> issingle(x) = !(typeof(x) <: AbstractArray)
issingle (generic function with 1 method)

julia> issingle(c1a)
false

julia> issingle(c1b)
true

I rarely come across situations where I don’t know if I’m dealing with a scalar or an array without running the code. This is also why explicit syntactic broadcasting is preferred over dispatching to different methods for scalar vs array (except when you need non-broadcasting behavior, e.g. matrix multiplication).

I suspect a code smell here. Where do these arrays come from, and why can’t you tell if objects are scalar or arrays without running the code?

8 Likes

I’m going to second this. This situation usually comes up from new Julia users coming from Matlab, and the real solution usually is to use broadcasting and not quite vector/matrix versions of your functions.

6 Likes

To elaborate on this, Matlab (and Python and R) want you to push vectorized operations “in” as far as possible so that you can pass large arrays into the primitive operations that are written in fast C code, allowing them to amortize over the entire array and try to make up for the slowness of the outer language. Julia wants you to do the opposite: push vectorization “out” as far as possible and leave it to the very last moment. Express your core operations in terms of scalars and then only do broadcasting when you need to. This works fine because the outer language is fast in itself and doesn’t need you to force everything into vectorized operations. In fact, this approach is typically more efficient since it uses less memory and has better memory locality by combining multiple operations on the same data instead of having to make many passes over the same data computing a stage at a time as Matlab, Python and R would.

10 Likes

Three times agree with the last posters.

Coming from Matlab and translating a Matlab serializer code.
To be specific, a Julia serializer translating into Matlab compatible structures, so departing from a MAT.jl approach using HDF as intermediate format.

So of course I know what is single or Vector, just a bit hesitant to accept multiple dispatch as the panacea. If not sufficiently generalizable to few types, I imagine a lot of code replication. There might be a threshold by the number of ifs to convert to MD though.

Thanks for giving a hand, maybe these little bits and pieces on discourse make it some day into the manual.

1 Like

If you run into any specific bits that feel weird in Julia, feel free to post an example. Usually, the answer is just to use broadcasting, but the specifics are sometimes a little more complicated/require rethinking the approach a little.

3 Likes

Here we go: https://discourse.julialang.org/t/how-to-dispatch-this-serializer-function/65981