Is broadcast possible over ternary expressions?

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! :stuck_out_tongue:

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

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 :wink: