├── .gitignore ├── LICENSE.html ├── README.md ├── arrows.asd ├── arrows.lisp └── test.lisp /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.fasl 3 | -------------------------------------------------------------------------------- /LICENSE.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

6 | 8 | CC0 11 |
12 | To the extent possible under law, Svante von Erichsen has waived all 13 | copyright and related or neighboring rights to 14 | arrows. 15 | This work is published from: 16 | 20 | Deutschland.

21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arrows 2 | 3 | Implements threading macros, inspired by Clojure (both core and 4 | the [swiss-arrows](https://github.com/rplevy/swiss-arrows) library). 5 | 6 | This is an ASDF system providing the package `arrows`. Its home is at 7 | https://gitlab.com/Harleqin/arrows, with a mirror at 8 | https://github.com/Harleqin/arrows. 9 | 10 | ## Overview 11 | 12 | You get: 13 | 14 | - the basic “thrushing” arrows `->` and `->>`, 15 | - “diamond” arrows `-<>` and `-<>>`, 16 | - binding arrow `as->`, 17 | - “maybe” arrows `some->` and `some->>`, 18 | - conditional arrows `cond->` and `cond->>`, and 19 | - “double arrow cancellers” `->*` and `as->*`. 20 | 21 | As far as I see, `->*` and `as->*` are new. Their purpose is to be nested in 22 | other threading forms to temporarily supplant their behaviour (see “Nesting” 23 | below). 24 | 25 | ## Other arrow libraries 26 | 27 | - [`arrow-macros`](https://github.com/hipeta/arrow-macros) 28 | - [`cl-arrows`](https://github.com/nightfly19/cl-arrows) is superseded by `arrows`. 29 | 30 | ## Notable differences to Clojure and swiss-arrows 31 | 32 | - `Cond->` and `cond->>` use one additional paren nesting for the clauses, so 33 | that each clause can contain multiple forms to thread/execute. 34 | 35 | - `-<>` and `-<>>` do not support literals to insert the `<>` placeholder. The 36 | placeholder really only works at the outermost level of the threaded forms. 37 | The reason for this is mostly that Common Lisp does not have so many literal 38 | syntax elements (by default) where it would make sense to do this kind of 39 | insertion. If you do need anything fancy, use `as->` or `as->*` for a real 40 | lexical binding. 41 | 42 | ## Notable differences to arrow-macros 43 | 44 | - `Cond->` and `cond->>` use one additional paren nesting for the clauses, so 45 | that each clause can contain multiple forms to thread/execute. 46 | 47 | - `-<>` and `-<>>` do not use a code walker to find out whether a placeholder is 48 | present in the next threaded form. The placeholder only works at the 49 | outermost level of the threaded forms. This reduces the dependencies of 50 | `arrows` (there are none at present). Instead, the recommendation is to use 51 | binding arrows `as->` or `as->*`, possibly nested (see below). 52 | 53 | - There is no `some-<>` nor `some-<>>` yet. Instead, you can use nested `as->` 54 | or `as->*` forms (see below). 55 | 56 | ## Nesting 57 | 58 | One useful idiom is to nest these arrows. The basic example is to use `->>` 59 | inside `->`: 60 | 61 | (-> deeply-nested-plist 62 | (getf :foo) 63 | (getf :bar) 64 | (->> (mapcar #'reverse))) 65 | 66 | This inspired the discovery of `->*`, which enables the inverse nesting: 67 | 68 | (->> deeply-nested-alist 69 | (assoc :foo) 70 | cdr 71 | (assoc :bar) 72 | cdr 73 | (->* (mod 3)) 74 | (expt 2)) 75 | 76 | Generally useful for overriding defaults are `as->` and `as->*`: 77 | 78 | (-> 3 79 | (as-> $ 80 | (< x $ y)) 81 | not) 82 | 83 | (some->> 15 84 | (as->* $ 85 | (progn 86 | (format t debug-formatter $) 87 | $)) 88 | (/ 75)) 89 | 90 | However, don't overdo it! This quickly leads to an unreadable mess. You may 91 | well be better off with a few explicit `let` bindings. 92 | 93 | ## Documentation 94 | 95 | #### `->` initial-form _&rest_ forms => results 96 | _[macro]_ Inserts INITIAL-FORM as first argument into the first of FORMS, the 97 | result into the next, etc., before evaluation. FORMS are treated as list 98 | designators. 99 | 100 | #### `->>` initial-form _&rest_ forms => results 101 | _[macro]_ Like `->`, but the forms are inserted as last argument instead of 102 | first. 103 | 104 | #### `->*` _&rest_ forms => results 105 | _[macro]_ Like `->`, but the last form is used as initial form, then the 106 | remainung forms as in `->`. This is intended for inversing the default in a 107 | `->>` form. 108 | 109 | #### `-<>` initial-form _&rest_ forms => results 110 | _[macro]_ Like `->`, but if a form in FORMS has one or more symbols named `<>` 111 | as top-level element, each such symbol is substituted by the primary result of 112 | the form accumulated so far, instead of it being inserted as first argument. 113 | Also known as diamond wand. 114 | 115 | #### `-<>>` initial-form _&rest_ forms => results 116 | _[macro]_ Like `-<>`, but if a form in FORMS has no symbols named `<>` as 117 | top-level element, insertion is done like in `->>`. Also known as diamond 118 | spear. 119 | 120 | #### `as->` initial-form var _&rest_ forms => results 121 | _[macro]_ Binds INITIAL-FORM to VAR, then successively each of FORMS to VAR, 122 | finally returns the last value of VAR. 123 | 124 | #### `as->*` var _&rest_ forms => results 125 | _[macro]_ Shorthand for the combination of `->*` and `as->`: the last form is 126 | used for initial binding, then the remaining forms used as in `as->`. This is 127 | intended for overriding the default in a `->>` form. 128 | 129 | #### `some->` initial-form _&rest_ forms => results 130 | _[macro]_ Like `->`, but short-circuits to nil as soon as either INITIAL-FORM or 131 | any of FORMS return nil. This is like all these forms are lifted to the maybe 132 | monad. 133 | 134 | #### `some->>` initial-form _&rest_ forms => results 135 | _[macro]_ Like `some->`, but with insertion behaviour as in `->>`. 136 | 137 | #### `cond->` initial-form _&rest_ clauses => results 138 | _[macro]_ CLAUSES is a list of clauses similar to COND clauses, each clause 139 | comprising first a test form, then a body of further forms. `Cond->` evaluates 140 | INITIAL-FORM to a value, then for each clause whose test evaluates to true, 141 | pipes (as in `->`) the value through each form in the body of the clause. Note 142 | that unlike in COND, there is no short-circuiting: each clause gets tested 143 | regardless of the outcome of the clauses before. 144 | 145 | #### `cond->>` initial-form _&rest_ clauses => results 146 | _[macro]_ Like `cond->`, but with insertion behaviour as in `->>`. 147 | 148 | ## Examples 149 | 150 | (-> 3 151 | /) ; insert into designated list (/) 152 | => 1/3 153 | 154 | (-> 3 155 | (expt 2)) ; insert as first argument 156 | => 9 157 | 158 | (->> 3 159 | (expt 2)) ; insert as last argument 160 | => 8 161 | 162 | (-<>> (list 1 2 3) 163 | (remove-if #'oddp <> :count 1 :from-end t) ; substitute <> 164 | (reduce #'+) ; insert last 165 | /) ; list designator 166 | => 1/3 167 | 168 | (let ((x 3)) 169 | (-<> (incf x) ; (let ((r (incf x))) 170 | (+ <> <>))) ; (+ r r)) 171 | => 8 172 | 173 | (->> 3 174 | (/ 12) ; (/ 12 3) => 4 175 | (->* (/ 2))) ; (/ 4 2) => 2 176 | => 2 177 | 178 | (flet ((say (n) 179 | (cond->> nil 180 | ((zerop (mod n 3)) (cons "Fizz")) 181 | ((zerop (mod n 5)) (cons "Buzz")) 182 | (t (->* (or (list (princ-to-string n)))) 183 | reverse 184 | (apply #'concatenate 'string))))) 185 | (mapcar #'say '(9 10 11 12 13 14 15))) 186 | => ("Fizz" "Buzz" "11" "Fizz" "13" "14" "FizzBuzz") 187 | -------------------------------------------------------------------------------- /arrows.asd: -------------------------------------------------------------------------------- 1 | (defsystem #:arrows 2 | :name "arrows" 3 | :version "0.2.0" 4 | :author "Svante von Erichsen " 5 | :license "CC0" 6 | :description 7 | "Implements -> and ->> from Clojure, as well as several expansions on the 8 | idea." 9 | :components ((:file "arrows")) 10 | :in-order-to ((asdf:test-op (asdf:test-op #:arrows/test)))) 11 | 12 | (defsystem #:arrows/test 13 | :depends-on (#:arrows #:hu.dwim.stefil) 14 | :components ((:file "test")) 15 | :perform (asdf:test-op (c v) (uiop:symbol-call '#:arrows/test 16 | '#:test-arrows))) 17 | -------------------------------------------------------------------------------- /arrows.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:cl-user) 2 | 3 | (defpackage #:arrows 4 | (:use #:common-lisp) 5 | (:export #:-> 6 | #:->> 7 | #:-<> 8 | #:-<>> 9 | #:as-> 10 | #:some-> 11 | #:some->> 12 | #:cond-> 13 | #:cond->> 14 | #:->* 15 | #:as->*)) 16 | 17 | (in-package #:arrows) 18 | 19 | (defun simple-inserter (insert-fun) 20 | (lambda (acc next) 21 | (if (listp next) 22 | (funcall insert-fun acc next) 23 | (list next acc)))) 24 | 25 | (defun expand-arrow (initial-form forms insert-fun) 26 | (reduce (simple-inserter insert-fun) 27 | forms 28 | :initial-value initial-form)) 29 | 30 | (defmacro -> (initial-form &rest forms) 31 | "Inserts INITIAL-FORM as first argument into the first of FORMS, the result 32 | into the next, etc., before evaluation. FORMS are treated as list designators." 33 | (expand-arrow initial-form forms #'insert-first)) 34 | 35 | (defmacro ->> (initial-form &rest forms) 36 | "Like ->, but the forms are inserted as last argument instead of first." 37 | (expand-arrow initial-form forms #'insert-last)) 38 | 39 | (defun diamond-inserter (insert-fun) 40 | (simple-inserter (lambda (acc next) 41 | (case (count-if #'<>p next) 42 | (0 (funcall insert-fun acc next)) 43 | (1 (substitute-if acc #'<>p next)) 44 | (t (let ((r (gensym "R"))) 45 | `(let ((,r ,acc)) 46 | ,(substitute-if r #'<>p next)))))))) 47 | 48 | (defmacro -<> (initial-form &rest forms) 49 | "Like ->, but if a form in FORMS has one or more symbols named <> as top-level 50 | element, each such symbol is substituted by the primary result of the form 51 | accumulated so far, instead of it being inserted as first argument. Also known 52 | as diamond wand." 53 | (reduce (diamond-inserter #'insert-first) 54 | forms 55 | :initial-value initial-form)) 56 | 57 | (defmacro -<>> (initial-form &rest forms) 58 | "Like -<>, but if a form has no symbol named <>, the insertion is done at the 59 | end like in ->>. Also known as diamond spear." 60 | (reduce (diamond-inserter #'insert-last) 61 | forms 62 | :initial-value initial-form)) 63 | 64 | (defun <>p (form) 65 | "Predicate identifying the placeholders for the -<> and -<>> macros." 66 | (and (symbolp form) 67 | (string= form "<>"))) 68 | 69 | (defun insert-first (arg surround) 70 | "Inserts ARG into the list form SURROUND as its first argument, after the 71 | operator." 72 | (list* (car surround) 73 | arg 74 | (cdr surround))) 75 | 76 | (defun insert-last (arg surround) 77 | "Inserts ARG into the list form SURROUND as its last argument." 78 | (append surround (list arg))) 79 | 80 | (defmacro as-> (initial-form var &rest forms) 81 | "Binds INITIAL-FORM to VAR, then successively each of FORMS to VAR, finally 82 | returns the last value of VAR." 83 | `(let* ,(mapcar (lambda (form) 84 | (list var form)) 85 | (cons initial-form forms)) 86 | ,var)) 87 | 88 | (defun some-inserter (insert-fun) 89 | (lambda (acc next) 90 | (destructuring-bind (let* bindings var) acc 91 | `(,let* (,@bindings 92 | (,var (when ,var 93 | ,(funcall insert-fun var next)))) 94 | ,var)))) 95 | 96 | (defun expand-some (initial-form forms insert-fun) 97 | (let ((var (gensym "some"))) 98 | (reduce (some-inserter (simple-inserter insert-fun)) 99 | forms 100 | :initial-value `(let* ((,var ,initial-form)) 101 | ,var)))) 102 | 103 | (defmacro some-> (initial-form &rest forms) 104 | "Like ->, but short-circuits to nil as soon as either INITIAL-FORM or any of 105 | FORMS return nil. This is like all these forms are lifted to the maybe monad." 106 | (expand-some initial-form forms #'insert-first)) 107 | 108 | (defmacro some->> (initial-form &rest forms) 109 | "Like some->, but with insertion behaviour as in ->>." 110 | (expand-some initial-form forms #'insert-last)) 111 | 112 | (defun cond-inserter (insert-fun) 113 | (lambda (acc next) 114 | (destructuring-bind (let* bindings var) acc 115 | (destructuring-bind (test . forms) next 116 | `(,let* (,@bindings 117 | (,var (if ,test 118 | ,(expand-arrow var forms insert-fun) 119 | ,var))) 120 | ,var))))) 121 | 122 | (defun expand-cond (initial-form clauses insert-fun) 123 | (let ((var (gensym "cond"))) 124 | (reduce (cond-inserter (simple-inserter insert-fun)) 125 | clauses 126 | :initial-value `(let* ((,var ,initial-form)) 127 | ,var)))) 128 | 129 | (defmacro cond-> (initial-form &rest clauses) 130 | "CLAUSES is a list of clauses similar to COND clauses, each clause comprising 131 | first a test form, then a body of further forms. Cond-> evaluates INITIAL-FORM 132 | to a value, then for each clause whose test evaluates to true, pipes (as in ->) 133 | the value through each form in the body of the clause. Note that unlike in 134 | COND, there is no short-circuiting: each clause gets tested regardless of the 135 | outcome of the clauses before." 136 | (expand-cond initial-form clauses #'insert-first)) 137 | 138 | (defmacro cond->> (initial-form &rest clauses) 139 | "Like cond->, but with insertion behaviour as in ->>." 140 | (expand-cond initial-form clauses #'insert-last)) 141 | 142 | (defmacro ->* (&rest forms) 143 | "Like ->, but the last form is used as initial form, then the remaining forms 144 | used as in ->. This is intended for inversing the default in a ->> form. 145 | 146 | Example: 147 | 148 | (->> 3 149 | (/ 12) 150 | (->* (/ 2))) 151 | => 2" 152 | `(-> ,@(append (last forms) (butlast forms)))) 153 | 154 | (defmacro as->* (var &rest forms) 155 | "Shorthand for the combination of ->* and as->: the last form is used for 156 | initial binding, then the remaining forms used as in as->. This is intended for 157 | overriding the default in a ->> form." 158 | `(as-> ,@(last forms) ,var 159 | ,@(butlast forms))) 160 | -------------------------------------------------------------------------------- /test.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:cl-user) 2 | 3 | (defpackage #:arrows/test 4 | (:use #:cl #:arrows #:hu.dwim.stefil)) 5 | 6 | (in-package #:arrows/test) 7 | 8 | (defsuite* test-arrows) 9 | 10 | (deftest test--> () 11 | (is (= (-> 3 /) 1/3)) 12 | (is (= (-> 3 (/)) 1/3)) 13 | (is (= (-> 3 (/ 2)) 3/2)) 14 | (is (= (-> 3 (/ 2) /) 2/3))) 15 | 16 | (deftest test-->> () 17 | (is (= (->> 3 /) 1/3)) 18 | (is (= (->> 3 (/)) 1/3)) 19 | (is (= (->> 3 (/ 2)) 2/3)) 20 | (is (= (->> 3 (/ 2) /) 3/2))) 21 | 22 | (deftest test--<> () 23 | (is (= (-<> 3 /) 1/3)) 24 | (is (= (-<> 3 (/)) 1/3)) 25 | (is (= (-<> 3 (/ 2)) 3/2)) 26 | (is (= (-<> 3 (/ 2) /) 2/3)) 27 | (is (= (let ((x 3)) 28 | (-<> (incf x) 29 | (+ <> <>))) 30 | 8))) 31 | 32 | (deftest test--<>> () 33 | (is (= (-<>> 3 /) 1/3)) 34 | (is (= (-<>> 3 (/)) 1/3)) 35 | (is (= (-<>> 3 (/ 2)) 2/3)) 36 | (is (= (-<>> 3 (/ 2) /) 3/2)) 37 | (is (= (-<>> (list 1 2 3) 38 | (remove-if #'oddp <> :count 1 :from-end t) 39 | (reduce #'+) 40 | /) 41 | 1/3))) 42 | 43 | (deftest test-as-> () 44 | (is (= (as-> 3 $ 45 | (* 5 $) 46 | (/ $ 7)) 47 | 15/7)) 48 | (is (= (as-> 0 n 49 | (1+ n) 50 | (1+ n)) 51 | 2))) 52 | 53 | (deftest test-some-> () 54 | (is (null (some-> 3 55 | (+ 5) 56 | (member '(2 5 9)) 57 | first 58 | (* 9)))) 59 | (is (= (some-> 3 60 | (+ 5) 61 | (member '(2 5 8 9)) 62 | first 63 | (* 9)) 64 | 72)) 65 | (is (= (some-> 3 66 | (+ 5) 67 | (member '(2 5 8 9)) 68 | second 69 | (* 9)) 70 | 81)) 71 | (is (null (some-> 3 72 | (+ 5) 73 | (member '(2 5 8 9)) 74 | third 75 | (* 9)))) 76 | (is (null (some-> '(:a 1) 77 | (getf :b) 78 | 1+)))) 79 | 80 | (deftest test-some->> () 81 | (is (= (some->> '((:a . 3) (:b . 5)) 82 | (assoc :a) 83 | cdr 84 | 1+) 85 | 4)) 86 | (is (null (some->> '((:a . 3) (:b . 5)) 87 | (assoc :c) 88 | cdr 89 | 1+)))) 90 | 91 | (deftest test-cond-> () 92 | (is (equal (labels ((strcat (&rest things) 93 | (with-output-to-string (s) 94 | (dolist (thing things) 95 | (when thing (princ thing s))))) 96 | (say (n) 97 | (cond-> nil 98 | ((zerop (mod n 3)) (strcat "Fizz")) 99 | ((zerop (mod n 5)) (strcat "Buzz")) 100 | (t (or (strcat n)))))) 101 | (mapcar #'say '(9 10 11 12 13 14 15))) 102 | '("Fizz" "Buzz" "11" "Fizz" "13" "14" "FizzBuzz")))) 103 | 104 | (deftest test-cond->> () 105 | (is (equal (flet ((say (n) 106 | (cond->> nil 107 | ((zerop (mod n 3)) (cons "Fizz")) 108 | ((zerop (mod n 5)) (cons "Buzz")) 109 | (t (->* (or (list (princ-to-string n)))) 110 | reverse 111 | (apply #'concatenate 'string))))) 112 | (mapcar #'say '(9 10 11 12 13 14 15))) 113 | '("Fizz" "Buzz" "11" "Fizz" "13" "14" "FizzBuzz")))) 114 | --------------------------------------------------------------------------------