Last active
May 2, 2016 13:49
-
-
Save wobh/4e9c40a76425494a1dc205b66a008bee to your computer and use it in GitHub Desktop.
pipes via reduction
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; Per https://twitter.com/wobher/status/726442806353399808 what I'm | |
;; trying to say, all too succinctly, is that structurally an | |
;; expression like "foo |> bar |> baz |> qux" or even | |
;; "foo.bar.baz.qux" is commonly considered syntax for a bunch of | |
;; nested calls: "qux(baz(bar(foo)))". However, in Lisp or any other | |
;; non-infix language, we can see that this need not be the case. | |
;; Lets define a `pipe' function; we'd like the following assertions | |
;; about it to be true: | |
(assert (= 0 | |
(1+ (lognot 0)) | |
(pipe 0 #'1+ #'lognot))) | |
(assert (= -2 | |
(lognot (1+ 0)) | |
(pipe 0 #'lognot #'1+))) | |
;; This should establish that it's non-commutative and the equivalent of | |
;; nested calls as above. We'd also like it to be a function so that | |
;; we can compose it's arguments at runtime. This assertion should | |
;; cover that: | |
(assert (zerop (funcall #'pipe 0 #'1+ #'lognot))) | |
;; So how should we implement this? One trick is to look at it's | |
;; arguments and see that `pipe' with two arguments is the same as | |
;; `funcall' with two arguments but with the order reversed: | |
(assert (= 1 | |
(funcall #'1+ 0) | |
(pipe 0 #'1+))) | |
;; We could write a recursive definition: | |
(defun pipe (value &rest functions) | |
(labels ((pipe-recursive (acc funcs) | |
(if (null funcs) | |
acc | |
(pipe-recursive (funcall (first funcs) acc) | |
(rest funcs))))) | |
(pipe-recursive value (reverse functions)))) | |
;; That requires us to reverse an arbitrarily long list of | |
;; functions. Besides, that use of `funcall' in the recursive call, | |
;; while syntactically symmetrical, offends our sense of evaluation | |
;; balance. Fortunately, we can do it iterively any number of ways, | |
;; but let's forgo almost all of them. | |
;; Look at what we're doing in the recursive version: we're evaluating | |
;; each argument pair-wise on the accumulated result of the last pair | |
;; of accumulator and function. This is a classic reduction, | |
;; specifically, a right fold. So these assertions must be true: | |
(assert (= 0 | |
(foldr #'funcall (cons 0 (list #'1+ #'lognot))) | |
(pipe 0 #'1+ #'lognot))) | |
(assert (= -2 | |
(foldr #'funcall (cons 0 (list #'lognot #'1+))) | |
(pipe 0 #'lognot #'1+))) | |
;; And just look at that beautiful `(cons value (list func1 func2 | |
;; ...))' construction! LISt Processing is the very soul of LISP. How | |
;; do we do a right fold in Common Lisp? | |
(defun foldr (function values) | |
(reduce function | |
(rest values) | |
:initial-value (first values) | |
:from-end t)) | |
;; From here, `pipe' is trivial: | |
(defun pipe (value &rest functions) | |
(foldr #'funcall (cons value functions))) | |
;; All of the assertions are true. | |
;; In later tweets, I made some remarks about `reduce' being the | |
;; equivalent of `pipe' rather than the means of `pipe'. I want to | |
;; concede here, that those statements were either wrong or certainly | |
;; awkward. My cheif concern was structural, looking at the arguments | |
;; of `pipe' as a list that can be constructed at runtime is, to my | |
;; mind, an essential, fundamental technique--something that Lisp | |
;; makes mentally easier for the programmer by virtue of highly | |
;; composable syntax, which may lead to highly composable code. | |
;; I also want to say, it certainly not a fault of the infix `.' or | |
;; `|>' operations or `->' and `->>' macros that they require the | |
;; sequence of functions to be known at write-time. They almost always | |
;; are known already, and write-time conveniences are still | |
;; conveniences. | |
;; One could argue that a program that composes arguments to `pipe' | |
;; would be harder to reason about, which I'll concede so far as it's | |
;; certainly possible, perhaps even likely. I'll dispute that it's | |
;; unnecessary--just try writing a program that must perform according | |
;; to a large domain of command-line options and see if the technique | |
;; doesn't make it much easier to read and reason about. Composability | |
;; isn't a virtue for no reason. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment