Two-place equality methods are a general solution, but not a very good idea. I have hit upon the a better abstraction for handling custom equality, which I call "equality substitution". An objects equality method doesn't perform the equality comparison, but rather nominates an alternative value which is used as that object's representative for equality comparisons. That alternative is the "equality substitute".

This is documented in the TXR Lisp reference manual: https://www.nongnu.org/txr/txr-manpage.html#N-02CDB347

The key idea is that we have a reasonably rich set of built-in objects in the language that support equal equality useful ways.

Instead of trying to bend other objects to also support equal equality from scratch, we require objects to calculate a representative of themselves which is some language built-in data type that natively supports equal.

Equality substitution, though not a perfect concept, removes much of the error proneness of defining equality.

- The application developer is never required to write a hashing function. Not only does that relieve the application programmer of burden, but it also relieves the system of the burden of having to support custom hashing functions everywhere such as in hash tables.

- Inequality comes for free also. For instance, suppose a user-account structure has an equality method that produces the user-id (a character string) as the substitute. This means that we can now compare two user-account objects using less. One user-account is less than another if their respective user-id fields are less, which is a lexicographic comparison.

- The equality substitute can be the result of a complex calculation, which is cached in the object. I.e. the equal method can produce some compound key the first time it is invoked, and then use the cached value subsequently.

Demo:

This is the TXR Lisp interactive listener of TXR 257.
Quit with :quit or Ctrl-D on an empty line. Ctrl-X ? for cheatsheet.
Join TXR Rewards now, and get 15000 closing parentheses you can use anywhere.
1> (defstruct account ()
user-id
(:method equal (me) me.user-id))
#<struct-type account>
2> (defvarl a (new account user-id "aardvark"))
a
3> (defvarl b (new account user-id "bat"))
b
4> (defvarl z (new account user-id "zebra"))
z
5> (less a b z)
t
6> (less b a)
nil
7> (equal a b)
nil
8> (equal a a)
t
9> (equal a (new account user-id "aardvark"))
t
10> (hash-equal a)
327970753
11> (hash-equal "aardvark")
327970753
12> (defvarl h (hash))
h
13> (set [h a] 100)
100
14> (set [h b] 200)
200
15> 4
4
16> (set [h z] 300)
300
17> h
#H(() (#S(account user-id "bat") 200) (#S(account user-id "aardvark") 100)
(#S(account user-id "zebra") 300))
18> [h a]
100
19> [h b]
200
20> [h "aardvark"]
100

A key observation here is that the object a, of account type, is equal to the character string "aardvark", as well as to any object of any type which produces that string as its equality substitute.

However, this is a very clear aspect of the model, and not a hidden pitfall.

We are saying that a participates in equality as if it literally were the string "aardvark" (though not satisfying the stringp predicate).
This is clear from the beginning and you program your logic accordingly, taking advantage of that while anticipating whatever risks it may cause.

The system is very easy to understand and use; so far in every program where I took advantage of equality substitution, it was a pleasure to work with.