About me

About me


RSS feed

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)

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)

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)

CL-USER > (best 12 15)

CL-USER > (best 15 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)


blog comments powered by Disqus