đź“–ClojureScript Unraveled (2nd edition)
- authors
- Antukh, Andrey and G'omez, Alejandro
- year
- 2019
- url
- https://funcool.github.io/clojurescript-unraveled/
Short syntax for anonymous functions
#(...)
syntax form%1
,%2
numbered argumentsnumber can be omitted if only one argument:
%
variadic form:
%&
recur https://funcool.github.io/clojurescript-unraveled/#looping-with-looprecur
recur
can be used insideloop
or functions to call it (recurse) with new bindings(loop [x 0] (println "Looping with " x) (if (= x 2) (println "Done looping!") (recur (inc x)))) ;; Looping with 0 ;; Looping with 1 ;; Looping with 2 ;; Done looping! ;; => nil (defn recursive-function [x] (println "Looping with" x) (if (= x 2) (println "Done looping!") (recur (inc x)))) (recursive-function 0) ;; Looping with 0 ;; Looping with 1 ;; Looping with 2 ;; Done looping! ;; => nil
for
sequence comprehension (https://funcool.github.io/clojurescript-unraveled/#for-sequence-comprehensions):let
new binding:while
for stopping when condition is false:when
for filteringdoseq
is analogous tofor
, but discards result values
identical?
predicate tests if two values are actually the same objectClojureScript has queues. They use the
#queue []
syntax.Destructuring (https://funcool.github.io/clojurescript-unraveled/#destructuring-section)
:or
can be used to give default values(let [{name :name :or {name "Anonymous"}} {:language "ClojureScript"}] name) ;; => "Anonymous"
:as
to bind original value:keys
to bind keywords to equivalent names:(let [{keys [name blah]} {:name "hello" :blah "world"}] name)
keyword syntax works, too (useful for namespaced keywords):
(let [{:keys [::name]} {::name "hello"}] name)
:strs
and:syms
works as:keys
but for strings and symbols.
Threading
->
thread-first (thread as first argument)->>
thread-last (thread last argument)as->
thread-as (allows specifying argument position)(as-> numbers $ (map inc $) (filter odd? $) (first $) (hash-map :result $ :id 1)) ;; => {:result 3 :id 1}
some->=/=some->>
thread-some (short-circuit if function returnsnil
)cond->=/=cond->>
thread-cond (conditionally skip steps)(defn describe-number [n] (cond-> [] (odd? n) (conj "odd") (even? n) (conj "even") (zero? n) (conj "zero") (pos? n) (conj "positive"))) (describe-number 3) ;; => ["odd" "positive"] (describe-number 4) ;; => ["even" "positive"]
Reader conditionals
only work in
.cljc
files#?
standard(defn parse-int [v] #?(:clj (Integer/parseInt v) :cljs (js/parseInt v)))
#?@
splicing(defn make-list [] (list #?@(:clj [5 6 7 8] :cljs [1 2 3 4]))) ;; On ClojureScript (make-list) ;; => (1 2 3 4)
Polymorphism
Protocols
(defprotocol IInvertible "This is a protocol for data types that are 'invertible'" (invert [this] "Invert the given item."))
ClojureScript’s protocols are similar to Haskell’s type-classes.
extend-type
(extend-type TypeA ProtocolA (function-from-protocol-a [this] ;; implementation here ) ProtocolB (function-from-protocol-b-1 [this parameter1] ;; implementation here ) (function-from-protocol-b-2 [this parameter1 parameter2] ;; implementation here ))
extend-protocol
(extend-protocol ProtocolA TypeA (function-from-protocol-a [this] ;; implementation here ) TypeB (function-from-protocol-a [this] ;; implementation here ))
satisfies?
can be used to check if value satisfies a protocol:(satisfies? IFn #{1})
Multi-methods
A more powerful version of protocols
defmulti
+defmethod
example
(defmulti say-hello "A polymorphic function that return a greetings message depending on the language key with default lang as =:en=" (fn [param] (:locale param)) :default :en) (defmethod say-hello :en [person] (str "Hello " (:name person "Anonymous"))) (defmethod say-hello :es [person] (str "Hola " (:name person "AnĂłnimo")))
Hierarchies
hierarchies allow defining relationship (not only between types)
derive
to define(derive ::circle ::shape) (derive ::box ::shape)
ancestors
,descendants
to explore hierarchy(ancestors ::box) ;; => #[[{:cljs.user/shape} (descendants]] ::shape) ;; => #{:cljs.user/circle :cljs.user/box}
isa?
to check(isa? ::box ::shape) ;; => true (isa? ::rect ::shape) ;; => false
make-hierarchy
to define local hierarchihes(def h (-> (make-hierarchy) (derive :box :shape) (derive :circle :shape)))
:hierarchy #'h
can be used indefmulti
to specify hierarchy to use
Types
deftype
(defines class)(deftype User [firstname lastname]) (def person (User. "Triss" "Merigold")) (.-firstname person) ;; => "Triss"
defrecord
(slightly higher-level) — records are mapsreify
allows ad-hoc protocol implementations(defn user [firstname lastname] (reify IUser (full-name [_] (str firstname " " lastname)))) (def yen (user "Yennefer" "of Vengerberg")) (full-name yen) ;; => "Yennefer of Vengerberg"
specify=/=specify!
allow adding protocols to existing JS objects
JavaScript interop
js/
namespace to access global functions(new js/RegExp "^foo$")
or(js/RegExp. "^foo$")
(recommended) — calling constructors(.test (js/RegExp. "^foo$") "foobar")
to call methods(js/Math.sqrt 2)
— shortcut(.-PI js/Math)
— property access (shortcut:js/Math.PI
)(js-obj "country" "UA")
— create JS object#js {:country "UA"}
— reader macro to create JS objectset!
to set JavaScript object propertiesclj->js=/=js->clj
— recursive versions of conversion macrosinto-array
for converting arraysmake-array
to pre-allocate array lengthaset=/=aget
to set/get array value
State management
Atoms
object containing value
supports watchers
supports validations
atom
to construct atomderef
or@
to dereferenceswap!
to alter value with a functionreset!
replace valueadd-watch=/=remove-watch
for watchers (identify watcher by specified keyword)
Volatiles
Mutable like atoms, but do not provide watchers and validations, thus are more performant
volatile!
to constructderef=/=@
vswap!
,vreset!
Transducers
reducing function__ is a function that takes accumulator, a value, and returns a new accumulator
transducer__ is a function that transforms one reducing function into another reducing function
eduction__ can be used to remember a series of transducers (returns an iterator) and will iterate execute them every time, a new element is requested. Applying a composition of transducers would return a seq (and remember values); applying eduction, does not remember sequence.
transient__ structures allow creating mutable data structures (temporarily)
transient
to createpersistent!
to freezetransient version is invalidated once converted into persistent one
Metadata
meta
to access metadatawith-meta
or^{:metavar "val"}
syntax to attach metadata^:keyword
is a shorthand for^{:keyword true}
^symbol
is a shorthand for^{:tag symbol}
(e.g.,^boolean
)vary-meta
to update metadata with functionalter-meta!
orreset-meta!
to update/set metadata in-place
Core protocols
IFn
(-invoke
) callable valuesIPrintWithWriter
(-pr-writer
) customize printingISeq
(-first
,-rest
) implement seq-likeINext
(-next
) fornext
ISequable
(-seq
)ICollection
(-conj
)IEmptyableCollection
(-empty
)ICounted
(-count
)IIndexed
(-nth
)ILookup
(-lookup
) forget
IAssociative
(-contains-key?
,-assoc
)IMap
(-dissoc
)IMapEntry
(-key
,-val
)IEquiv
(-equiv
) for=
IComparable
(-compare
)IMeta
(-meta
),IWithMeta
(-with-meta
)IEncodeJS
(-clj->js
),IEncodeClojure
(-js->clj
)IReduce
(-reduce
)IKVReduce
(-reduce-kv
)IPending
(forrealized?
)IDeref
(forderef
)IReset
forreset!
ISwap
forswap!
IWatchable
(-add-watch
,-remove-watch
,-notify-watches
)IVolatile
forvreset!
IEditableCollection
fortransient
,ITransientCollection
forpersistent!
ITransientAssociative
forassoc!
ITransientVector
forassoc-n!
ITransientSet
fordisj!
CSP for Communicating Sequential Processes
built on channels abstraction
chan
to create channelput!
,take!
,close!
put!
andtake!
take callbacksput!
returnstrue
orfalse
— whether channel is opentake!
is called withnil
when channel closespassing
nil
is not allowedcurrently, there can be only up to 1024 pending puts or takes
chan
has a buffer argument (either number or a buffer). When place is available in the buffer,put!
succeeds (callback called) immediatelychannels are unbuffered by default
dropping-buffer
like fixed buffer, but drops extra values (instead of enqueuing them)sliding-buffer
discard oldest values in favor of the new oneschannels accept transducer as second argument
chan
’s third argument allows handling exceptionsoffer!
andpoll!
are synchronous versions ofput!
andtake!
offer!
andpoll!
cannot distinguish closed channels
go
macro for running asynchronous “thread.”>!
and<!
for putting and taking from channel(require '[cljs.core.async :refer [chan <! >!]]) (require-macros '[cljs.core.async.macros :refer [go]]) (enable-console-print!) (def ch (chan)) (go (println [:a] "Gonna take from channel") (println [:a] "Got" (<! ch))) (go (println [:b] "Gonna put on channel") (>! ch 42) (println [:b] "Just put 42")) ;; [:a] Gonna take from channel ;; [:b] Gonna put on channel ;; [:b] Just put 42 ;; [:a] Got 42
timeout
returns a channel that is going to be closed after the given amount of the milliseconds (use withtake!
)alts!
allow takes and puts to race (it returns whichever was faster)if
:priority true
is passed,alts!
will try channels in orderif
:default value
is passed,value
is returned when neither channel is ready
pipe
to pass values from first channel to the secondpipeline-async
calls an async function for all values and puts result in the result channel (first argument controls how many functions it can run in parallel)result values seem to be in the same order as corresponding input values (even if async functions finish in different time)
pipeline
runs a transducer for each value in input channelsplit
splits channel into two (where predicate is true, and where it false)reduce
reduce channel into a single-value channelonto-chan
pass collection into a channelto-chan
converts a collection and returns a channelmerge
merge multiple channels into onemult
to make channel multicasttap
,untap
,untap-all
to connect to multicast channelpub
,sub
for publish-subcsribeMixing channels
mix
,admix
,unmix
,unmix-all
toggle
with:mute
,:pause
,:solo
A muted input channel means that, while still taking values from it, they won’t be forwarded to the output channel. Thus, while a channel is muted, all the values put in it will be discarded.
A paused input channel means that no values will be taken from it. This means that values put in the channel won’t be forwarded to the output channel nor discarded.
When soloing one or more channels the output channel will only receive the values put in soloed channels. By default, non-soloed channels are muted but we can use
solo-mode
to decide between muting or pausing non-soloed channels.