Broadcasted assignment of custom types

broadcast

#1

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?


#2

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)

#3

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)


#4

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


#5

@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
=# 

#6

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


#8

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.


#9

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])

#10

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])

#11

@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.


#12

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