Adding a key parameter
18th January 2016
The other day I wanted to find the longer of two lists or strings; the obvious way is:
(if (> (length a) (length b)) a b)
I thought that it would be nice if there was a variant of max which could take a :key parameter, like for example sort, so one could write:
(max* a b :key #'length)
For example, we could write:
CL-USER > (max* "short" "longer" :key #'length) "longer"
We could easily define this as a Lisp function:
(defun max* (a b &key (key #'identity)) (if (> (funcall key a) (funcall key b)) a b))
It does what we want:
CL-USER > (max* '(a b c) '(d e f g) :key #'length) (D E F G)
Generalising adding a key parameter
My next thought was to generalise this by writing a function that adds a key parameter to any function that returns one of two values, such as min, or max.
Here's my first attempt:
(defun add-key1 (test) #'(lambda (a b &key (key #'identity)) (if (funcall test (funcall key a) (funcall key b)) a b)))
This function add-key1 returns a function that applies the test function, using the specified key parameter. So for min we can write:
(funcall (add-key1 #'<) "tiny" "large" :key #'length)
We can associate the function returned by add-key1 with a function name using symbol-function:
(setf (symbol-function 'min*) (add-key1 #'<))
So now we can just write:
CL-USER > (min* "tiny" "large" :key #'length) "tiny"
Mystery best function
One problem: the above approach requires us to know the comparison function associated with the function we want to redefine, such as > in the case of max and < in the case of min. What about functions where the comparison function isn't obvious?
To test this idea I devised a function best that returns the "best" of two numbers according to a secret criterion. Here are some examples:
CL-USER > (best 7 12) 12 CL-USER > (best 12 15) 15 CL-USER > (best 15 7) 7
As these examples show, it's a non-transitive relation. The best function could be used as the basis for a game between two players, in which they each try to win as many rounds as possible. Here's the definition of best:
(defun best (a b) (if (> (abs (- a b)) (min a b)) (min a b) (max a b)))
The larger number wins, unless it's more than twice as large as the other number, in which case the smaller number wins.
Can the add-key function be made to work with this too? What we want is a function that takes one parameter, the function to be modified, and it should return a function that accepts a key parameter, like this:
(funcall (add-key2 #'best) "tiny" "too large" :key #'length)
This should display "tiny".
At first I thought this was impossible, but eventually saw a way of doing it. Try solving this before looking at my suggested answer below.
General add-key function
Here's my solution:
(defun add-key2 (fun) #'(lambda (a b &key (key #'identity)) (let ((afun (funcall key a)) (bfun (funcall key b))) (if (eq (funcall fun afun bfun) afun) a b))))
This function add-key takes a function, and returns a new function with a key parameter added to its parameters. So now we can write:
(setf (symbol-function 'best*) (add-key1 #'best))
and use this as a new function:
CL-USER > (best* "tiny" "too large" :key #'length) "tiny"
blog comments powered by Disqus