JuliaSymbolics/SymbolicUtils.jl

Semantics of `==` not good for generic programming

jlapeyre opened this issue · 7 comments

Having == construct an equation might be a good idea if this package were a standalone CAS, like Mathematica. But it's meant to work with the larger Julia ecosystem. The semantics of == in Julia at large is consistent. The semantics here is completely different, which means you using Sym in generic code will often do the wrong thing, in fact throw an error.

I can't find a single instance in Julia where a == b does not return Bool. Maybe there is one, but I can't find it. This line in Base.jl is largely responsible:

==(x, y) = ===(x,y)

You have...

julia> @code_typed ["aa"] == 3
CodeInfo(
1return false
) => Bool

julia> @code_typed Base == 3
CodeInfo(
1return false
) => Bool

As a result, this assumption is made everywhere in the Julia ecosystem.

This issue is similar to #15. I did not check this against the version of SymbolicUtils in that issue, but I think the problem there was also that == does not return a Bool, not simplification.

I noticed this a couple of years ago, and may have posted something somewhere. But I want to state the issue clearly. I can't think of a function in Julia that is more widespread and with more consistent semantics than Base.==. And the method defined for ::Sym breaks this consistency for the sake of giving the frontend experience of Mathematica. This makes SymbolicUtils more like a DSL and less like a library for use in other Julia projects.

A response could be "just use isequal". But that would miss my point. Julia is about composability an genericness. (And isequal has a different, specified, semantics.)

(btw. +100 on performance of rules and of load time of SymbolicUtils! This makes it easier to argue for Julia.)

lairez commented

Same problem here. For example, we cannot use Polynomials.jl with symbolic expression coefficients.

shashi commented

What are you proposing == do?

If there is a reasonable answer, we can implement it. But a==b is meant to be a value check in most code, so just returning false would be wrong.

I can't find a single instance in Julia where a == b does not return Bool. Maybe there is one, but I can't find it.

julia> missing == 2
missing

that would miss my point. Julia is about composability an genericness.

I hope it's also about correctness.

lairez commented

What are you proposing == do?

That it does what isequal do?

shashi commented

The current behavior of == is actually used in packages. (MTK for example).

That it does what isequal do?

This will result in wrong code as I mentioned.

What are you proposing == do?
If there is a reasonable answer, we can implement it. But a==b is meant to be a value check in most code, so just returning false would be wrong.

I mean that == always returns a Bool and everyone expects it. Not returning a Bool is more wrong. An exception is missing which is a continual source of headaches and forces everyone to deal with itself.

Because == falls back to ===, you can always call == and expect a Bool. So people rely on it. A lot of people wish it were not this way. I've seen Stefan opine that it was a mistake.

It looks like 1.0 was released a couple of months ago. That's unfortunate. This limits the usability of SymbolicUtils.jl. I'm not sure how to fix it. Maybe deprecate it's use and use a different symbol, like Symbolics.jl does.

Here is a related problem shield Base.require from invalidations when loading Symbolics.jl. SymbolicUtils extends Base.isequal for no reason to do something it is not documented to do. So far, this has required at least this commit to Julia itself. If there is some way to take this back or fix it in a breaking release, I suggest doing it.

I think this kind of thing is quite common. I have some packages that should be released at v1 and this is a good reminder to vet them for gratuitous extensions, which I know I have made in the past. Like everyone else, I was over-enamored with multiple dispatch. It's probably the relatively recent focus on invalidations that has brought this problem to light.

btw Convex.jl also works this way, where == returns a constraint object, and has since ~2014 or so when the package was first developed. I don't have a strong opinion about whether or not it should, but just want to point that out. I think it hasn't caused many issues there because Convex's objects don't really work in generic code so they tend to only be passed into Convex.jl-aware functions.

shashi commented

If there is a strong desire for this, I think one can make a number type which behaves in the way need, just as we have LiteralReal and SafeReal.

@syms x::LiteralReal makes it so that x-x remains as-is and is not represented as 0. @syms x::SafeReal makes it so that (x*y)/x is not simplified but x-x is.