Partial Function Application Pragmatics

“No. Layers. Onions have layers. Functions can have their arguments applied in several layers. Onions have layers. You get it? We both have layers.” ~Shrek

Partial function application is the fancy term for fixing some of the arguments to a function, to obtain a lower-arity function. And I have had case to use it at work over the past few weeks. In particular, I have wanted to map/filter over a list – but with functions that take more than one variable. E.g.

(defun already-defined-function (p1 p2)
  ...)

(...
 (mapcar #'already-defined-function
         p1-arg-list
         ???)
 ...)

In my case, p2-arg would be the same in each case. That is, I already had one desired binding for p2-arg, unlike the whole list of different values I wanted to map over for p1-arg-list. What I wanted was to fix the value of p2. Now, there are numerous ways to handle this. E.g.

;; wrap the call in a lambda
(mapcar (lambda (p1-arg)
          (already-defined-function p1-arg p2-arg))
        p1-arg-list)

;; build a list of repeated p2-arg:
(mapcar #'already-defined-function
        p1-arg-list
        (create-repeated-list p2-arg (length p1-arg-list)))

;; play human compiler for mapcar
(loop for p1-arg in p1-arg-list
      collect (already-defined-function p1-arg p2-arg))

;; implement a lenient mapcar
(defun lenient-mapcar (fn &rest args)
  (cons (apply fn args)
        (unless (exit-condition-p args)
          (apply lenient-mapcar
                 (append (list fn)
                         (mapcar (lambda (arg)
                                   (if (listp arg)
                                       (cdr arg)
                                       arg))
                                 args))))))

Note that none of the above allow me to directly express what I want to do: fix some of the arguments.

Instead, there’s a lambda wrapper – where the lambda could contain anything. There’s a kludge by making a list of repeated elements. There’s manually writing a loop to do what I want, which involves invoking a general utility – like with the lambda approach, but strikes me as having less semantics because there’s no specific signal for the mapping I want to do. Instead, it just so happens that a loop over a list which collects something for each element is equivalent to a mapcar.

lenient-mapcar, while a potentially useful utility if this problem comes up a lot, still suffers as an approach because there would need be similar implementations for e.g. lenient-filter. Besides, it doesn’t allow me to directly express what I want: fix some of the arguments.

Let’s fix that:

(defun right-fix-args (fn &rest args)
  (lambda (&rest more-args)
    (apply fn (append more-args args))))

Ah, finally, a utility to do what’s desired! Now, the above example becomes:

(mapcar (right-fix-args #'already-defined-function p2)
        p1-arg-list)

Order has been restored! I can express directly what I want!

Note also that the version using right-fix-args has a total of five atoms. Compare:

;; seven atoms
(mapcar (lambda (p1-arg)
          (already-defined-function p1-arg p2-arg))
        p1-arg-list)

;; seven atoms
(mapcar #'already-defined-function
        p1-arg-list
        (create-repeated-list p2-arg (length p1-arg-list)))

;; nine atoms
(loop for p1-arg in p1-arg-list
      collect (already-defined-function p1-arg p2-arg))

;; four atoms
(lenient-mapcar #'already-defined-function
                p1-arg-list
                p2-arg)

So using right-fix-args makes for the second-shortest version of the mapcar call here. Unlike with lenient-mapcar, though, it solves the general problem of fixing arguments, rather than the specific problem of using fixed arguments for mapcar.

Well, right-fix-args, left-fix-args and infix-args solve the general problem. As would a simple pattern-matching variant. E.g.

(defun partial-fix (fn &rest args)
  "Do a partial function application, specifying
   unfixed args with a :? placeholder."
  (lambda (&rest more-args)
    (let ((inner-args (copy-list more-args)))
      (apply fn (mapcar (lambda (arg)
                          (if (eq arg :?)
                              (pop inner-args)
                              arg))
                        args)))))

(defun test (a b c d e f)
  (list :a a :b b :c c :d d :e e :f f))

CL-USER> (funcall (partial-fix #'test 1 :? 3 :? 5 :?) 'b 'd 'f)
=> (:A 1 :B B :C 3 :D D :E 5 :F F)

While partial-fix is verboser than left-fix-args and right-fix-args, it solves the complicated cases neatly.

Click Here to Leave a Comment Below