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