1 Explain
dxtr edited this page 2026-10-18 12:07:40 +02:00

Explain

explain is the single extension point for failure messages: a generic function dispatched on the operator of a failed function-call form, returning one line or NIL.

(defgeneric explain (operator values)
  (:documentation "One line on why (OPERATOR . VALUES) was false, or NIL."))
  • It is asked only when an assertion failed and the form was a function-call form. (not ...) forms and opaque forms get no explanation.
  • operator is the symbol at the head of the form; values is the list of every operand's value in call order, literals included.
  • A method returns a string without a trailing newline, or NIL when it has nothing useful to say. A method must not signal — a method that errors is a bug in the method.

The point of the protocol is the eq/eql/equal hints: the five minutes everyone loses once a year to (eq "a" "a") become one line.

Shipped methods

Text in code is the exact output. ~s prints under the report's printer settings (downcased, no package prefixes). Each method returns NIL when the values are not of the kind it describes.

Operator When Explanation
= two reals differ by ~a with (abs (- a b))
eq two numbers that are = = but not EQ; use = or EQL
eq two values that are equal EQUAL but not EQ (distinct objects); did you mean EQUAL?
eql two numbers = but not eql = but not EQL (different types); use =
eql two values that are equal EQUAL but not EQL (distinct objects); did you mean EQUAL?
equal, equalp two strings as string= below
equal, equalp two sequences, mismatch past the end of one lengths differ: ~d vs ~d
equal, equalp two sequences, mismatch inside both first difference at position ~d: ~s vs ~s
equal two numbers that are = numerically equal but of different types; use =
equalp two hash tables keys only in the first: ~s; keys only in the second: ~s, or values differ at key ~s: ~s vs ~s
string=, string-equal two strings, mismatch past the end of one same up to index ~d, then lengths differ: ~d vs ~d
string=, string-equal two strings, mismatch inside both differ at index ~d: ~s vs ~s, each an excerpt of up to 20 characters with … where cut
typep always actual type: ~s with (type-of value)
every a predicate and a sequence first failing element at position ~d: ~s
some a predicate and a sequence no element satisfies ~s among ~d
subsetp two lists missing from the second list: ~s
search two sequences ~s does not occur in ~s
approx= always differ by ~a, tolerance ~a
called-p always (see Mocks) ~(~s~) was called ~d time~:p, with ~{~s~^, ~} or ~(~s~) was never called

Teaching assay about your predicates

Add a method with an (eql 'symbol) specializer:

(defmethod assay:explain ((op (eql 'vec3=)) values)
  (destructuring-bind (a b) values
    (format nil "components differ by ~a" (map 'list #'- a b))))
CL-USER> (is (vec3= #(1.0 2.0 3.0) #(1.0 2.5 3.0)))
✗ (is (vec3= #(1.0 2.0 3.0) #(1.0 2.5 3.0)))
      #(1.0 2.0 3.0) ⇒ #(1.0 2.0 3.0)
      #(1.0 2.5 3.0) ⇒ #(1.0 2.5 3.0)
      components differ by (0.0 -0.5 0.0)

Return NIL to decline — the operands are still shown, just without an extra line. Because values holds every operand (keyword arguments included), a method for a predicate that takes options can pick them out:

(defmethod assay:explain ((op (eql 'close-enough)) values)
  (destructuring-bind (a b &key (epsilon 1e-3)) values
    (format nil "off by ~a (epsilon ~a)" (abs (- a b)) epsilon)))

Next: Structure.