Julia: UnitRange broacasting with literals gives a Vector

Created on 13 Nov 2017  Â·  9Comments  Â·  Source: JuliaLang/julia

The lowering of dotted operators can have surprising effects when literals are mixed with ranges. In particular:

julia> a = 1:4
1:4

julia> a .+ 1    # Not a range!
4-element Array{Int64,1}:
 2
 3
 4
 5

julia> b = 1
1

julia> a .+ b  # A range, as is probably desired
2:5

julia> broadcast(+, a, 1)  # Also a range
2:5

The reason is that a .+ 1 is lowered to broadcast(x->x+1, a), so this doesn't match the special broadcast rule broadcast(::typeof(+), r::AbstractRange, x::Number).

In 0.6 this perhaps wasn't a big problem as you could use +(r::Range, x::Number), but this has been deprecated. However, the problem isn't specific to ranges - it's a general problem for any situation where you'd like the output type to depend on the function being broadcasted.

broadcast

Most helpful comment

Ah, savor the sour fruits of breaking referential transparency.

All 9 comments

@Keno this was your gripe from today.

See #23445 and the linked issues/PRs. In particular https://github.com/JuliaLang/julia/issues/23445#issuecomment-331622677

I really think we should revisit the special casing of literals in broadcast and see if we can’t get the same effect with constant propagation. @vtjnash has been working on that, so I wonder if it may soon be good enough for this.

Improved constant propagation sounds like a great plan.

If that doesn't pan out in the short term, perhaps we could change the lowering to allow access to the rawer form in a way which doesn't break things. It'd be ugly internally, but we could give access to both the original function and the closure capturing the literals if a .+ 1 lowered to broadcast_opt(broadcast_raw(+, a, 1), x->x+1, a)

And in turn we had the following default implementations:

struct NoRawBroadcast; end
broadcast_raw(args...) = NoRawBroadcast()
broadcast_opt(::NoRawBroadcast, f, args...) = broadcast(f, args...)
broadcast_opt(a, args...) = a

Users would then overload braodcast_raw(::typeof(+), ::AbstractRange, ::Number) to fix the literal problem.

I know, it's a pretty ugly solution. But it does show that lowering gives us some options to solve this with the current state of the compiler technology.

As part of #23939 we could give AbstractRange its own BroadcastStyle and appropriate combination rules.

Would that help? It seems to me we need something more like Jameson's work in https://github.com/JuliaLang/julia/pull/23692 to solve this issue.

[Edit] which is essentially attacking the same problem as the hack I proposed above, but in a more general and non-disgusting way :-)

@timholy we need to specialize the output container based on the input containers and the operator, unitrange .+ integer --> unitrange, potentially unitrange .+ unitrange --> range, etc. Here it's that the dot fusion at lowering that masks the use of +.

Constant propagation may solve the literal problem but it's a symptom of fusion in general. I agree with @c42f that it seems to me we either need something like #23692, or perhaps to make broadcast lazy-and-not-fused-at-lowering.

Hmm, yes, here we do need to pay attention to the operator.

When it's operator-specific then we could even consider just short-circuiting the whole call. But that would presumably run into trouble with 1 .+ r .- 1. So either the combination rules need to take the operator into account, and/or we need #23692.

Ah, savor the sour fruits of breaking referential transparency.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sbromberger picture sbromberger  Â·  3Comments

StefanKarpinski picture StefanKarpinski  Â·  3Comments

omus picture omus  Â·  3Comments

musm picture musm  Â·  3Comments

i-apellaniz picture i-apellaniz  Â·  3Comments