Base.Fix2 can be constructed using broadcasting syntax but does not retain broadcasting functionality

Example:

a = [1,2,3]

(>=(2)).(a) # works as intended 
.>=(2)  # can be constructed 
.>=(2).f # fields do not retain information that function should be broadcasted
(.>=(2))(a) # breaks 
(x -> x .>= 2)(a) # intended behavior.  
.>=(a, 2) # also intended behavior. 

Is there a reason that Base.Fix2 does not retain information that the function should be broadcasted, or is this an oversight? If there is a reason, then why isn’t an exception thrown when .>=(2) it is constructed?

Okay, this is definitely a little bit of a journey. Two things to note at the top.

  1. Ints in Julia are iterable.
julia> collect(1)
0-dimensional Array{Int64, 0}:
1
  1. Broadcasting attempts to materialize a vector with the result of the broadcast eagerly.

When you run .>=(2), it calls the top-level Base.broadcast function which in turn calls materialize(broadcasted(f, As...)). broadcasted returns what you’d expect: Base.Broadcast.Broadcasted(>=, (1,)), but materialize returns the surprising Base.Fix2(>=, 2).

Digging into materialize, it normally uses similar to create a destination Array and then copies the result of broadcasting f over the arguments. In this case, where the input is a “0-dimensional Array” (an Int), it instead returns the result without an Array wrapper (because you couldn’t access the contents of a 0-dim Array to get the value).

Edit:
To get a lazy broadcasting object, you could do:

julia> x = Base.Fix1(broadcast, >=(2))
(::Base.Fix1{typeof(broadcast), Base.Fix2{typeof(>=), Int64}}) (generic function with 1 method)

julia> x([1,2,3])
3-element BitVector:
 0
 1
 1
3 Likes

Great sleuthing and excellent exposition! I understand now why it behaves as it does.

Would it make sense for Julia Base to add something like:

fix2_functions = [>=, <=, ==]  # and other functions for which Base.Fix2 is called when the function is applied to a single argument 

# For exposition purposes, using ::NTuple{Any, 1} even though equivalent to the more parsiminous ::Tuple 
Base.broadcast(f::Union{typeof.(fix2_functions)...}, args::NTuple{Any, 1}...) =  
    Base.Fix1(broadcast, f(args...)) 

Current behavior of .>=(2) is unintuitive. The only reason someone would ever write .>=(2) is if to broadcast the comparison over some as-yet undefined iterable.

The question of how to do partial application in Julia is… fraught. Lots of ideas about how to do it, what the rules should be, etc., but not a lot of consensus or momentum.

I think special casing a few functions is not great.

If I were in charge, I’d use underscores for partial application, so the way to do this would be:

julia> f = _.>=2

julia> f([1,2,3])
[false, true, true]

But I’m not in charge, thankfully :slight_smile:

This is exactly the same as .√(2) — that will call sqrt on all the element(s) in its argument and return the result(s). Similarly .>(2) calls > on all the element(s) in its arg and returns the result(s). In both cases there’s exactly one (unwrapped) result — 1.414... and a Fix2, respectively.

It’s just tricky in this case because calling > on a single arg returns a function to be later called. It’s true that you wouldn’t usually use construct an array of functions like this, but you totally can — and you can even do something useful with it. Here I’ll apply different thresholds to each column:

julia> checks = .>([-1 -0.5 0 0.5 1])
1×5 Matrix{Base.Fix2{typeof(>), Float64}}:
 Fix2{typeof(>), Float64}(>, -1.0)  …  Fix2{typeof(>), Float64}(>, 1.0)

julia> randn(10,5) .|> checks
10×5 BitMatrix:
 1  0  1  1  0
 1  1  0  0  0
 1  1  1  1  0
 1  1  0  1  0
 1  1  1  0  0
 1  1  0  1  0
 1  1  0  0  0
 1  1  0  1  1
 1  0  1  1  0
 1  0  1  0  0
1 Like

Given that numbers are iterable in Julia v1, I agree with @mbauman. There is no way to generically look at a type and know if it should be treated as a vector-like thing or a scalar thing.

In a bigger picture sense, though, I wish that numbers were not iterable so that it could be meaningful to distinguish between Base.broadcast(typeof(>=), ::Number) and Base.broadcast(typeof(>=), ::AbstractArray).

Ah, this makes a lot of sense.

Given that my question is actually a special case for the more general question of what syntax Julia should use for partial application (mrufsvold 2nd reply in thread) – the answer to which has not been decided – nothing should be done about my specific question.

Thanks!

1 Like

There are huge advantages to “pushing” the . down to the time that you use it — specifically it will then fuse with other .s in the same expression. It also generally tracks with Julia’s best practices — define the “scalar” operation and then use it broadcastedly at the call site when necessary.

In other words, your (.>=(2))(a) # breaks example should be written as (>=(2)).(a), or perhaps more readably:

op = >=(2)
op.(a)
6 Likes