Continuing with the features of Lisp that a newcomer arriving from C++ might not have encountered, we look at define-modify-macro. The rationale behind this is that certain operations that you might naively think of as modifying a data structure might actually create a new data structure. This is generally not true of functions that act on objects, but may be true of functions that act on lists.
The C++ programmer is used to having explicit syntax to decide whether a variable is passed by value or by reference. In Lisp, that is determined by the data type, rather than in the declaration of the function. Numbers and symbols are passed by value. If a parameter to a function evaluates as a number or a symbol, and the function modifies the parameter in its body, the effect of such modification is not propagated back to the caller. On the other hand, if the parameter is a structure, object, or array, it is passed by reference, and modifications made to the contents of such entities will persist after the function returns. Lists, though, are a bit complicated. The thing that is passed is a pointer to a node in a singly-linked list. If the list is modified, for instance by reversing its contents, the caller will still have a pointer to the same node, but now the forward links from that node won’t necessarily traverse the whole list. You go from having a reference to the beginning of the list to having a reference to somewhere in the middle of the list, and the forward linkage means you can’t back up to see what has been moved ahead of your entry point. Consequently, functions that modify lists will usually return a new list, rather than modifying the linkages of the passed list.
It’s a common mistake for a beginner to expect the sort function, when acting on a list as opposed to an array, to modify the list and make it sorted. In fact, it returns a new list, and destroys the original list (the programmer cannot make any assumptions about what the input list looks like after sort is called). The programmer is responsible for retaining the returned value and using it when the sorted list is desired.
The define-modify-macro macro allows you to construct the fairly common construct of calling a function on a list and assigning the result back to the place (think of this like a variable or pointer) that held the original list. Here’s an example of how this might work with reverse.
(define-modify-macro reversef (&rest args) reverse) (defun demonstrate () (let* ((input-list '(9 1 3 2 4 8 7 6)) (input-list-copy input-list)) (format t "Input list: ~A~%" input-list) (reverse input-list) (format t "Input list after calling reverse: ~A~%" input-list) (reversef input-list) (format t "Input list after calling reversef: ~A~%" input-list) (format t "Input list copy after calling reversef: ~A~%" input-list-copy)))
CL-USER> (demonstrate) Input list: (9 1 3 2 4 8 7 6) Input list after calling reverse: (9 1 3 2 4 8 7 6) Input list after calling reversef: (6 7 8 4 2 3 1 9) Input list copy after calling reversef: (9 1 3 2 4 8 7 6) NIL
Notice that only the place passed to reversef sees the change. The input-list-copy variable, while originally pointing at the same list as input-list, is a different place, and is not updated. Where, originally, the two places pointed to the same list, they now point to different lists.