đź“–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,%2numbered arguments- number can be omitted if only one argument:
% - variadic form:
%&
- recur https://funcool.github.io/clojurescript-unraveled/#looping-with-looprecur
recurcan be used insideloopor 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
forsequence comprehension (https://funcool.github.io/clojurescript-unraveled/#for-sequence-comprehensions):letnew binding:whilefor stopping when condition is false:whenfor filteringdoseqis analogous tofor, but discards result values
identical?predicate tests if two values are actually the same object- ClojureScript has queues. They use the
#queue []syntax. - Destructuring (https://funcool.github.io/clojurescript-unraveled/#destructuring-section)
:orcan be used to give default values(let [{name :name :or {name "Anonymous"}} {:language "ClojureScript"}] name) ;; => "Anonymous"
:asto bind original value:keysto 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)
- keyword syntax works, too (useful for namespaced keywords):
:strsand:symsworks as:keysbut 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
.cljcfiles #?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)
- only work in
- 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)
deriveto define(derive ::circle ::shape) (derive ::box ::shape)
ancestors,descendantsto 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-hierarchyto define local hierarchihes(def h (-> (make-hierarchy) (derive :box :shape) (derive :circle :shape)))
:hierarchy #'hcan be used indefmultito 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 mapsreifyallows 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
- Protocols
- 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-arrayfor converting arraysmake-arrayto pre-allocate array lengthaset=/=agetto set/get array value
- State management
- Atoms
- object containing value
- supports watchers
- supports validations
atomto construct atomderefor@to dereferenceswap!to alter value with a functionreset!replace valueadd-watch=/=remove-watchfor 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!
- Atoms
- 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)
transientto createpersistent!to freeze- transient version is invalidated once converted into persistent one
- Metadata
metato access metadatawith-metaor^{:metavar "val"}syntax to attach metadata^:keywordis a shorthand for^{:keyword true}^symbolis a shorthand for^{:tag symbol}(e.g.,^boolean)vary-metato 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) fornextISequable(-seq)ICollection(-conj)IEmptyableCollection(-empty)ICounted(-count)IIndexed(-nth)ILookup(-lookup) forgetIAssociative(-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)IResetforreset!ISwapforswap!IWatchable(-add-watch,-remove-watch,-notify-watches)IVolatileforvreset!IEditableCollectionfortransient,ITransientCollectionforpersistent!ITransientAssociativeforassoc!ITransientVectorforassoc-n!ITransientSetfordisj!
- CSP for Communicating Sequential Processes
- built on channels abstraction
chanto create channelput!,take!,close!put!andtake!take callbacksput!returnstrueorfalse— whether channel is opentake!is called withnilwhen channel closes- passing
nilis not allowed - currently, there can be only up to 1024 pending puts or takes
chanhas a buffer argument (either number or a buffer). When place is available in the buffer,put!succeeds (callback called) immediately- channels are unbuffered by default
dropping-bufferlike fixed buffer, but drops extra values (instead of enqueuing them)sliding-bufferdiscard oldest values in favor of the new ones- channels accept transducer as second argument
chan’s third argument allows handling exceptionsoffer!andpoll!are synchronous versions ofput!andtake!offer!andpoll!cannot distinguish closed channels
gomacro 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
timeoutreturns 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 trueis passed,alts!will try channels in order - if
:default valueis passed,valueis returned when neither channel is ready
- if
pipeto pass values from first channel to the secondpipeline-asynccalls 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)
pipelineruns a transducer for each value in input channelsplitsplits channel into two (where predicate is true, and where it false)reducereduce channel into a single-value channelonto-chanpass collection into a channelto-chanconverts a collection and returns a channelmergemerge multiple channels into onemultto make channel multicasttap,untap,untap-allto connect to multicast channelpub,subfor publish-subcsribe- Mixing channels
mix,admix,unmix,unmix-alltogglewith: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-modeto decide between muting or pausing non-soloed channels.