Partial Function Application Pragmatics

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.