Extensions of Clojure's standard -> macro used to build code blocks that operate on a threaded topic.
SynThread is available from clojars. Add it to your project dependencies:
[synthread "1.3.0"]
See unit tests for specific examples of each macro.
Some general guidelines:
- Require SynThread with the alias
->
like this in Clojure:
(ns your.ns.here
(:require [lonocloud.synthread :as ->]))
or like this if you use Clojurescript:
(ns your.ns.here
(:require [lonocloud.synthread :as -> :include-macros true]))
- Always start a threaded block with Clojure's standard
->
macro or->/do
(see next point). This clearly identifies the topic to be threaded.
;; good
(-> {:first "John" :last "Smith"}
(->/update :last first)) ;; replace last name with last initial
;; bad
(->/update {:first "John" :last "Smith"}
:last first) ;; replace last name with last initial
- Don't change the type (or shape) of the topic as it flows through. In our
experience, the threading macros are used to either dig into a deep data
structure massaging the topic the deeper it goes or are used to build a
result by describing a pipeline of operations. The main difference between
digging and building is that the type and shape of the threaded topic is
changing or is constant respectively. We use synthread macros for
building as a general rule. The
->/do
macro will behave like->
but will also check that the topic is maintained through a threaded block.
;; good
(-> {:a 1 :b 2}
(->/update :a inc)
(->/in [:b]
(->/for [n (range 3)]
(inc n))))
;; returns {:a 2 :b 5}
;; bad
(-> {:a 1 :b 2}
(->/update :a inc)
:b ;; type changed from map to number
inc)
;; returns 3
- Use
->/as
to put the threaded value (or "topic") into a named local variable. This is useful when you need to call a function or macro that needs access to the topic in some parameter slot other than the first:
(-> {:a 1 :b 2}
(->/update :a inc)
(->/as topic ;; label topic
(->/when (> (:b topic) 10)
(assoc :large-b true))))
Standard destructuring is supported by ->/as
:
(-> {:a 1 :b 2}
(->/update :a inc)
(->/as {:keys [b]} ;; destructure topic
(->/when (> b 10)
(assoc :large-b true))))
Additionally, a special destructuring form is supported allowing the use of functions. Passing a threaded form will implicity insert the topic at the front and use the last argument as the binding label. For example:
(-> {:a 1 :b 2}
(->/as (-> vals (->/apply max) max-val) ;; use functions on topic
(->/when (> max-val 10)
(assoc :large-val true))))
- Clojure's
do
anddoto
macros are useful in these threading contexts, so don't be afraid to use them.do
lets you stop threading, and yet pass a result to the next threaded step:
(-> {:a 1 :b 2}
(->/update :a inc)
(->/when we-should-reset?
(do {:a 0 :b 0})) ;; see also ->/reset function
(->/update :b inc))
This can be particularly useful in conjunction with ->/as
:
(-> {10 :orig, 20 :orig}
(->/as topic
(do
(reduce #(assoc %1 %2 :default) topic (range 5)))))
On the other hand, doto
is nice when you do not want to pass a
result to the next step:
(-> {:a 1 :b 2}
(doto prn)
(->/update :a inc))
The debugging prn
above works, but the patten rapidly becomes
awkward if you want to provide a label to the prn. That would actually
require a combination of ->/as
to label it and do
to prevent the
topic from being printed before the label because of threading. This
is exactly the purpose of ->/aside
:
(-> {:a 1 :b 2}
(->/aside topic ;; note the body is entirely unthreaded
(prn :topic topic)
(println "Note: b is currently" (:b topic))) ;; return value is ignored
(->/update :a inc))
- In addition to the threading macros, two helper functions are also
available:
->/reset
and->/apply
. Use->/reset
to set the value of the threaded topic (similar to Clojure'sreset!
) and use->/apply
to call a function with the threaded topic as its first argument (similar to Clojure'sapply
).
;; example of ->/reset
(-> false
(->/reset true))
;; returns true
;; example of ->/apply
(-> [0]
(->/apply conj [1 2 3]) ;; => (conj [0] 1 2 3)
(->/apply [conj 4 5 6])) ;; also works!
;; returns [0 1 2 3 4 5 6]
- Some of the macros are marked EXPERIMENTAL to reflect the fact that they have seen little or no use in our live code.
This is not the first library to have sailed these waters. Some that we were aware of during the design of SynThread include:
- Clojure 1.5:
cond->
,as->
,when->
- Pallet thread-expr:
arg->
,for->
,when->
,let->
, etc. - Sierra's Syntactic Pipelines:
defpipe
- Prismatic plumbing:
fn->
- Levy's swiss-arrows:
-<>
,-?<>
,-<><:p
, etc. core.incubator
:-?>
,-?>>
,.?.
, etc.
http://www.infoq.com/presentations/Macros-Monads
© 2013-2014 LonoCloud. © 2015 Hoang Minh Thang.
All rights reserved. The use and distribution terms for this software are covered by the Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) which can be found in the file epl-v10.html at the root of this distribution. By using this software in any fashion, you are agreeing to be bound by the terms of this license. You must not remove this notice, or any other, from this software.