lispcommon-lispansi-common-lisp

Does #'adjoin in Common Lisp work as per HyperSpec when used with `:key`?


Looking at the docs for #'adjoin in the HyperSpec, I see the following in the Examples section:

(setq slist '()) =>  NIL
(setq slist (adjoin '(test-item 1) slist)) =>  ((TEST-ITEM 1))
(adjoin '(new-test-item 1) slist :key #'cadr) =>  ((TEST-ITEM 1))

I would have expected the following, instead:

(adjoin '(new-test-item 1) slist :key #'cadr) =>  ((NEW-TEST-ITEM 1) (TEST-ITEM 1))

My expectation is due to the following text in the HyperSpec (17.2.1):

When an object O is being considered iteratively against each element Ei of a sequence S by an operator F listed in the next figure, it is sometimes useful to control the way in which the presence of O is tested in S is tested by F. This control is offered on the basis of a function designated with either a :test or :test-not argument.

and, further:

The object O might not be compared directly to Ei. If a :key argument is provided, it is a designator for a function of one argument to be called with each Ei as an argument, and yielding an object Zi to be used for comparison. (If there is no :key argument, Zi is Ei.)

The function designated by the :key argument is never called on O itself. However, if the function operates on multiple sequences (e.g., as happens in set-difference), O will be the result of calling the :key function on an element of the other sequence.

So we have slist (the sequence, S) as '((TEST-ITEM 1)) and O as '(new-test-item 1). To check if O should be adjoined, the function #'cadr is applied to the element(s) of S, the first being '(test-item 1). So, that test gives:

(cadr '(test-item 1)) => 1

Now, when O, '(new-test-item 1), is checked against the result of applying #'cadr to E1 of S with #'eql (the equality function used when none is supplied with :test), the false result should mean that O is adjoined. At least that's what I would think. What am I misunderstanding?


Solution

  • This is a bug in the HyperSpec and Issue ADJOIN-SPECIFICATION has been written about it on the CLiki. The relevant parts are:

    Problem Description:

    CLHS specifies ADJOIN behavior in presence of a :KEY argument by reference to Section 17.2.1 Satisfying a Two-Argument Test. This is incorrect, since section 17.2.1 specifies that the key function is NOT called on the ITEM parameter, but ADJOIN does, as is specified in pushnew. Proposal (ADJOIN:CLARIFICATION):

    Replace:

    The test, test-not, and key affect how it is determined whether item is the same as an element of list. For details, see Section 17.2.1 (Satisfying a Two-Argument Test).

    by:

    Whether or not item is already a member of the list is determined by comparisons using :test or :test-not. The first argument to the :test or :test-not function is the result returned by the :key function (if supplied), applied to the item; the second argument is an element of the list as returned by the :key function (if supplied). If :key is supplied, it is used to extract the part to be tested from both item and the list element.

    Rationale:

    Clarification of the specification of ADJOIN.

    Current practice:

    All implementations implement ADJOIN as specified on pushnew, and not as specified on adjoin.