arrows
2018-10-18
Implements -> and ->> from Clojure, as well as several expansions on the idea.
Upstream URL
Author
License
Arrows
Implements threading macros, inspired by Clojure (both core and the swiss-arrows library).
This is an ASDF system providing the package arrows
. Its home is at
https://gitlab.com/Harleqin/arrows, with a mirror at
https://github.com/Harleqin/arrows.
Overview
You get:
- the basic “thrushing” arrows
->
and->>
, - “diamond” arrows
-<>
and-<>>
, - binding arrow
as->
, - “maybe” arrows
some->
andsome->>
, - conditional arrows
cond->
andcond->>
, and - “double arrow cancellers”
->*
andas->*
.
As far as I see, ->*
and as->*
are new. Their purpose is to be nested in
other threading forms to temporarily supplant their behaviour (see “Nesting”
below).
Other arrow libraries
arrow-macros
cl-arrows
is superseded byarrows
.
Notable differences to Clojure and swiss-arrows
-
Cond->
andcond->>
use one additional paren nesting for the clauses, so that each clause can contain multiple forms to thread/execute. -
-<>
and-<>>
do not support literals to insert the<>
placeholder. The placeholder really only works at the outermost level of the threaded forms. The reason for this is mostly that Common Lisp does not have so many literal syntax elements (by default) where it would make sense to do this kind of insertion. If you do need anything fancy, useas->
oras->*
for a real lexical binding.
Notable differences to arrow-macros
-
Cond->
andcond->>
use one additional paren nesting for the clauses, so that each clause can contain multiple forms to thread/execute. -
-<>
and-<>>
do not use a code walker to find out whether a placeholder is present in the next threaded form. The placeholder only works at the outermost level of the threaded forms. This reduces the dependencies ofarrows
(there are none at present). Instead, the recommendation is to use binding arrowsas->
oras->*
, possibly nested (see below). -
There is no
some-<>
norsome-<>>
yet. Instead, you can use nestedas->
oras->*
forms (see below).
Nesting
One useful idiom is to nest these arrows. The basic example is to use ->>
inside ->
:
(-> deeply-nested-plist
(getf :foo)
(getf :bar)
(->> (mapcar #'reverse)))
This inspired the discovery of ->*
, which enables the inverse nesting:
(->> deeply-nested-alist
(assoc :foo)
cdr
(assoc :bar)
cdr
(->* (mod 3))
(expt 2))
Generally useful for overriding defaults are as->
and as->*
:
(-> 3
(as-> $
(< x $ y))
not)
(some->> 15
(as->* $
(progn
(format t debug-formatter $)
$))
(/ 75))
However, don't overdo it! This quickly leads to an unreadable mess. You may
well be better off with a few explicit let
bindings.
Documentation
->
initial-form &rest forms => results
[macro] Inserts INITIAL-FORM as first argument into the first of FORMS, the result into the next, etc., before evaluation. FORMS are treated as list designators.
->>
initial-form &rest forms => results
[macro] Like ->
, but the forms are inserted as last argument instead of
first.
->*
&rest forms => results
[macro] Like ->
, but the last form is used as initial form, then the
remainung forms as in ->
. This is intended for inversing the default in a
->>
form.
-<>
initial-form &rest forms => results
[macro] Like ->
, but if a form in FORMS has one or more symbols named <>
as top-level element, each such symbol is substituted by the primary result of
the form accumulated so far, instead of it being inserted as first argument.
Also known as diamond wand.
-<>>
initial-form &rest forms => results
[macro] Like -<>
, but if a form in FORMS has no symbols named <>
as
top-level element, insertion is done like in ->>
. Also known as diamond
spear.
as->
initial-form var &rest forms => results
[macro] Binds INITIAL-FORM to VAR, then successively each of FORMS to VAR, finally returns the last value of VAR.
as->*
var &rest forms => results
[macro] Shorthand for the combination of ->*
and as->
: the last form is
used for initial binding, then the remaining forms used as in as->
. This is
intended for overriding the default in a ->>
form.
some->
initial-form &rest forms => results
[macro] Like ->
, but short-circuits to nil as soon as either INITIAL-FORM or
any of FORMS return nil. This is like all these forms are lifted to the maybe
monad.
some->>
initial-form &rest forms => results
[macro] Like some->
, but with insertion behaviour as in ->>
.
cond->
initial-form &rest clauses => results
[macro] CLAUSES is a list of clauses similar to COND clauses, each clause
comprising first a test form, then a body of further forms. Cond->
evaluates
INITIAL-FORM to a value, then for each clause whose test evaluates to true,
pipes (as in ->
) the value through each form in the body of the clause. Note
that unlike in COND, there is no short-circuiting: each clause gets tested
regardless of the outcome of the clauses before.
cond->>
initial-form &rest clauses => results
[macro] Like cond->
, but with insertion behaviour as in ->>
.
Examples
(-> 3
/) ; insert into designated list (/)
=> 1/3
(-> 3
(expt 2)) ; insert as first argument
=> 9
(->> 3
(expt 2)) ; insert as last argument
=> 8
(-<>> (list 1 2 3)
(remove-if #'oddp <> :count 1 :from-end t) ; substitute <>
(reduce #'+) ; insert last
/) ; list designator
=> 1/3
(let ((x 3))
(-<> (incf x) ; (let ((r (incf x)))
(+ <> <>))) ; (+ r r))
=> 8
(->> 3
(/ 12) ; (/ 12 3) => 4
(->* (/ 2))) ; (/ 4 2) => 2
=> 2
(flet ((say (n)
(cond->> nil
((zerop (mod n 3)) (cons "Fizz"))
((zerop (mod n 5)) (cons "Buzz"))
(t (->* (or (list (princ-to-string n))))
reverse
(apply #'concatenate 'string)))))
(mapcar #'say '(9 10 11 12 13 14 15)))
=> ("Fizz" "Buzz" "11" "Fizz" "13" "14" "FizzBuzz")