[a...] vs [a]

What does this line of code mean?

adds = add isa Union{AbstractArray, Tuple}? [add...] : [add]

Here ‘add’ is supposed to be a vector or a single number.

It makes an adds array, regardless of whether add is a scalar, array, or tuple. The [add...] is to make an array (the brackets) consisting of the elements of add, where ... means splat: list the elements as if not in an array. This is intended to work with either array or tuple. Finally, if add is a scalar, the ternary else [add] puts that into a 1-element array.

This code looks like the clunky stuff I would write. I imagine there are cleaner ways to do this.

2 Likes

I think the idiomatic way is to primarily write code for scalars, and broadcasting explicitly when using it on a vector. Or at least doing

toadds(adds::AbstractArray) = adds
toadds(adds::Tuple) = collect(adds)
toadds(add::T) where {T} = Vector{T}(add)

Splatting a potentially large vector is discouraged.

6 Likes

The expression [a...] also works when a is a scalar number

2 Likes

First, let’s see what that expression does.

julia> adds(add) = add isa Union{AbstractArray, Tuple} ? [add...] : [add]
adds (generic function with 1 method)

julia> adds(5)
1-element Vector{Int64}:
 5

julia> adds([1,2])
2-element Vector{Int64}:
 1
 2

julia> adds(1:5)
5-element Vector{Int64}:
 1
 2
 3
 4
 5

julia> adds((1,2,3))
3-element Vector{Int64}:
 1
 2
 3

Now let’s try an alternate implementation.

julia> adds(add) = [add...]
adds (generic function with 1 method)

julia> adds(5)
1-element Vector{Int64}:
 5

julia> adds([1,2])
2-element Vector{Int64}:
 1
 2

julia> adds(1:5)
5-element Vector{Int64}:
 1
 2
 3
 4
 5

julia> adds((1,2,3))
3-element Vector{Int64}:
 1
 2
 3

The second version works because numbers are also iterable in Julia. Iteration of a number just provides a number.

Let’s do some type analysis.

julia> @code_warntype adds(5)
MethodInstance for adds(::Int64)
  from adds(add) @ Main REPL[15]:1
Arguments
  #self#::Core.Const(adds)
  add::Int64
Body::Vector{Int64}
1 ─ %1 = Core._apply_iterate(Base.iterate, Base.vect, add)::Vector{Int64}
└──      return %1


julia> @code_warntype adds((1,2))
MethodInstance for adds(::Tuple{Int64, Int64})
  from adds(add) @ Main REPL[15]:1
Arguments
  #self#::Core.Const(adds)
  add::Tuple{Int64, Int64}
Body::Vector{Int64}
1 ─ %1 = Core._apply_iterate(Base.iterate, Base.vect, add)::Vector{Int64}
└──      return %1


julia> @code_warntype adds(1:5)
MethodInstance for adds(::UnitRange{Int64})
  from adds(add) @ Main REPL[15]:1
Arguments
  #self#::Core.Const(adds)
  add::UnitRange{Int64}
Body::Union{Vector{Any}, Vector{Int64}}
1 ─ %1 = Core._apply_iterate(Base.iterate, Base.vect, add)::Union{Vector{Any}, Vector{Int64}}
└──      return %1


julia> @code_warntype adds((1,2,3))
MethodInstance for adds(::Tuple{Int64, Int64, Int64})
  from adds(add) @ Main REPL[15]:1
Arguments
  #self#::Core.Const(adds)
  add::Tuple{Int64, Int64, Int64}
Body::Vector{Int64}
1 ─ %1 = Core._apply_iterate(Base.iterate, Base.vect, add)::Vector{Int64}

We see there is some type instability in the case of a UnitRange{Int}.

We can addess that.

julia> adds(add::AbstractVector{T}) where T = T[add...]
adds (generic function with 2 methods)

julia> @code_warntype adds(1:5)
MethodInstance for adds(::UnitRange{Int64})
  from adds(add::AbstractVector{T}) where T @ Main REPL[26]:1
Static Parameters
  T = Int64
Arguments
  #self#::Core.Const(adds)
  add::UnitRange{Int64}
Body::Vector{Int64}
1 ─ %1 = add::UnitRange{Int64}
│   %2 = Core.tuple($(Expr(:static_parameter, 1)))::Core.Const((Int64,))
│   %3 = Core._apply_iterate(Base.iterate, Base.getindex, %2, %1)::Vector{Int64}

In summary, the function seems to be able to take a an array, scalar number, or tuple and return a Vector. The implementation can be simplified for numbers since they are iterable. If this function were to be applied to non-number scalars, it might not work. Type stability issues can be addressed by adding a type to the vector construction.

6 Likes

does not the function adds do the same as collect?

No, collect does not always return a Vector, since it keeps the “shape”:

julia> collect([1 2; 3 4])
2×2 Matrix{Int64}:
 1  2
 3  4

Returning to OP’s question, there are some good replies here. I was saying “like the clunky stuff I would write.” In Matlab, you often want a function to work equally well with scalars or arrays, so I suspect this code is ported from Matlab, as many of us have done in the past.

However, @gustaphe points out this can usually be avoided by broadcasting, which is also more idiomatic Julia. Broadcasting uses dot syntax, as in f.(add) will apply function f to add whether its a scalar, tuple, or vector, and will also maintain the original “shape.” Where f should just be written expecting a scalar.

The other alternatives discussed here are better than original. Thanks @mkitti for the type analysis! But they’re still just better ways to propagate the Matlab-ism, i.e. “putting lipstick on a pig.” Broadcasting is the preferred approach.