Broadcasted assignment of custom types

This works natively:

a = Vector{Int}(undef, 2)
a .= 2

However, this does not work, failing with a cryptic error:

struct Foo
    x::Int
end
a = Vector{Foo}(undef, 2)
a .= Foo(2)
#=
ERROR: MethodError: no method matching length(::Foo)
Stacktrace:
 [1] _similar_for(::UnitRange{Int64}, ::Type, ::Foo, ::Base.HasLength) at .\array.jl:532
 [2] _collect(::UnitRange{Int64}, ::Foo, ::Base.HasEltype,
::Base.HasLength) at .\array.jl:563
 [3] collect(::Foo) at .\array.jl:557
 [4] broadcastable(::Foo) at .\broadcast.jl:617
 [5] broadcasted(::Function, ::Foo) at .\broadcast.jl:1166
 [6] top-level scope at none:0
=#

Is there a reason not to provide for broadcasted assignment of custom types into an array of that type?

1 Like

Types default to be iterable-y and not scalar-y. So do something like:

julia> a .= (Foo(2),)
2-element Array{Foo,1}:
 Foo(2)
 Foo(2)

or define

julia> Broadcast.broadcastable(a::Foo) = (a,)

julia> a .= Foo(2)
2-element Array{Foo,1}:
 Foo(2)
 Foo(2)
4 Likes

Interesting, okay. Would it do any harm, or is it possible, to detect if length is defined for a type, and if not, treat it as scalar? (Thinking about whether it is worth suggesting a change to the language)

See recent broadcast changes (iterate by default), scalar struct, and `@.` and linked issues/PRs.

2 Likes

@kristoffer.carlsson, Your suggestion allows me to broadcast to an array range, but not to a single element of the array.

a = Vector{Foo}(undef, 3)
a[1:2] .= Foo(2) # works
a[3] .= Foo(2)   # error
#=
ERROR: MethodError: no method matching size(::Foo)
Stacktrace:
 [1] axes at .\abstractarray.jl:75 [inlined]
 [2] materialize!(::Foo, ::Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple},Nothing,typeof(identity),Tuple{Tuple{Foo}}}) at .\broadcast.jl:759
 [3] top-level scope at none:0
=# 

Use = for that? It doesn’t work for other types either.

1 Like

Ah, nevermind the deleted post, I thought it worked for numbers.

Okay, so I need to check whether my array lookup will be one element or multiple elements and then have special cases for each? Seems like that shouldn’t be necessary.

EDIT: Ah, so a[3:3] works okay but a[3] does not. That’s okay then, I suppose.

I might be stating the obvious, but beware it’s one and the same object, which can lead to surprises if your objects are not fully immutable:

julia> struct Foo x::Vector{Int} end

julia> a = Vector{Foo}(undef, 2);

julia> a .= (Foo([2]),)
2-element Array{Foo,1}:
 Foo([2])
 Foo([2])

julia> push!(a[1].x, 3);

julia> a
2-element Array{Foo,1}:
 Foo([2, 3])
 Foo([2, 3])
1 Like

Btw, another way of creating a with identical objects is:

julia> struct Foo x::Vector{Int} end

julia> a = fill(Foo([2]), 2)
2-element Array{Foo,1}:
 Foo([2])
 Foo([2])

(There’s also fill!.) In case you want unique objects, you can do:

julia> a = 1:2 .|>_-> Foo([2])
2-element Array{Foo,1}:
 Foo([2])
 Foo([2])

julia> push!(a[1].x, 3);

julia> a
2-element Array{Foo,1}:
 Foo([2, 3])
 Foo([2])
3 Likes

@bennedich - Took me a moment to realize what you were saying, that only one object is created and then each array item points to it. Thank you, that’s good to remember.

Also, didn’t know Julia had the |> operator for function chaining, so thanks for showing me that.