đź“–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 arguments

    • number can be omitted if only one argument: %

    • variadic form: %&

  • recur https://funcool.github.io/clojurescript-unraveled/#looping-with-looprecur

    • recur can be used inside loop 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 filtering

    • doseq is analogous to for, 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)

    • :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 returns nil)

    • 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 in defmulti 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 maps

      • reify 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 object

    • set! to set JavaScript object properties

    • clj->js=/=js->clj — recursive versions of conversion macros

    • into-array for converting arrays

    • make-array to pre-allocate array length

    • aset=/=aget to set/get array value

  • State management

    • Atoms

      • object containing value

      • supports watchers

      • supports validations

      • atom to construct atom

      • deref or @ to dereference

      • swap! to alter value with a function

      • reset! replace value

      • add-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 construct

      • deref=/=@

      • 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 create

    • persistent! to freeze

      • transient version is invalidated once converted into persistent one

  • Metadata

    • meta to access metadata

    • with-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 function

    • alter-meta! or reset-meta! to update/set metadata in-place

  • Core protocols

    • IFn (-invoke) callable values

    • IPrintWithWriter (-pr-writer) customize printing

    • ISeq (-first, -rest) implement seq-like

    • INext (-next) for next

    • ISequable (-seq)

    • ICollection (-conj)

    • IEmptyableCollection (-empty)

    • ICounted (-count)

    • IIndexed (-nth)

    • ILookup (-lookup) for get

    • 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 (for realized?)

    • IDeref (for deref)

    • IReset for reset!

    • ISwap for swap!

    • IWatchable (-add-watch, -remove-watch, -notify-watches)

    • IVolatile for vreset!

    • IEditableCollection for transient, ITransientCollection for persistent!

    • ITransientAssociative for assoc!

    • ITransientVector for assoc-n!

    • ITransientSet for disj!

  • CSP for Communicating Sequential Processes

    • built on channels abstraction

    • chan to create channel

    • put!, take!, close!

    • put! and take! take callbacks

    • put! returns true or false — whether channel is open

    • take! is called with nil when channel closes

    • passing nil is not allowed

    • currently, 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) immediately

    • channels 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 ones

    • channels accept transducer as second argument

    • chan’s third argument allows handling exceptions

    • offer! and poll! are synchronous versions of put! and take!

      • offer! and poll! 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 with take!)

    • alts! allow takes and puts to race (it returns whichever was faster)

      • if :priority true is passed, alts! will try channels in order

      • if :default value is passed, value is returned when neither channel is ready

    • pipe to pass values from first channel to the second

    • pipeline-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 channel

    • split splits channel into two (where predicate is true, and where it false)

    • reduce reduce channel into a single-value channel

    • onto-chan pass collection into a channel

    • to-chan converts a collection and returns a channel

    • merge merge multiple channels into one

    • mult to make channel multicast

    • tap, untap, untap-all to connect to multicast channel

    • pub, sub for publish-subcsribe

    • Mixing 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.