In Common Lisp it is idiomatic for functions to allow their behavior to be overridden with keyword arguments.
So Lispers often write functions whose sole purpose is to call a function that takes keyword arguments, but with different defaults.
Since keyword arguments are processed from left to right, and the leftmost occurrence takes precedence, this is easy to do:
(hash-table-test (make-hash-table)) => EQL
(defun make-equal-hash-table (&rest args &key &allow-other-keys)
(apply #'make-hash-table :test 'equal args))
(hash-table-test (make-equal-hash-table)) => EQUAL
The downside is that the new default is fixed: the caller cannot override it.
(hash-table-test (make-equal-hash-table :test 'eql)) => EQUAL
Can we change the default, but still allow the new default to be overridden? Of course. The obvious way is to append the new default to the end of the argument list:
(defun make-equal-hash-table-v2 (&rest args &key &allow-other-keys)
(apply #'make-hash-table (append args '(:test equal))))
(hash-table-test (make-equal-hash-table-v2)) => EQUAL
(hash-table-test (make-equal-hash-table-v2 :test 'eql)) => EQL
Unfortunately this allocates a new list. Even SBCL is not smart enough to eliminate the allocation, as you can see from (disassemble 'make-hash-table-v2)
:
; DE5: 488B3DA4FEFFFF MOV RDI, [RIP-348] ; '(:TEST EQUAL)
; DEC: 488B05A5FEFFFF MOV RAX, [RIP-347] ; #<SB-KERNEL:FDEFN SB-IMPL::APPEND2>
Of course you may not care about such minor allocations. But for situations where you do care, there is another way — one that involves no allocation.
To capture multiple values in Common Lisp, we generally use multiple-value-bind
:
(multiple-value-bind (x y z) (values 1 2 3)
(+ x y z))
=> 6
But multiple-value-bind
is just a macro; the actual special form that allows capturing multiple values is the much less frequently encountered multiple-value-call
. You could rewrite the above to use multiple-value-call
directly:
(multiple-value-call (lambda (&optional x y z)
(+ x y z))
(values 1 2 3))
=> 6
But multiple-value-call
is more flexible than that, because it captures all the values returned from all the forms that are provided to it as arguments:
(multiple-value-call (lambda (&optional x y z)
(+ x y z))
(values 1) (values 2 3))
=> 6
(multiple-value-call (lambda (&optional x y z)
(+ x y z))
(values 1 2) 3)
=> 6
(multiple-value-call (lambda (&optional x y z)
(+ x y z))
1 2 3)
=> 6
And this takes place entirely on the stack, without any allocations.
To complete the trick, besides multiple-value-call
, we need one other function, values-list
, which takes a list and returns it as multiple values:
(values-list '(1 2 3))
=> 1, 2, 3
And now, with multiple-value-call
and values-list
, we have everything we need to write wrapper functions that allow their new defaults to be overridden, without any allocation:
(defun make-equal-hash-table-v3 (&rest args &key &allow-other-keys)
(multiple-value-call #'make-hash-table
(values-list args)
(values :test 'equal)))
(hash-table-test (make-equal-hash-table-v3)) => EQUAL
(hash-table-test (make-equal-hash-table-v3 :test 'eql)) => EQL
(To understand how the argument list is constructed, watch what happens when multiple-value-calling list
:
(multiple-value-call #'list (values-list '(:test eql)) :test 'equal)
=> (:TEST EQL :TEST EQUAL)
)