Marco Antoniotti
mantoniotti at common-lisp.net
2011-02-15
This document presents new generic functions for Common Lisp that provide user hooks for extensible equality and comparison tests. This is in addition to the standard equality and comparison predicates. The current proposal is minimal, in the sense that it just provides one conceptually simple set of hooks in what is considered a cross-language consensus.
Several Common Lisp functions rely on the
:test
keyword to pass a predicate to be used in their
operations. This is a satisfactory solution in most cases, yet, while
writing algorithms and libraries it would be useful to have
"hooks" in the type and class system allowing for the definition of
extensible equality and comparison tests.
This proposal contains a minimal set of (generic) functions that can be recognized in several language specifications, e.g., Java.
The specification is centered on two concepts: that of an equality test and that of a comparison generic operator. The comparison operator returns different values depending on whether the its execution determines the ordering relationship (or lack thereof) of two objects.
The the proposal describes the equality and
comparison operators. The equality operator is
called AEQUALIS
and some synonyms are also defined. The
comparison operators is called COMPARE
. The
utility functions LT
, GT
, LTE
,
and GTE
are also defined. Some synonyms are also
defined.
The comparison operator returns one of four values: the
symbols <
, >
, =
, or
/=
. The intent of such definition is to make it
usable in conjunction with case
, ccase
, and
ecase
; also, its intent is to make it possible to capture
partial orders among objects in a set.
AEQUALIS
AEQUALIS
a b
&optional
recursive-p
&rest
keys
&key &allow-other-keys
→ resultNote: Maybe it would make sense to supply a
\:key
parameter (defaulting to identity
) as
well.
AEQUALIS
(a T
) (b T
)
&optional
recursive-p
&rest
keys
&key &allow-other-keys
AEQUALIS
(a number
) (b number
)
&optional
recursive-p
&rest
keys
&key &allow-other-keys
AEQUALIS
(a cons
) (b cons
)
&optional
recursive-p
&rest
keys
&key &allow-other-keys
AEQUALIS
(a character
) (b character
)
&optional
recursive-p
&rest
keys
&key
case-sensitive-p &allow-other-keys
AEQUALIS
(a string
) (b string
)
&optional
recursive-p
&rest
keys
&key
case-sensitive-p &allow-other-keys
AEQUALIS
(a array
) (b array
)
&optional
recursive-p
&rest
keys
&key &allow-other-keys
AEQUALIS
(a hash-table
) (b hash-table
)
&optional
recursive-p
&rest
keys
&key
(by-key t
)
(by-value t
)
(check-properties t
)
&allow-other-keys
NIL
.
boolean
.
list
(as per the usual behavior).
T
.
T
.
NIL
.
T
.
The AEQUALIS
generic functions defines methods to test for
"equality" of two objects a and b. When two
objects a and b are AEQUALIS
under an
appropriate and context-dependent notion of "equality", then the
function returns T
as result; otherwise AEQUALIS
returns NIL
as result.
If the optional argument recursive-p is T
, then
AEQUALIS
may recurse down the "structure" of a
and b. The description of each known method contains the
relevant information about its recursive-p dependent
behavior.
AEQUALIS
provides some default behavior, but it is intended mostly
as a hook for users. As such, it is allowed to add keyword arguments
to user-defined AEQUALIS
methods, as the &key
and
&allow-other-keys
lambda-list markers imply.
The following are the descriptions of AEQUALIS
known methods;
unless explicitely mentioned recursive-p and keys
are to be considered as ignore
ed.
AEQUALIS
(a T
) (a T
)
&optional
recursive-p
&rest
keys &key &allow-other-keys
The default behavior for two objects a and b of
type/class T
is to fall back on the function equalp
.
AEQUALIS
(a number
) (a number
)
&optional
recursive-p
&rest
keys &key &allow-other-keys
The default behavior for two objects a and b of
type/class number
is to bypass equalp
and to fall back
directly on the function =
Note: it may be
worthwhile to add a :epsilon
keyword describing the tolerance
of the equality test and other keys describing the "nearing"
direction (Subnote: must check the correct numerics terminology.)
AEQUALIS
(a cons
) (a cons
)
&optional
recursive-p
&rest
keys &key &allow-other-keys
The default behavior for two objects a and b of
type/class cons
is to call the function tree-equal
with AEQUALIS
as \:test
.
AEQUALIS
(a character
) (a character
)
&optional
recursive-p
&rest
keys
&key
(case-sensitive-p T
)
&allow-other-keys
The behavior for two character
objects depends on the value of
the keyword parameter case-sensitive-p: if non-NIL
(the
default) then the test uses char=
, otherwise char-equal
.
AEQUALIS
(a string
) (a string
)
&optional
recursive-p
&rest
keys
&key
(case-sensitive-p T
)
&allow-other-keys
The behavior for two string
objects depends on the value of
the keyword parameter case-sensitive-p: if non-NIL
(the
default) then the test uses string=
, otherwise string-equal
.
AEQUALIS
(a array
) (a array
)
&optional
recursive-p
&rest
keys &key &allow-other-keys
The default behavior for two objects a and b of
type/class array
is to call AEQUALIS
element-wise, as per
equalp
. The recursive-p argument is passed
unmodified in each element-wise call to AEQUALIS
.
Example: the following may be an implementation of
AEQUALIS
on array
s (modulo "active elements",
fill-pointers and other details).
(defmethod aequalis ((a array) (b array) &optional recursive-p &rest keys &key &allow-other-keys) (let ((a-ts (array-total-size a)) (b-ts (array-total-size b)) ) (when (/= a-ts b-ts) (return-from equiv nil)) (loop for i from 0 below a-ts always (apply #'equiv (row-major-aref a i) (row-major-aref b i) recursive-p keys)) ))
AEQUALIS
(a structure-object
) (a structure-object
)
&optional
recursive-p
&rest
keys &key &allow-other-keys
The AEQUALIS
default behaviour for two structure-object
s
is to fall back on equalp
Note: an alternative choice would be to fall back on eq
.
In this case a Java (or C++) programmer may find the connection
more immediate, as this would make the behavior of AEQUALIS
similar to the default java.lang.Object equals
method.
Another reason to fall back on eq
would be to make the
behavior between the treatment of structure-object
s and
standard-object
s uniform.
AEQUALIS
(a standard-object
) (a standard-object
)
&optional
recursive-p
&rest
keys &key &allow-other-keys
The AEQUALIS
default behaviour for two standard-object
s
is to fall back on eq
.
AEQUALIS
(a hash-table
) (a hash-table
)
&optional
recursive-p
&rest
keys
&key
(by-key t
)
(by-value t
)
(check-properties t
)
&allow-other-keys
The AEQUALIS
default behaviour for two hash-table
object
is the following. If a and b are eq
, the
result is T
. Otherwise, first it is checked that
the two hash-tables have the same number of entries, then three
tests are performed "in parallel".
NIL
then the keys of the
a and b are compared with AEQUALIS
(with
recursive-p passed as-is). The semantics of this test
are as if the following code were executed
(loop for k1 in (ht-keys a) for k2 in (ht-keys b) always (equiv k1 k2 recursive-p))If by-key is
NIL
, the subtest is true.
NIL
then the values of the
a and b are compared with AEQUALIS
(with
recursive-p passed as-is). The semantics of this test
are as if the following code were executed
(loop for v1 in (ht-values a) for v2 in (ht-values b) always (equiv v1 v2 recursive-p))If by-value is
NIL
, the subtest is true.
NIL
then all the
standard hash-table properties are checked for equality using
eql
, =
, or null
as needed.
Implementation-dependent properties are checked accordingly.
If check-properties is NIL
, the subtest is true.
Synonyms: the name AEQUALIS
is Latin for "equal"; of
course, this may not be the best name for a Common Lisp function; some
synonims may be the symbol ==
or
EQUIV
. In general, synonyms should be defined by setting their
fdefinition
to (symbol-function 'aequalis)
.
cl-prompt> (aequalis 42 42) T cl-prompt> (aequalis 42 'a) NIL cl-prompt> (aequalis "abc" "abc") T cl-prompt> (aequalis (make-hash-table) (make-hash-table)) T cl-prompt> (aequalis "FOO" "Foo") T cl-prompt> (aequalis "FOO" "Foo" nil :case-sensitive-p nil) NIL cl-prompt> (defstruct foo a s d) FOO cl-prompt> (aequalis (make-foo :a 42 :d "a string") (make-foo :a 42 :d "a string")) NIL ; If falling back on EQUALP. T if falling back on EQ. cl-prompt> (aequalis (make-foo :a 42 :d "a bar") (make-foo :a 42 :d "a baz")) NIL cl-prompt> (defmethod aequalis ((a foo) (b foo) &optional (recursive-p t) &key &allow-other-keys) (declare (ignore recursive-p)) (or (eq a b) (= (foo-a a) (foo-a b)))) #<STANDARD METHOD aequalis (FOO FOO)> cl-prompt> (aequalis (make-foo :a 42 :d "a bar") (make-foo :a 42 :d "a baz")) T
None.
TBD.
TBD.
COMPARE
COMPARE
a b &optional
recursive-p
&rest
keys
&key &allow-other-keys
→
result
COMPARE
(a T
) (a T
)
&optional
recursive-p
&rest
keys
&key &allow-other-keys
COMPARE
(a number
) (a number
)
&optional
recursive-p
&rest
keys &key &allow-other-keys
COMPARE
(a character
) (a character
)
&optional
recursive-p
&rest
keys
&key
(case-sensitive-p NIL)
&allow-other-keys
COMPARE
(a string
) (a string
)
&optional
recursive-p
&rest
keys
&key
(case-sensitive-p NIL)
&allow-other-keys
COMPARE
(a symbol
) (a symbol
)
&optional
recursive-p
&rest
keys
&allow-other-keys
NIL
.
symbol
of type (member < > = /=)
.
list
(as per the usual behavior).
T
.
The generic function COMPARE
defines
methods to test the ordering of two objects a and
b, if such order exists. The result value returned
by COMPARE
is one of the four symbols: \texttt{<}, \texttt{>},
\texttt{=}, or \texttt{/=}. The COMPARE
function returns
/=
as result by default; thus it can represent
partial orders among objects. The equality tests should be
coherent with what the generic function AEQUALIS
does.
If the optional argument recursive-p is T
, then
COMPARE
may recurse down the "structure" of a
and b. The description of each known method contains the
relevant information about its recursive-p dependent
behavior.
COMPARE
(a T
) (a T
)
&optional
recursive-p
&rest
keys
&key &allow-other-keys
The default behavior for COMPARE
when applied to two objects
a and b of "generic" type/class is to return the
symbol /=
as result. The intended meaning is to
signal the fact that no ordering relation is known among them.
COMPARE
(a number
) (a number
)
&optional
recursive-p
&rest
keys &key &allow-other-keys
The default behavior for two objects a and b of
type/class number
is to compute result according to
the standard predicates <
, >
, and =
.
COMPARE
(a character
) (a character
)
&optional
recursive-p
&rest
keys
&key
(case-sensitive-p NIL)
&allow-other-keys
The behavior for two string
objects depends on the value of
the keyword parameter case-sensitive-p: if non-NIL
(the
default) then the test uses string<
, string>
, and
string=
to compute result; otherwise it uses
string-lessp
, string-greaterp
, and string-equal
.
COMPARE
(a string
) (a string
)
&optional
recursive-p
&rest
keys
&key
(case-sensitive-p NIL)
&allow-other-keys
The behavior for two string
objects depends on the value of
the keyword parameter case-sensitive-p: if non-NIL
(the
default) then the test uses string<
, string>
, and
string=
to compute result; otherwise it uses
string-lessp
, string-greaterp
, and string-equal
.
COMPARE
(a symbol
) (a symbol
)
&optional
recursive-p
&rest
keys
&allow-other-keys
When called with two symbol
s, the method returns =
if
a and b are eq
, otherwise it returns /=
.
cl-prompt> (compare 42 0) > cl-prompt> (compare 42 1024) < cl-prompt> (compare pi pi) = cl-prompt> (compare pi 3.0s0) > cl-prompt> (compare 'this-symbol 'this-symbol) = cl-prompt> (compare 'this-symbol 'that-symbol) /= cl-prompt> (compare '(q w e r t y) '(q w e r t y)) = cl-prompt> (compare #(q w e r t y) #(q w e r t y 42)) /= cl-prompt> (compare "asd" "asd") = cl-prompt> (compare "asd" "ASD") > cl-prompt> (compare "asd" "ASD" t :case-sensitive-p nil) = cl-prompt> (defstruct foo a s d) FOO cl-prompt> (compare (make-foo :a 42) (make-foo :a 42)) /= cl-prompt> (defmethod compare ((a foo) (b foo) &optional recursive-p &rest keys &key &allow-other-keys) (let ((d-r (apply #'compare (foo-d a) (foo-d b) recursive-p keys)) (a-r (apply #'compare (foo-a a) (foo-a b) recursive-p keys)) ) (if (eq d-r a-r) d-r '/=))) #<STANDARD METHOD compare (FOO FOO)> cl-prompt> (compare (make-foo :a 0 :d "I am a FOO") (make-foo :a 42 :d "I am a foo")) /= cl-prompt> (compare (make-foo :a 0 :d "I am a FOO") (make-foo :a 42 :d "I am a foo") t :case-sensitive-p nil) < cl-prompt> (compare (make-array 3 :initial-element 0) (vector 1 2 42)) Error: Uncomparable objects #(0 0 0) and #(1 2 42).
LT
, LTE
, GT
, and GTE
LT
a b &optional
recursive-p
&rest
keys
&key &allow-other-keys
→
result
LTE
a b &optional
recursive-p
&rest
keys
&key &allow-other-keys
→
result
GT
a b &optional
recursive-p
&rest
keys
&key &allow-other-keys
→
result
GTE
a b &optional
recursive-p
&rest
keys
&key &allow-other-keys
→
result
Synonyms: the full-name synonyms lessp
,
not-greaterp
, greaterp
, and not-lessp
are
provided s well. Their implementation should be based on setting the
relevant fdefinition
.
The functions LT
, LTE
, GT
,
and GTE
are shorthands for calls
to COMPARE
. Each one calls COMPARE
as
(apply #'compare a b recursive-p keys)The appropriate result is returned when
COMPARE
, on its turn,
returns <
, >
, or =
. If compare returns
/=
, then no ordering relation can be
established, and the functions LT
, LTE
, GT
, and GTE
signal an error.If the optional argument recursive-p is T
, then
AEQUALIS
may recurse down the "structure" of a
and b. The description of each known method contains the
relevant information about its recursive-p dependent
behavior.
cl-prompt> (lt 42 0) NIL cl-prompt> (lt 42 1024) T cl-prompt> (gte pi pi) T cl-prompt> (greaterp pi 3.0s0) T cl-prompt> (lt "asd" "asd") NIL cl-prompt> (lte "asd" "ASD") NIL cl-prompt> (lte "asd" "ASD" t :case-sensitive-p nil) T cl-prompt> (defstruct foo a s d) FOO cl-prompt> (defmethod compare ((a foo) (b foo) &optional recursive-p &rest keys &key &allow-other-keys) (let ((d-r (apply #'compare (foo-d a) (foo-d b) recursive-p keys)) (a-r (apply #'compare (foo-a a) (foo-a b) recursive-p keys)) ) (if (eq d-r a-r) d-r '/=))) #<STANDARD METHOD compare (FOO FOO)> cl-prompt> (lte (make-foo :a 0 :d "I am a FOO") (make-foo :a 42 :d "I am a foo")) Error: Uncomparable objects #S(FOO :a 0 :s NIL :d "I am a FOO") and #S(FOO :a 0 :s NIL :d "I am a foo") cl-prompt> (lte (make-foo :a 0 :d "I am a FOO") (make-foo :a 42 :d "I am a foo") t :case-sensitive-p nil) < cl-prompt> (lte (make-array 3 :initial-element 0) (vector 1 2 42)) Error: Uncomparable objects #(0 0 0) and #(1 2 42).
None.
TBD.
An "error" is signalled when called on a pair of objects for which no predicate is defined (which is like what happens for undefined methods).
This document is put in the public-domain.