Document splatting behaviour in broadcasting

Hello!

This use of splatting just turned up in my code:

# using Pkg
# Pkg.add("ColorTypes")
using ColorTypes

_colors = [[0.0,0.2,1.0], [0.0,0.5,0.0], [1.0,0.0,0.0]];
_color = RGB.(_colors...)
# 3-element Array{RGB{Float64},1} with eltype RGB{Float64}:
#  RGB{Float64}(0.0,0.0,1.0)
#  RGB{Float64}(0.2,0.5,0.0)
#  RGB{Float64}(1.0,0.0,0.0)

Here, splatting is applied somehow “before” broadcasting, in the sense that it splats each of _colors subarrays, and then broadcasts over each component.

However, what I might have expected is the output produced by this:

_color = [RGB(a...) for a in _colors]
# 3-element Array{RGB{Float64},1} with eltype RGB{Float64}:
#  RGB{Float64}(0.0,0.2,1.0)
#  RGB{Float64}(0.0,0.5,0.0)
#  RGB{Float64}(1.0,0.0,0.0)

Is this behavior documented anywhere?

Thanks :slight_smile:

1 Like

I think this behaviour feels natural to me at least, the splatting is “further in” into the expression and evaluated before is my intuition.

Looking here we see that doing f.(a,b) is pretty much the same as broadcast(f, a, b), meaning that your example with RGB.(_colors...) would be the same as calling broadcast(RGB, _colors...) where it is reasonable that the splatting should happen before entering the broadcast function.

In order to get the splatting at the outer level, you need to broadcast a function that takes one argument, and splats it into a call to RGB. This is what Base.splat does for you (although I’m not sure whether it is part of the public API).

julia> using ColorTypes

julia> _colors = [[0.0,0.2,1.0], [0.0,0.5,0.0], [1.0,0.0,0.0]];

julia> _color = RGB.(_colors...)
3-element Array{RGB{Float64},1} with eltype RGB{Float64}:
 RGB{Float64}(0.0,0.0,1.0)
 RGB{Float64}(0.2,0.5,0.0)
 RGB{Float64}(1.0,0.0,0.0)

julia> _color = Base.splat(RGB).(_colors)
3-element Array{RGB{Float64},1} with eltype RGB{Float64}:
 RGB{Float64}(0.0,0.2,1.0)
 RGB{Float64}(0.0,0.5,0.0)
 RGB{Float64}(1.0,0.0,0.0)
3 Likes

@ffevotte’s solution is awesome.

Still not very clear why the OP’s example broadcasts as shown, but just to add that the desired result can be achieved with a little more splatting work:

RGB.(eachrow(hcat(_colors...))...)

 RGB{Float64}(0.0,0.2,1.0)
 RGB{Float64}(0.0,0.5,0.0)
 RGB{Float64}(1.0,0.0,0.0)

But that’s what parentheses do, right? They group things and enforce precedence. In

a * (b + c) 

won’t you expect the contents of the parentheses to execute first?

BTW, this

should be a tuple:

_colors = ([0.0,0.2,1.0], [0.0,0.5,0.0], [1.0,0.0,0.0]);

Splatting vectors is not great, and RGB takes three arguments, so a 3-tuple is exactly the right concept.

I do not care much about the whole tuple/vector since this was a MWE.

But it was better to have an array of arrays so that the output was always an Array of RGB , and thus more visually consistent.

Okay, this was amazing. I had absolutely no idea that this existed, and it’s awesome. Thanks!

Still, wouldn’t it be better that this behavior of ... was documented somewhere?

No, the output would be the same, only the performance is better, and it makes more sense.

I don’t really agree with the ‘just an MWE’ way of thinking. I always assume that in an MWE, almost everything is significant, or it wouldn’t be ‘minimal’. Also, the fact that you misunderstand what the output would be, tells me that it was relevant information.

I guess it’s not so much the behavior of ... that surprises you, but rather whether it applies to the call to RGB itself (what you’d have expected) or the broadcast as a whole (what actually happens).

As pointed out by @albheim earlier in this thread, there is a sentence explaining what goes on in the reference documentation for broadcast:

In fact, f.(args...) is equivalent to broadcast(f, args...)

Still, your being surprised by this behavior might be a good indication that this is not sufficient, and you’re probably in the best position to help improve the situation by suggesting which parts of the documentation need fixing, and how they might be made clearer.