Is there a good reason that Base.Fix2
uses the default comparison (i.e. ==(x,y) = x === y
) instead of comparing its members by ==
?
Because this causes the rather unintuitive:
julia> startswith("abc") == startswith("abc")
true
julia> startswith(r"abc") == startswith(r"abc")
false
Even though:
julia> r"abc" == r"abc"
true
(Maybe it would be considered breaking to change anyway?)
I think the reason is just that no-one bothered to implement == for Fix2.
So the problem here is really the default of ==. If you design a fallback implementation, you better be sure it's the one you actually want. I think most people by now agree that the fallback definition of == was a mistake (with disagreement on whether it should be defined recursively in terms of the fields, or whether it should have no default implementation)
And a PR for Fix2
(and Fix1
) would probably not be accepted because it's breaking?
I doubt it'd actually break anything.
I mean, I'm definitely open to being surprised, but this sounds like a very fixable issue. I'd just open a PR, and then we can run the regression tests and find out if there is a problem or not.
I may be wrong, but I don't remember anyone defending the fact ==
defaults to ===
, it seems to me more a (very unfortunate) accident
I've literally never seen someone complain about Tuple
or NamedTuple
using ==
fieldwise, which really makes me suspect that nobody would have objected in practice to that just being the default for all structs.
@Mosè Giordano I have some vague recollection of Stefan Karpinski saying that they thought it was a good idea to have it default to === back in the day, but now realize that whenever you want to compare two things and don't know their types, you essentially always want to use === anyway.
FWIW I would prefer to have it have no default, for the same reason. Usually you want ===. In the cases where you don't you usually have to manually define == anyway.
But a struct that includes a Vector{Int}
will by default give false
even if the fields are ==
. Applying ==
fieldwise would work... Habitually using a macro @noeq MyStruct
that throws, until you decide to write a method might be a good idea.
The issue of equality for ComposedFunction
seems somewhat similar to the issue of equality for Fix
:
https://github.com/JuliaLang/julia/issues/53853
I'm not sure there's a meaningful definition for ==
in any of the two cases, though. Maybe those should just throw?
Specifically, for Fix
, regarding the x
field, I think that the required comparison depends on the f
field. E.g., for some values of the f
field, it might make sense to compare the x
fields with ==
, while for other values of the f
field it might make sense to compare the x
fields with ===
.
The point being it doesn't seem correct to compare among Fix
in a pairwise fashion, rather equality should IMO only be defined when the types of both fields are known to whoever is defining the ==
method.
This is the definition of struct Fix
, btw: https://github.com/JuliaLang/julia/blob/98f8aca9ac8eb1c9467b685c7e8594d981192d96/base/operators.jl#L1173-L1175
Rasmus Henningsson said:
Because this causes the rather unintuitive:
julia> startswith("abc") == startswith("abc") true julia> startswith(r"abc") == startswith(r"abc") false
Isn't the truly weird part here that "abc" === "abc"
?
Apparently, strings are heap allocated, isbitstype(String) == false
, ismutabletype(String) == true
, pointer("abc") != pointer("abc")
, but still Base.isidentityfree(String) == true
, i.e., strings have value identity like immutable structs. How does this work?
I know that Julia strings are immutable, but that _could_ just have meant that there's no interface that lets you mutate them and the compiler makes assumptions accordingly. Instead, it appears to mean that the full package of immutable struct semantics has been grafted onto a non-isbits, heap-allocated, variable-length type.
I suppose objectid(::String)
is special-cased to simply be another hash
, and ===(::String, ::String)
is special-cased to compare contents instead of memory address in the case of an objectid
collision. Maybe that's sufficient for everything to work?
Yeah pretty much
Neven Sajko said:
Specifically, for
Fix
, regarding thex
field, I think that the required comparison depends on thef
field. E.g., for some values of thef
field, it might make sense to compare thex
fields with==
, while for other values of thef
field it might make sense to compare thex
fields with===
.The point being it doesn't seem correct to compare among
Fix
in a pairwise fashion, rather equality should IMO only be defined when the types of both fields are known to whoever is defining the==
method.
I think it makes sense with ==
for Fix1
/Fix2
. (As @Jakob Nybo Nissen mentioned, the problem is more general, but that ship has sailed. However we can still improve where it makes sense if it doesn't break anything.)
The main problem here is that both ==
and ===
are useful, but since ==
falls back to ===
we lose that distinction. The current choice makes Julia code less expressive, and harder to understand.
Consider two vectors v1 = [1,2,3]
and v2 = [1,2,3]
. v1==v2
is true
, but v1===v2
is false.
In a similar way, we could use Fix1
to define accumulators
acc = Base.Fix1(push!, v1)
acc2 = Base.Fix1(push!, v2)
There are two meaningful ways to compare acc
and acc2
.
===
is natural.==
would be natural.This is currently impossible with acc
and acc2
however. And crucially, the behavior changes compared to v1
and v2
.
Unless someone finds a good counterexample, I think any case that you actually want the ===
behavior, it is also natural to use ===
to compare the Fix1
/Fix2
objects. :smile: (I guess I'm repeating @Jakob Nybo Nissen's point here.)
Daniel Wennberg said:
...
Isn't the truly weird part here that"abc" === "abc"
?
...
Yes, that's a good point. (But we could also consider that an implementation detail. In theory, since strings are immutable in Julia, it could have been implemented using some fancy string pool so that if two identical strings were created, they would indeed share the same memory. Isn't Symbol
akin to this? I might be wrong about that though. :smile:)
But I hope my Vector
example above shows another situation where it makes much more sense to compare with ==
. Even though there's no case where Fix1
/Fix2
handles that well at the moment.
Neven Sajko said:
The issue of equality for
ComposedFunction
seems somewhat similar to the issue of equality forFix
:
https://github.com/JuliaLang/julia/issues/53853
Thanks for this! That's a great example to compare to when considering making this change.
(I'm currently on a shaky internet connection, but I'll make a pull request sometime soon.)
Isn't
Symbol
akin to this?
Yup.
Last updated: Jan 29 2025 at 04:38 UTC