How to control the output of broadcast?

calling fun.() below returns an Array of NamedTuples:

julia> function fun(x)::NamedTuple
           (a = x + 1, b = x - 1)
       end
fun (generic function with 1 method)

julia> fun.([1, 2, 3])
3-element Array{NamedTuple{(:a, :b),Tuple{Int64,Int64}},1}:
 (a = 2, b = 0)
 (a = 3, b = 1)
 (a = 4, b = 2)

however, I want the output to be:

(a = [2, 3, 4], b = [0, 1, 2])

i.e., a NamedTuple of Arrays

of course I could manipulate the returns from fun., but is there a direct way to control the output of “dot” functions?

thanks.

the dot is just apply the function to each element of the array and that’s it.

You get what you should in this case.

1 Like

You put the broadcast where it needs to be.

julia> fun(x)::NamedTuple = (a = x .+ 1, b = x .- 1)
fun (generic function with 1 method)

julia> fun([1,2,3])
(a = [2, 3, 4], b = [0, 1, 2])

1 Like

Dotted functions are special syntax, they’re not calling different versions of the original function. Since the broadcasting behaviour depends on the input types of the arguments, changing it in a generic way would probably be type piracy and have a bunch of unintended side effects in the case of your Array example.

For more info about broadcasting and how it works, see here.

1 Like

You can kind of do it by combining StructArrays and LazyArrays:

julia> using StructArrays

julia> using LazyArrays: @~

julia> x = 1:2
1:2

julia> f(x) = (a=x+1, b=x-1)
f (generic function with 1 method)

julia> y = StructVector(@~ f.(x))
2-element StructArray(::Array{Int64,1}, ::Array{Int64,1}) with eltype NamedTuple{(:a, :b),Tuple{Int64,Int64}}:
 (a = 2, b = 0)
 (a = 3, b = 1)

julia> y.a
2-element Array{Int64,1}:
 2
 3

julia> y.b
2-element Array{Int64,1}:
 0
 1

julia> fieldarrays(y)
(a = [2, 3], b = [0, 1])
4 Likes

today I finally “find out” a solution! :grinning: let’s review the original situation:

f(x) = (a = g(x), b = h(x))
g(x) = x + 1
h(x) = x - 1

f.([1, 2, 3])

julia> f.([1, 2, 3])
3-element Array{NamedTuple{(:a, :b),Tuple{Int64,Int64}},1}:
 (a = 2, b = 0)
 (a = 3, b = 1)
 (a = 4, b = 2)

the trick is to

  1. wrapping Base.broadcasted
  2. override Base.materialize (and Base.materialize! if needed):
struct MyBC{Tf, S}
    content::S
    MyBC{Tf}(x) where {Tf} = new{Tf, typeof(x)}(x)
end

@inline Base.broadcasted(::typeof(f), x) = MyBC{typeof(f)}(x)
@inline Base.materialize(mybc::MyBC{typeof(f)}) = 
        (a = Base.materialize(Base.broadcasted(g, mybc.content)), 
         b = Base.materialize(Base.broadcasted(h, mybc.content)) )

julia> f.([1, 2, 3])
(a = [2, 3, 4], b = [0, 1, 2])

hope it helps :smiley:

This implementation is not composable with the rest of broadcasting mechanism. For example, this does not work:

sumab(x) = x.a + x.b
sumab.(f.([1, 2, 3]))

I think you need to use a custom broadcasting style to make it better.

Yes and No. Remember that, as far as I know, broadcasting is original aimed for scalar functions. But now f() returns a pair of values (i.e. not a scalar), so naturally it cannot be chained with other broadcasting unless it’s the last one:

julia> f.([1, 2, 3] .* 2 .+ 1)
(a = [4, 6, 8], b = [2, 4, 6])

which is fine.

or, if it’s not the last function, we could e.g.:

temp = f.([1, 2, 3])
julia> (temp[1] .* 2 .+ 1, temp[2] .* 3 .- 2)
([5, 7, 9], [-2, 1, 4])

that said, as f() is not a scalar function, we should not expect it could directly be broadcasted (in the middle of the chain) with others.

as I understand, a custom broadcasting style is used for a custom typed argument, e.g. a new type of vector. But now the thing needed to be specially handled is the function to be broadcasted, so I think it’s irrelevant to broadcasting style.
Actually, this implementation handles different types of argument smoothly. For example, we can broadcast on a tuple rather than a vector:

julia> f.((10, 20) )
(a = (11, 21), b = (9, 19))