Functions that take one argument and return one value are cool because you can make pipelines out of them. You just send the output of one function into the input of the next. Of course the output of the first function must be of a type that the second function can accept. Or you can just stick with functions that take and return the same type, and then you can compose these as desired.
But what if you want to compose binary functions that take two arguments and return two values? There are a couple of ways to take multiple arguments: you can pass them in a list (or vector), or you can pass the arguments one at a time by curry the function.
(defun curry (B) (lambda (l) (lambda (r) (funcall B l r))))
There are two lambdas here. The outer lambda accepts the left argument and returns the inner lambda. The inner lambda accepts the right argument and calls the original binary function with both arguments.
To call a curried function, you first call it with the left argument. This will return a lambda that you call with the right argument.
(funcall (funcall curried-binary-function left) right)
The two-argument identity function would be curried like this:
(defun curried-values (left) (lambda (right) (values left right)))
Composing two curried functions is a little complicated. Let us
suppose that F
and G
are curried binary
functions. Further assume that G
has already been
applied to its left argument, i.e., it is an inner lambda of a
curried binary function. We want to compute the inner lambda
of F
composed with G
.
(defun curried-compose (f g) (lambda (right) ; return an inner lambda (multiple-value-bind (vall valr) (funcall g right) (funcall (funcall f vall) valr))))
Here is how it works. We return an inner lambda that accepts the
right argument of the composition. We call the inner lambda G
on
this argument and collect the two values it returns. We then pass
the two values one at a time to F
.
If we did this right, we should end up with these identities:
(curried-compose #’F (curried-values left)) ==> (F left)
and
(curried-compose #’curried-values G-inner) ==> G-inner
furthermore, curried-compose
should be
associative:
(curried-compose (curried-composeF
G
)H
) ==> (curried-composeF
(curried-composeG
H
))
Let’s try it out.
(defun curried-cons (left) (lambda (right) (cons left right))) ;; Check first identity. (let ((x (curried-compose #’curried-cons (curried-values ’car)))) (assert (equal (funcall x ’cdr) ’(car . cdr)))) ;; Check second identity. (let ((x (curried-compose #’curried-values (curried-cons ’car)))) (assert (equal (funcall x ’cdr) ’(car . cdr)))) ;; Check associativity. (defun curried-sum-difference (left) (lambda (right) (values (+ left right) (- left right)))) (let ((x (curried-compose #’curried-cons (curried-compose #’curried-sum-difference (curried-values 7)))) (y (curried-compose (lambda (v) (curried-compose #’curried-cons (curried-sum-difference v))) (curried-values 7)))) (assert (equal (funcall x 3) (funcall y 3))))
If the binary function is closed over its right argument, then when you curry it, the inner lambda will be closed over its argument. We should be able to compose various inner lambdas to make a pipeline. The pipeline will daisy chain the right argument from one curried function in the pipeline to the next.
Pipelines like this have two properties we want to exploit: first, they force an order of execution on the functions in the pipeline. The earlier stages in the pipeline have to produce an answer before the later stages consume it, even if all the functions in the pipeline are lazy pure functional. Second, we can abstract out the argument we are daisy chaining through the stages.
Let’s use this in a real problem. Our task is to build a binary tree where each node has a serial number. The serial number will be our right hand argument which is daisy chained through the computation.
(defun next-sn (sn) (values sn (1+ sn))) (defun curried-make-node (depth children) (curried-compose (lambda (sn) (curried-values (cons (cons depth sn) children))) #’next-sn)) (defun curried-make-tree (depth) (if (zerop depth) (curried-make-node depth nil) (curried-compose (lambda (left-branch) (curried-compose (lambda (right-branch) (curried-make-node depth (list left-branch right-branch))) (curried-make-tree (1- depth)))) (curried-make-tree (1- depth)))))
curried-make-tree
returns a curried function. It
hasn’t built a tree, but built a function that builds the
tree once it gets the serial number passed in.
Notice these two things: we have no global state or assignment, but
we get a serial number that is incremented for each node. The
serial number is passed around as the curried right hand argument
and returned as the right hand value. Notice, too,
that curried-make-tree
has no mention of the serial
number. It is a hidden variable.
curried-compose
is reminiscent of a let
expression, so we can create some analagous syntactic sugar:
(defmacro LetM (((var val) &rest more) &body body) ‘(curried-compose (lambda (,var) ,@(if more ‘((LetM ,more ,@body)) body)) ,val)) (defun curried-make-tree (depth) (if (zerop depth) (curried-make-node depth nil) (LetM ((left-branch (curried-make-tree (1- depth))) (right-branch (curried-make-tree (1- depth)))) (curried-make-node depth (list left-branch right-branch)))))
There is a special name for the inner lambda that is expecting the
right hand argument. It is a “monad”. curried-values
is the monad “unit”, and curried-compose
is the monad
“bind”.
As I mentioned in a previous post, it makes more sense to use this approach in a lazy functional language. The chain of curried functions force sequential evaluation because the output of each curried function must be computed before it becomes the input of the next curried function. The right hand argument that is threaded through the curried calls can be used as a “store” that is (functionally) updated by each curried function before passing it along, giving you the illusion of mutable storage.
A monad is how
you embed an imperative language within a pure, lazy, functional
language. But in a mixed language like Lisp, you can force
sequential evaluation by progn
and the store is
actually mutable, so there is less of a need for monads.
Nonetheless, they provide a way to “hide” a variable that
is threaded through the curried functions and that can come in
handy. For example, if you are doing I/O, you can hide the stream
variable so you don’t have to pass it along to every
intermediate function.
1 comment:
Indeed I find it handy to use the monad paradigm while doing CPS, where you hide the continuation.
Post a Comment