Curried Broadcasting Comparison Operators

This is not what I expected:

julia> >(2) === .>(2)
true

julia> .>(1:4)
4-element Vector{Base.Fix2{typeof(>), Int64}}:
 (::Base.Fix2{typeof(>), Int64}) (generic function with 1 method)
 (::Base.Fix2{typeof(>), Int64}) (generic function with 1 method)
 (::Base.Fix2{typeof(>), Int64}) (generic function with 1 method)
 (::Base.Fix2{typeof(>), Int64}) (generic function with 1 method)

Intuitively, I thought .>(y) would be a broadcast(>, _, y) partial applicator (in lambda terms, x->broadcast(>, x, y)), but instead it’s a broadcast(Base.Fix2, >, y)—making it so that it’s not a function, but an array of functions, and thus is not a callable object.

This is inconsistent with, for example, .!, which is a callable object and can be directly called on a collection:

julia> .!((true, false, true))
(false, true, false)

For comparison, .>(y) can’t be called on anything:

julia> (>(2))(1:4)
ERROR: MethodError: no method matching isless(::Int64, ::UnitRange{Int64})

julia> (.>(2))(1:4)
ERROR: MethodError: no method matching isless(::Int64, ::UnitRange{Int64})

julia> (.>(1:4))(2)
ERROR: MethodError: objects of type Vector{Base.Fix2{typeof(>), Int64}} are not callable

julia> (.>(1:4)).(2)
ERROR: MethodError: objects of type Vector{Base.Fix2{typeof(>), Int64}} are not callable

julia> (.>(1:4))(4:-1:1)
ERROR: MethodError: objects of type Vector{Base.Fix2{typeof(>), Int64}} are not callable

julia> (.>(1:4)).(4:-1:1)
ERROR: MethodError: objects of type Vector{Base.Fix2{typeof(>), Int64}} are not callable

I can only get it to do something under three scenarios:

julia> (.>(2))(2)
false

julia> (.>(2)).(1:4)
4-element BitVector:
 0
 0
 1
 1

julia> ((x,y)->x(y)).(.>(1:4), 4:-1:1)
4-element BitVector:
 1
 1
 0
 0

However, the first two scenarios are already covered by (non-broadcasted) >(y):

julia> (>(2))(2)
false

julia> (>(2)).(1:4)
4-element BitVector:
 0
 0
 1
 1

So really, .>(y) (and all the other curried broadcasting binary operators) only do something useful in the case where a function which calls another function is being broadcasted.

Maybe I’m just unimaginative, but can someone help me out with finding a use case for this, and why this behavior is preferred over a partial function applicator on broadcast, e.g. Fix{(1,3),3}(broadcast, >, 1:4) (supposing that specializations on broadcast would be written for Fix{typeof(broadcast)} objects)? I can imagine use cases for (.>(2))(1:4) or for (.>(1:4))(2), but I’m having trouble imagining use cases for its current behavior.

1 Like

It’s just how broadcasting works in Julia: f.(1:4) == [f(1), f(2), f(3), f(4)].
Substitute f with >, and you get .>(1:4) == [>(1), >(2), >(3), >(4)], that is an array of functions.
And of course this is consistent with !: .!((true, false, true)) == (!(true), !(false), !(true)) == (false, true, false).

1 Like

Relevant discussion: Broadcasting tuple functions · Issue #22129 · JuliaLang/julia · GitHub

You can get the desired behavior with the (very verbose) Base.Broadcast.BroadcastFunction, which “dots” the function it wraps:

julia> Broadcast.BroadcastFunction(<(2))
Base.Broadcast.BroadcastFunction(Base.Fix2{typeof(<), Int64}(<, 2))

julia> Broadcast.BroadcastFunction(<(2))([1, 2, 3])
3-element BitVector:
 1
 0
 0

julia> Broadcast.BroadcastFunction(<)
Base.Broadcast.BroadcastFunction(<)

julia> Broadcast.BroadcastFunction(<)([1, 2, 3])
3-element Vector{Base.Fix2{typeof(<), Int64}}:
 (::Base.Fix2{typeof(<), Int64}) (generic function with 1 method)
 (::Base.Fix2{typeof(<), Int64}) (generic function with 1 method)
 (::Base.Fix2{typeof(<), Int64}) (generic function with 1 method)

julia> Broadcast.BroadcastFunction(<)([1, 2, 3], [3, 2, 1])
3-element BitVector:
 1
 0
 0

But I agree that given the verbosity of this, it would be great to have something simpler. .<(2) can’t work for syntactic reasons. But if .! and .< are how you create the dotted versions of those functions, why not just allow .f to mean “f, but dotted” for any function f. So .<(2) would still broadcast < over 2, giving (::Base.Fix2{typeof(<), Int64}), but .(<(2)) would give Base.Broadcast.BroadcastFunction(Base.Fix2{typeof(<), Int64}(<, 2)).

You can just do <(2).(x) to broadcast <(2), like f.(x) for any other function f:

julia> <(2).([3,2,1])
3-element BitVector:
 0
 0
 1

Yes, but you can’t use this to pass the broadcasted function around as a first-class object. This only allows you to call the function immediately.