I would love to be able to do something like the following:
@. l < u ? f!(x) : g!(x)
I.e.
[li<ui ? f!(xi) : g!(xi) for (li, ui, xi) in zip(l, u, x)]
However, expanding the first line, it’s clear that it won’t be equivalent to the second, due to there being nothing like ?.
and :.
. (the whole thing is directly parsed as one if-else statement)
julia> @macroexpand @. l < u ? f!(x) : g!(x)
:(if l .< u
f!.(x)
else
g!.(x)
end)
As a trivial example of what happens when you try the above:
julia> @. [-1] < [1] ? [0] : [1]
ERROR: TypeError: non-boolean (BitArray{1}) used in boolean context
Using ifelse
will broadcast correctly, but because it evaluates both arguments ahead of time, it cannot be used exactly how I would like, since the functions f!
and g!
will both be applied (admittedly a niche limitation).
I’m assuming there isn’t already a way of doing what I’d like in a simple expression, but if there is, I’d appreciate a pointer. If indeed there isn’t, could it be possible to allow something like this in the language? Assuming the mechanism for turning ternary expressions into if-else statements stays the same, then presumably this would require defining broadcast behavior over if-else statements. It seems to me that this could at least be done with @.
(it could modify the expression as a special case) but not easily (or at all?) with the regular broadcast
approach, since my understanding is that it applies only to functions.
Interested to hear others’ thoughts on this.
You can do this with ifelse
because that evaluates it’s arguments, and can’t short circuit the way ternaries can.
3 Likes
Yeah, there’s not such a construct AFAIK. In general broadcast does not apply to control flow, which as you note, ?:
is just a shorthand for if ... else ... end
. I’d be very hesitant to introduce such a feature.
2 Likes
In this case could you just write your own function that works for a single value/comparison and then broadcast that?
1 Like
Yeah, there’s not such a construct AFAIK. In general broadcast does not apply to control flow
Yes, it would certainly have to modify control flow, turning one if-else into something like a generator or map with if-else statements. I’m not familiar with the broadcast machinery that makes differently indexible object all work together nicely, but the idea would be, in addition to propagating broadcast
to subexpressions in an @. if ...
statement, also converting the top level expression into something amenable to iteration. The comprehension in the original post was a poor imitation.
In this case could you just write your own function that works for a single value/comparison and then broadcast that?
Yes, in this case it would be simple enough to do:
h!(li, ui, xi) = li<ui ? f!(xi) : g!(xi)
h!.(l, u, x)
In my “real” use case, it’s much uglier to do it this way than if the single ternary expression were possible. Of course all of this is about aesthetics!
You can do this with ifelse
because that evaluates it’s arguments, and can’t short circuit the way ternaries can.
Unfortunately, if f!
and g!
have some effect on xᵢ
(or otherwise), applying both of them in the ifelse
will be different than only the applying the “correct” one in an if-else
.
1 Like
Looking at the __dot__
macro, it looks like some blocks are already special cased.
E.g. @. for
doesn’t distribute the broadcast to the for i = 1:n
.
Since a for-loop is also control-flow, perhaps this is less extreme than it first sounded? I still don’t know what the if-else expression would have to change to to work as expected, but whatever that may be, I guess it could happen under elseif x.head == :if
That’s funny, I have exactly the opposite takeaway. Note that @. for
doesn’t actually embed the loop inside the broadcast — it ignores it and keeps it as outer control flow! That is:
julia> x = 1:4
1:4
julia> @. for i in 2:3
println(x^i)
end
1
4
9
16
1
8
27
64
The semantics you want would (I think) print 1, 1, 4, 8, 9, 27, 16, 64 and would return a vector of nothing
s.
1 Like
Hmm, yes I see your point. I suppose it boils down to what the intuition is behind the syntax:
@. if l < u
x1
else
x2
end
Ignoring the non-scalar l/u
case (non-boolean error), the above is equivalent to
if l < u
@. x1
else
@. x2
end
I see your point that this is in line with the approach to for
, but I still think it’s missing out on something more useful, which is broadcasting over the entire conditional