Stream: helpdesk (published)

Topic: Fix2 and ==


view this post on Zulip Rasmus Henningsson (Jan 17 2025 at 07:41):

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?)

view this post on Zulip Jakob Nybo Nissen (Jan 17 2025 at 09:11):

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)

view this post on Zulip Rasmus Henningsson (Jan 17 2025 at 09:28):

And a PR for Fix2 (and Fix1) would probably not be accepted because it's breaking?

view this post on Zulip Mason Protter (Jan 17 2025 at 09:49):

I doubt it'd actually break anything.

view this post on Zulip Mason Protter (Jan 17 2025 at 09:50):

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.

view this post on Zulip Mosè Giordano (Jan 17 2025 at 09:59):

I may be wrong, but I don't remember anyone defending the fact == defaults to ===, it seems to me more a (very unfortunate) accident

view this post on Zulip Mason Protter (Jan 17 2025 at 10:04):

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.

view this post on Zulip Jakob Nybo Nissen (Jan 17 2025 at 14:54):

@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.

view this post on Zulip Jakob Nybo Nissen (Jan 17 2025 at 14:55):

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.

view this post on Zulip John Lapeyre (Jan 17 2025 at 19:57):

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.

view this post on Zulip Neven Sajko (Jan 17 2025 at 20:49):

The issue of equality for ComposedFunction seems somewhat similar to the issue of equality for Fix:
https://github.com/JuliaLang/julia/issues/53853

view this post on Zulip Neven Sajko (Jan 17 2025 at 20:53):

I'm not sure there's a meaningful definition for == in any of the two cases, though. Maybe those should just throw?

view this post on Zulip Neven Sajko (Jan 17 2025 at 20:58):

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.

view this post on Zulip Neven Sajko (Jan 17 2025 at 20:59):

This is the definition of struct Fix, btw: https://github.com/JuliaLang/julia/blob/98f8aca9ac8eb1c9467b685c7e8594d981192d96/base/operators.jl#L1173-L1175

view this post on Zulip Daniel Wennberg (Jan 17 2025 at 23:26):

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.

view this post on Zulip Daniel Wennberg (Jan 17 2025 at 23:30):

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?

view this post on Zulip Jakob Nybo Nissen (Jan 18 2025 at 05:57):

Yeah pretty much

view this post on Zulip Rasmus Henningsson (Jan 18 2025 at 13:11):

Neven Sajko said:

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.

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.

  1. Are they the same accumulator? For this === is natural.
  2. Do they currently have the same state? For this == 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.)

view this post on Zulip Rasmus Henningsson (Jan 18 2025 at 13:18):

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.

view this post on Zulip Rasmus Henningsson (Jan 18 2025 at 13:21):

Neven Sajko said:

The issue of equality for ComposedFunction seems somewhat similar to the issue of equality for Fix:
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.)

view this post on Zulip Timothy (Jan 18 2025 at 15:21):

Isn't Symbol akin to this?

Yup.


Last updated: Jan 29 2025 at 04:38 UTC