# How bad is it if x \< y doesn't return a Bool?

**URL:** <https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611>\
**Category:** General Usage\
**Tags:** method-invalidation, type-piracy\
**Created:** [May 23, 2024, 7:52am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611 "2024-05-23T07:52:41Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [May 23, 2024, 7:52am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/1 "2024-05-23T07:52:41Z")

</div>

Suppose I define a number type `T` for which order comparison is not meaningful: `<(x::T, y::T)` doesn’t make any mathematical sense, because `T` actually contains a set. There are good reasons why I’m doing this, see SparseConnectivityTracer.jl, but that’s not the topic of today.

My problem is that I still want to allow control flow with `ifelse`. In my scenario, `ifelse(x < y, a, b)` should ignore the comparison result and just return some function `f(a, b)` (essentially taking both branches at once).  
How bad is it if I define something like the following?

```julia
struct T <: Real
    s::Set{Int}
end

f(a::T, b::T) = T(union(a.s, b.s))
Base.:<(x::T, y::T) = missing
Base.ifelse(::Missing, a::T, b::T) = f(a, b)

```

It works:

```julia
julia> a, b = T(Set([1, 2, 3])), T(Set([3, 4, 5]));

julia> ifelse(a < b, a, b)
T(Set([5, 4, 2, 3, 1]))

```

But I’m afraid it might lead to invalidations or other bad behavior. How criminal is it to break the convention that `<` must return a `Bool`?

---

<div class="post-metadata">

**Author:** ![Datseris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/datseris/32/13406_2.png) [@Datseris](https://discourse.julialang.org/u/Datseris)\
**Post date:** [May 23, 2024, 8:03am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/2 "2024-05-23T08:03:10Z")

</div>

I would say at least 10 years in prison level of criminal. But it doesn’t have to be.

Why don’t you make a singleton type so

```julia
struct MyTIsLess end
Base.:<(x::T, y::T) = MyTIsLess()

```

so that you extend the method

```julia
Base.ifelse(::MyTIsLess, a::T, b::T) = f(a, b)

```

? There is no type piracy in this approach. Is there any meanigful reason you chose `Missing` initially?

---

<div class="post-metadata">

**Author:** ![Datseris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/datseris/32/13406_2.png) [@Datseris](https://discourse.julialang.org/u/Datseris)\
**Post date:** [May 23, 2024, 8:04am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/3 "2024-05-23T08:04:01Z")

</div>

In any case though, I would choose a different symbol than `<`. You can use many of the supported unicode infix operators.

---

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [May 23, 2024, 8:05am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/4 "2024-05-23T08:05:34Z")

</div>

> [@Datseris](#):
>
> There is no type piracy in this approach.

There is no type piracy in the initial approach either because I overload `ifelse(::Missing, ::T, ::T)` and I own `T`.  
However you’re right: returning a singleton type that I own would allow me to overload `ifelse(::MyTIsLess, ::Any, ::Any)`, which is even better.

Sadly, in both cases, we might cause invalidations if already-compiled methods assume that `<` should return a `Bool`.

> [@Datseris](#):
>
> In any case though, I would choose a different symbol than `<`. You can use many of the supported unicode infix operators.

The point is to use this in an operator overloading approach, so that it works with pre-existing code (à la ForwardDiff). If I need to introduce new symbols, I destroy this hope.

---

<div class="post-metadata">

**Author:** ![lrnv](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lrnv/32/19373_2.png) [@lrnv](https://discourse.julialang.org/u/lrnv)\
**Post date:** [May 23, 2024, 8:09am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/5 "2024-05-23T08:09:25Z")

</div>

If I understand you correctly, your code implies that `(x < y) === (y < x)`, which seems pretty bad.

Maybe a type _holding_ the two parts of the comparison would make more sense :

```julia
struct MyCompare{T}
  x::T
  y::T
end
Base.:<(x::T, y::T) = MyCompare(x,y)
Base.ifelse(C::MyCompare, a::T, b::T) = f(C.x,C.y, a, b)

```

?

Otherwise, you better ensure that `f(a,b) == f(b,a)` !

---

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [May 23, 2024, 8:11am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/6 "2024-05-23T08:11:42Z")

</div>

> [@lrnv](#):
>
> If I understand you correctly, your code implies that `(x < y) === (y < x)`, which seems pretty bad.

In my case that’s exactly what I want. Essentially, I need a way to take _both_ branches of `ifelse` and so the comparison result doesn’t matter.

The context is sparsity detection for Jacobians. I’m trying to determine which input variables x\_j influence which output variables y\_i, because they will correspond to nonzeros in the Jacobian matrix \partial y / \partial x. If I have control flow in my function y(x), I need to consider both branches, because they may lead to different sparsity patterns depending on the value of x. That’s what the sets are about in the struct `T` (they are sets of input indices j), and that’s why I take `f` to be the `union` operation.

---

<div class="post-metadata">

**Author:** ![lrnv](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lrnv/32/19373_2.png) [@lrnv](https://discourse.julialang.org/u/lrnv)\
**Post date:** [May 23, 2024, 8:15am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/7 "2024-05-23T08:15:28Z")

</div>

Ok so the union of sets part is actually what you are doing and not a mock-up ? In which case it seems resonable to use a singleton type as DatSeries proposed with `ifelse(::CompareIsUnion, a, b) = union(a,b)` directly and then `union(a::T,b::T) = T(union(a.set,b.set))`

---

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [May 23, 2024, 8:43am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/8 "2024-05-23T08:43:47Z")

</div>

This will probably lead to method invalidations, right? Are those avoidable?

---

<div class="post-metadata">

**Author:** ![Elrod](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/elrod/32/22461_2.png) [@Elrod](https://discourse.julialang.org/u/Elrod)\
**Post date:** [May 23, 2024, 9:21am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/9 "2024-05-23T09:21:57Z")

</div>

To answer the initial question of “had bad is it”, there are precedents. For example, Symbolics.jl:

```julia
julia> using Symbolics

julia> @variables w x y z;

julia> x < y
x < y

julia> ifelse(x < y, w, z)
ifelse(x < y, w, z)

```

The answer of `ifelse` also contains the union of inputs (i.e., both `w` and `z`), but if you’re only interested in the incidence, you can be more efficient by not preserving the structure.

As an aside, I’d also suggest numbering your inputs and using `BitSet`s for much faster set operations.

The other packages that immediately come to mind which don’t return `Bool` iare the SIMD packages, VectorizationBase.jl and SIMD.jl.

---

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [May 23, 2024, 9:46am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/10 "2024-05-23T09:46:53Z")

</div>

Thanks! And indeed, `BitSet` is used under the hood 🙂

---

<div class="post-metadata">

**Author:** ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)\
**Post date:** [May 23, 2024, 9:56am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/11 "2024-05-23T09:56:45Z")

</div>

> [@Elrod](#):
>
> To answer the initial question of “had bad is it”, there are precedents. For example, [Symbolics.jl](https://juliahub.com/ui/Packages/General/Symbolics):

To be fair though, Symbolics.jl is much closer to being its own language with its own semantics than just a regular julia package. It using Julia semantics as a sort-of semi-CAS is not a good example for why subverting expectations on logical operations is a good idea, especially since that has already led to friction in the past.

@gdalle since you want to have `ifelse(foo, a, b)` mean sort-of `foo(a,b)`, why use `ifelse` at all? That operation sounds like a very different one than `ifelse`.

---

<div class="post-metadata">

**Author:** ![ericphanson](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ericphanson/32/215186_2.png) [@ericphanson](https://discourse.julialang.org/u/ericphanson)\
**Post date:** [May 23, 2024, 10:01am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/12 "2024-05-23T10:01:00Z")

</div>

BTW in terms of precedent, Convex.jl also returns constraint objects from `==`, `<=`, and `>=` between its expression objects and numbers/matrices (and between expressions and other expressions), and has done so for ~10 years. I don’t think it’s caused severe issues, but I wouldn’t be surprised if there were some invalidations or such.

---

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [May 23, 2024, 10:02am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/13 "2024-05-23T10:02:16Z")

</div>

> [@Sukera](#):
>
> To be fair though, [Symbolics.jl](https://juliahub.com/ui/Packages/General/Symbolics) is much closer to being its own language with its own semantics than just a regular julia package. It using Julia semantics as a sort-of semi-CAS is not a good example for why subverting expectations on logical operations is a good idea, especially since that has already led to friction in the past.

@Sukera do you have examples of such friction? The comparison actually makes sense because what we’re coding with @hill is an alternative to Symbolics for Jacobian and Hessian sparsity pattern detection.

> [@Sukera](#):
>
> @gdalle since you want to have `ifelse(foo, a, b)` mean sort-of `foo(a,b)`, why use `ifelse` at all? That operation sounds like a very different one than `ifelse`.

Because I’m not writing the code that will be subjected to operator overloading, my users are writing it. And I want my new (set-containing) types to flow freely through existing code that uses comparisons or `ifelse`.

---

<div class="post-metadata">

**Author:** ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)\
**Post date:** [May 23, 2024, 10:06am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/14 "2024-05-23T10:06:58Z")

</div>

> [@gdalle](#):
>
> @Sukera do you have examples of such friction?

[Generalize `==` Documentation to not be Statistics-Specific by ChrisRackauckas · Pull Request #53024 · JuliaLang/julia · GitHub](https://github.com/JuliaLang/julia/pull/53024) for one, or the whole `Static{Int}` debate for another.

> [@gdalle](#):
>
> The comparison actually makes sense because what we’re coding with @hill is an alternative to Symbolics for Jacobian and Hessian sparsity pattern detection.

What you’re describing sounds to me like abstract interpretation, i.e. a static analysis pass like the kind of thing JET.jl is doing. Not execution under Julia semantics and bending/breaking the expected API of a set of functions.

---

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [May 23, 2024, 10:09am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/15 "2024-05-23T10:09:34Z")

</div>

> [@Sukera](#):
>
> What you’re describing sounds to me like abstract interpretation, i.e. a static analysis pass like the kind of thing [JET.jl](https://juliahub.com/ui/Packages/General/JET) is doing. Not execution under Julia semantics and bending/breaking the expected API of a set of functions.

Abstract interpretation is a common technique in autodiff, which is the intended application of our code. Except that here we implement it with simple operator overloading, instead of a more sophisticated compiler-hooked machinery like JET. It’s a similar distinction as between ForwardDiff (operator overloading AD) and e.g. Zygote or Enzyme (source transformation AD).

---

<div class="post-metadata">

**Author:** ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)\
**Post date:** [May 23, 2024, 10:31am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/16 "2024-05-23T10:31:19Z")

</div>

> [@gdalle](#):
>
> Abstract interpretation is a common technique in autodiff,

[Abstract interpretation](https://en.m.wikipedia.org/wiki/Abstract_interpretation) is a compiler technique to reason about some syntax (in this case, julia syntax) with some semantics (in your case, non-julia semantics, since `ifelse` is not actually supposed to branch). What you’re describing is doing exactly that, but you’re trying to do that inside of the semantics of julia and its type system. By necessity, that means breaking the contract of e.g. `==` or `<`, since those are (generally) expected to return a `Bool`.

> [@gdalle](#):
>
> Except that here we implement it with simple operator overloading, instead of a more sophisticated compiler-hooked machinery like JET. It’s a similar distinction as between ForwardDiff (operator overloading AD) and e.g. Zygote or Enzyme (source transformation AD).

It’s my understanding that this is already how Symbolics.jl works under the hood, with all the problems that entails. As such, I think those problems also apply to your approach 🤷 Maybe I’m misunderstanding something?

---

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [May 23, 2024, 10:36am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/17 "2024-05-23T10:36:20Z")

</div>

> [@Sukera](#):
>
> [Abstract interpretation](https://en.m.wikipedia.org/wiki/Abstract_interpretation) is a compiler technique

In the autodiff literature, this term refers to a wide array of techniques, not all of which hook into the compiler. It’s more of a broad synonym for “changing the semantics of your code”. Minor terminology quirk, which @oxinabox could probably clarify better than me.

> [@Sukera](#):
>
> It’s my understanding that this is already how [Symbolics.jl](https://juliahub.com/ui/Packages/General/Symbolics) works under the hood, with all the problems that entails. As such, I think those problems also apply to your approach 🤷 Maybe I’m misunderstanding something?

I think you’re right. It’s also the same paradigm that ForwardDiff.jl and Measurements.jl follow. Changing semantics by operator overloading is dangerous, and there’s a compromise to find on how disruptive you wanna be. This topic is about placing the needle correctly

---

<div class="post-metadata">

**Author:** ![Elrod](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/elrod/32/22461_2.png) [@Elrod](https://discourse.julialang.org/u/Elrod)\
**Post date:** [May 23, 2024, 10:38am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/18 "2024-05-23T10:38:06Z")

</div>

> [@Sukera](#):
>
> To be fair though, [Symbolics.jl](https://juliahub.com/ui/Packages/General/Symbolics) is much closer to being its own language with its own semantics than just a regular julia package. It using Julia semantics as a sort-of semi-CAS is not a good example for why subverting expectations on logical operations is a good idea, especially since that has already led to friction in the past.

FWIW, I don’t really care about Symbolics as a CAS.  
I care about Symbolics as a means of tracing Julia code for use by packages like ModelingToolkit, which is quite similar to what gdalle wants to do here.

---

<div class="post-metadata">

**Author:** ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)\
**Post date:** [May 23, 2024, 11:18am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/19 "2024-05-23T11:18:34Z")

</div>

> [@gdalle](#):
>
> It’s more of a broad synonym for “changing the semantics of your code”.

Yes, that’s exactly what I said in the sentences following the one you quoted 🙂 You can substitute “compiler” for “interpreter” or “static analyser” in my post above and the intended meaning would not change - for the purposes of this discussion, they are equivalent.

> [@gdalle](#):
>
> Changing semantics by operator overloading is dangerous, and there’s a compromise to find on how disruptive you wanna be. This topic is about placing the needle correctly

Ultimately, the question that you’ll have to answer is what you do about code that ends up doing the equivalent of `<(a,b)::Bool`, be it for type stability or some other reason. Since you can’t overload `::`, overloading `ifelse` will get you stuck with an assertion. That’s why I’m saying that what you want to do goes outside of the semantics of julia-the-language.

You can either ignore that kind of code and consider it out of scope for your project, or suggest to your users that they shouldn’t assume functions return what they’re documented to return (which sounds much more dangerous to me). The only “perfect”/nondisruptive solution is an actual abstract interpreter that can properly handle these situations, by operating on the level of an actual compiler/interpreter handling `::` natively.

---

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [May 23, 2024, 11:21am UTC](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611/20 "2024-05-23T11:21:39Z")

</div>

> [@Sukera](#):
>
> Ultimately, the question that you’ll have to answer is what you do about code that ends up doing the equivalent of `<(a,b)::Bool`, be it for type stability or some other reason. Since you can’t overload `::`, overloading `ifelse` will get you stuck with an assertion.

In that case an error will be thrown, and I guess that’s coherent with what I want to do

[Next page](https://discourse.julialang.org/t/how-bad-is-it-if-x-y-doesnt-return-a-bool/114611.md?page=2)
