Skip to content

Instantly share code, notes, and snippets.

@Mavlarn
Created January 20, 2014 12:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Mavlarn/8518989 to your computer and use it in GitHub Desktop.
Save Mavlarn/8518989 to your computer and use it in GitHub Desktop.
An Introduction to ClojureScript for Light Table users Got from https://github.com/swannodette/lt-cljs-tutorial.
;; +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
;; An Introduction to ClojureScript for Light Table users
;; +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
;; Basics
;; ============================================================================
;; To begin open the command pane (type Control-SPACE), Add Connection, select
;; Light Table UI. Once connected you can evaluate all the forms in this file
;; by placing the cursor after the form and typing Command-ENTER.
;; IMPORTANT: You must evaluate the very first form, the namespace definition.
;; Declaring a namespaces
;; ----------------------------------------------------------------------------
;; ClojureScript supports modularity via namespaces. They allow you to group
;; logical definitions together.
(ns lt-cljs-tutorial
(:require [clojure.string :as string]))
;; :require is how you can import functionality from a different namespace into
;; the current one. Here we are requiring `clojure.string` and giving it an
;; alias. We could write the following:
(clojure.string/blank? "")
;; But that's really verbose compared to:
(string/blank? "")
;; Comments
;; ----------------------------------------------------------------------------
;; There are three ways to create comments in ClojureScript. The first way is
;; by preceding a line with a semi-colon, just like the lines you are reading
;; now.
;; The second way is by preceding a form with `#_`. This causes ClojureScript
;; to skip the evaluation of only the form immediately following, without
;; affecting the evaluation of the surrounding forms.
;; Try to reveal the secret message below:
(str "The secret word is " #_(string/reverse "tpircSerujolC"))
;; Finally, you can also create a comment using the `comment` macro. One common
;; technique is to use the `comment` macro to include code to be evaluated in a
;; REPL, but which you do not normally want to be included in the compiled
;; source.
;; For example, try placing your cursor after the last `)` below and type
;; Command-ENTER:
(comment
(string/upper-case "This is only a test...")
)
;; The `comment` macro makes the whole form return `nil`. Now go back and
;; highlight just the middle line, then type Command-ENTER. In this way
;; you can include code samples or quick tests in-line with the rest of
;; your code.
;; Definitions
;; ----------------------------------------------------------------------------
;; Once you have a namespace you can start creating top level definitions in
;; that namespace.
;; You can define a top level with `def`.
(def x 1)
x
;; You can also refer to top level definitions by fully qualifying them.
lt-cljs-tutorial/x
;; This means top levels can never be shadowed by locals and function
;; parameters.
(let [x 2]
lt-cljs-tutorial/x)
;; One way to define a function is like this.
(def y (fn [] 1))
(y)
;; Defining functions in ClojureScript is common enough that `defn` sugar is
;; provided and idiomatic.
(defn z [] 1)
(z)
;; Literal data types
;; ----------------------------------------------------------------------------
;; ClojureScript comes out of the box with the usual useful data literals.
;; Booleans
(def a-boolean true)
;; Strings
(def a-string "Hello!")
;; Numbers
(def a-number 1)
;; Function literals
;; ----------------------------------------------------------------------------
;; ClojureScript also supports a short hand function literal which is useful
;; You can use the % and %N placeholders to represent function arguments.
;; You should not abuse the function literal notation as it degrades readability
;; outside of simple cases. It is nice for simple functional cases such as
;; the following.
(map (fn [n] (* n 2)) [1 2 3 4 5])
(map #(* % 2) [1 2 3 4 5])
;; JavaScript data type literals
;; ----------------------------------------------------------------------------
;; You can construct a JavaScript array with the `array` function.
(def an-array (array 1 2 3))
;; But ClojureScript also supports JavaScript data literals via the `#js`
;; reader literal.
(def another-array #js [1 2 3])
;; Similarly you can create simple JavaScript objects with `js-obj`.
(def an-object (js-obj "foo" "bar"))
;; But again you can save a few characters with `#js`.
(def another-object #js {"foo" "bar"})
;; It's important to note that `#js` is shallow, the contents of `#js` will be
;; ClojureScript data unless preceded by `#js`.
;; This is a mutable JavaScript object with an immutable ClojureScript vector
;; inside.
(def shallow #js {"foo" [1 2 3]})
;; Constructing a type
;; ----------------------------------------------------------------------------
;; Of course some JavaScript data types you will want to create with
;; constructor.
;; (js/Date.) is equivalent to new Date().
(def a-date (js/Date.))
;; Note the above returns an `#inst` data literal.
(def another-date #inst "2014-01-15")
;; Handy
;; NOTE: js/Foo is how you refer to global JavaScript entities of any kind.
js/requestAnimationFrame
;; If you're curious about other JavaScrip interop jump to the bottom of this
;; tutorial.
;; ClojureScript data types
;; ============================================================================
;; Unless there is a good reason you should generally write your ClojureScript
;; programs with ClojureScript data types. They have many advantages over
;; JavaScript data types - they present a uniform API and they are immutable.
;; Vectors
;; ----------------------------------------------------------------------------
;; Instead of arrays ClojureScript programmers use persistent vectors, they are
;; like arrays - they support efficient random access, efficient update
;; and efficient addition to the end.
(def a-vector [1 2 3 4 5])
;; We can get the length of a vector in constant time via `count`.
(count a-vector)
;; We can add an element to the end.
(conj a-vector 6)
;; Note this does not mutate the array! `a-vector` will be left unchanged.
a-vector
;; Hallelujah!
;; We can access any element in a vector with `nth`. The following will
;; return the second element.
(nth ["foo" "bar" "baz"] 1)
;; Surprisingly vectors can be treated as functions. This is actually
;; a very useful property for associative data structures to have as
;; we'll see below with sets.
(["foo" "bar" "baz"] 1)
;; Maps
;; ----------------------------------------------------------------------------
;; Along with vectors maps are the most common data type in ClojureScript.
;; Map usage is analogous to the usage of Object in JavaScript, but
;; ClojureScript maps are immutable and considerably more flexible.
;; Let's define a simple map. Note `:foo` is a ClojureScript keyword.
;; ClojureScript programmers generally do not use strings for keys.
(def a-map {:foo "bar" :baz "woz"})
;; We can get the number of key-value pairs in constant time.
(count a-map)
;; We can access a particular value for a key with `get`.
(get a-map :foo)
;; We can add a new key-value pair with `assoc`.
(assoc a-map :noz "goz")
;; Again a-map is unchanged!
a-map
;; We can remove a key value pair with `dissoc`.
(dissoc a-map :foo)
;; Again a-map is unchanged!
a-map
;; Like vectors maps can act like functions.
(a-map :foo)
;; However ClojureScript keywords themselves can act like functions and the
;; following is more idiomatic.
(:foo a-map)
;; We can check if a map contains a key, with `contains?`.
(contains? a-map :foo)
;; We can get all the keys in a map with `keys`.
(keys a-map)
;; And all of the values with `vals`.
(vals a-map)
;; There are many cool ways to create maps.
(zipmap [:foo :bar :baz] [1 2 3])
(hash-map :foo 1 :bar 2 :baz 3)
(apply hash-map [:foo 1 :bar 2 :baz 3])
(into {} [[:foo 1] [:bar 2] [:baz 3]])
;; Unlike JavaScript objects ClojureScript maps support complex keys.
(def complex-map {[1 2] :one-two [3 4] :three-four})
(get complex-map [3 4])
;; Keyword digression
;; ----------------------------------------------------------------------------
;; Let's take a moment to digress about keywords as they are so ubiquitous
;; in ClojureScript code.
(identity :foo)
;; If you add an additional preceding colon you'll get namespaced keyword.
(identity ::foo)
;; What good is this for? It allows you to put data into collections without
;; fear of namespace clashes without the tedium of manual namespacing them
;; in your source.
(identity {:user/foo ::foo})
;; Namespaced keywords are essential to Light Table's modularity.
;; Sets
;; ----------------------------------------------------------------------------
;; ClojureScript also supports sets.
(def a-set #{:cat :dog :bird})
;; `:cat` is already in `a-set`, so it will be unchanged.
(conj a-set :cat)
;; But `:zebra` isn't.
(conj a-set :zebra)
;; If you haven't guessed already, `conj` is a "polymorphic" function that adds
;; an item to a collection. This is some of the uniformity we alluded to
;; earlier.
;; `contains?` works on sets just like they do on maps.
(contains? a-set :cat)
;; Like vectors and maps, sets can also act as functions. If the argument
;; exists in the set it will be returned, otherwise the set will return nil.
(#{:cat :dog :bird} :cat)
;; This is powerful when combined with conditionals.
(defn check [x]
(if (#{:cat :dog :bird} x)
:valid
:invalid))
(check :cat)
(check :zebra)
;; Lists
;; ----------------------------------------------------------------------------
;; A less common ClojureScript data structure is lists. This may be surprising
;; as ClojureScript is a Lisp, but maps, vectors and sets are the goto for most
;; applications. Still lists are sometimes useful.
(def a-list '(:foo :bar :baz))
;; Equality
;; ============================================================================
;; ClojureScript has a much simpler notion of equality than what is present
;; in JavaScript. In ClojureScript equality is always deep equality.
(= {:foo "bar" :baz "woz"} {:foo "bar" :baz "woz"})
;; Maps are not ordered.
(= {:foo "bar" :baz "woz"} {:baz "woz" :foo "bar"})
;; For sequential collections equality just works.
(= [1 2 3] '(1 2 3))
;; It is possible to check whether two things are represented by the same thing
;; in memory with `identical?`.
(def my-vec [1 2 3])
(def your-vec [1 2 3])
(identical? my-vec your-vec)
;; Control
;; ============================================================================
;; In order to write useful programs we need to be able to be able to express
;; control. ClojureScript provides the usual control constructs, however
;; truth-y and false-y values are not the same as in JavaScript so it's worth
;; reviewing.
;; if
;; ----------------------------------------------------------------------------
;; 0 is not a false-y value.
(if 0
"Zero is not false-y"
"Yuck")
;; Nor is the empty string.
(if ""
"An empty string is not false-y"
"Yuck")
;; The only false-y values in ClojureScript are `nil` and `false`. `undefined`
;; is not really a valid ClojureScript value and is generally coerced to `nil`.
;; cond
;; ----------------------------------------------------------------------------
;; Nesting if tends to be noisy and hard to read so ClojureScript provides
;; a `cond` macro to deal with this.
(cond
nil "Not going to return this"
false "Nope not going to return this either"
:else "Default case")
;; loop/recur
;; ----------------------------------------------------------------------------
;; The most primitive looping construct in ClojureScript is loop/recur. Most
;; of the iteration constructs are defined in terms of it. Using loop/recur is
;; usually considered bad style if a reasonable functional solution via
;; map/filter/reduce or a list comprehension is possible.
(loop [i 0 ret []]
(if (< i 10)
(recur (inc i) (conj ret i))
ret))
;; Moar functions
;; ============================================================================
;; Functions are the essence of any significant ClojureScript program so
;; we will dive into features that are unique to ClojureScript functions that
;; might be unfamiliar.
;; Here is a simple function that takes two arguments and adds them.
(defn foo1 [a b]
(+ a b))
(foo1 1 2)
;; Functions can have multiple arities.
(defn foo2
([a b] (+ a b))
([a b c] (* a b c)))
(foo2 3 4)
(foo2 3 4 5)
;; Multiple arities can be used to supply default values.
(defn defaults
([x] (defaults x :default))
([x y] [x y]))
(defaults :explicit)
(defaults :explicit1 :explicit2)
;; Functions support rest arguments.
(defn foo3 [a b & d]
[a b d])
(foo3 1 2)
(foo3 1 2 3 4)
;; You can apply functions.
(apply + [1 2 3 4 5])
;; multimethods
;; ----------------------------------------------------------------------------
;; Often when you need some polymorphism and performance isn't an issue
;; multimethods will suffice. Multimethods are functions that allow open
;; extension, but instead of limiting dispatch to type, dispatch is controlled
;; by whatever value the dispatch fn originally supplied to defmulti returns.
;; Here is a function that takes a list. It dispatches on the first element
;; of the list!
(defmulti parse (fn [[f & r :as form]] f))
(defmethod parse 'if
[form] {:op :if})
(defmethod parse 'let
[form] {:op :let})
(parse '(if a b c))
(parse '(let [x 1] x))
;; Scoping
;; ============================================================================
;; Unlike JavaScript there is no hoisting in ClojureScript. ClojureScript
;; has lexical scoping. In ClojureScript functions parameters and let binding
;; locals are not mutable!
(def some-x 1)
(let [some-x 2]
some-x)
some-x
;; Unlike JavaScript loop locals are not mutable! In JavaScript you would see
;; a list of ten 9's. In ClojureScript we see the expected numbers from 0 to 9.
(let [fns (loop [i 0 ret []]
(if (< i 10)
(recur (inc i) (conj ret (fn [] i)))
ret))]
(map #(%) fns))
;; Destructuring
;; ============================================================================
;; In any serious ClojureScript program there will be significant amounts of
;; data manipulation. Again we will see that ClojureScript's uniformity
;; pays off.
;; In ClojureScript anywhere bindings are allowed like `let` or function
;; parameters destructuring is allowed. This is similar to the destructuring
;; proposed for ES6, but the system provided in ClojureScript benefits from
;; all the collections supporting uniform access.
;; Sequence destructuring
;; ----------------------------------------------------------------------------
;; Destructuring sequential types is particularly useful.
(let [[f & r] '(1 2 3)]
f)
(let [[f & r] '(1 2 3)]
r)
(let [[r g b] [255 255 150]]
g)
;; _ is just a convention for saying that you are not interested at the
;; item in the corresponding position. it has no other special meaning.
;; Here we're only interested at the third local variable named `b`.
(let [[_ _ b] [255 255 150]]
b)
;; destructuring function arguments works just as well. Here we are
;; only intersted at the second argument `g`.
(defn green [[_ g _]] g)
(green [255 255 150])
;; Map destructuring
;; ----------------------------------------------------------------------------
;; Map destructuring is also useful. Here we destructure the value for the
;; `:foo` key and bind it to a local `f`, and the value for `:baz` key
;; and bind it to a local `b`.
(let [{f :foo b :baz} {:foo "bar" :baz "woz"}]
[f b])
;; If we don't want to rename we can just use `:keys`.
(let [{:keys [first last]} {:first "Bob" :last "Smith"}]
[first last])
;; the above map destructuring form is very useful when you need to
;; define a function with optional, non positional and defaulted
;; arguments.
(defn magic [& {:keys [k g h]
:or {k 1
g 2
h 3}}]
(hash-map :k k
:g g
:h h))
(magic)
(magic :k 10)
(magic :g 100)
(magic :h 1000)
(magic :k 10 :g 100 :h 1000)
(magic :h 1000 :k 10 :g 100)
;; Sequences
;; ============================================================================
;; We said that ClojureScript data structures are preferred as they
;; provide a uniform interface. All ClojureScript collections satisfy
;; the ISeqable protocol, which means iteration is uniform
;; (i.e. polymorphic) for all collection types.
;; Map / Filter / Reduce
;; ----------------------------------------------------------------------------
;; ClojureScript supports the same bells and whistles out of the box you may
;; be familiar with from other functional programming languages or JavaScript
;; libraries such as Underscore.js
(map inc [0 1 2 3 4 5 6 7 8 9])
(filter even? (range 10))
(remove odd? (range 10))
;; ClojureScript's map and filter operations are lazy. You can stack up
;; operations without getting too concerned about multiple traversals.
(map #(* % %) (filter even? (range 20)))
(reduce + (range 100))
;; List comprehensions
;; ----------------------------------------------------------------------------
;; ClojureScript supports list comprehensions you might know from various
;; languages. List comprehensions are sometimes more natural / readable
;; than a chain of map and filter operations.
(for [x (range 1 10)
y (range 1 10)]
[x y])
(for [x (range 1 10)
y (range 1 10)
:when (and (zero? (rem x y))
(even? (quot x y)))]
[x y])
(for [x (range 1 10)
y (range 1 10)
:let [prod (* x y)]]
[x y prod])
;; Seqable collections
;; ----------------------------------------------------------------------------
;; Most ClojureScript collections can be coerced into sequences.
(seq {:foo "bar" :baz "woz"})
(seq #{:cat :dog :bird})
(seq [1 2 3 4 5])
(seq '(1 2 3 4 5))
;; Many ClojureScript functions will call `seq` on their arguments in order to
;; provide the expected behavior. The following demonstrates that you can
;; uniformly iterate over all the ClojureScript collections!
(first {:foo "bar" :baz "woz"})
(rest {:foo "bar" :baz "woz"})
(first #{:cat :dog :bird})
(rest #{:cat :dog :bird})
(first [1 2 3 4 5])
(rest [1 2 3 4 5])
(first '(1 2 3 4 5))
(rest '(1 2 3 4 5))
;; Metadata
;; ============================================================================
;; All of the ClojureScript standard collections support metadata. Metadata
;; is a useful way to annotate data without affecting equality. The
;; ClojureScript compiler uses this language feature to great effect.
;; You can add meta data to a ClojureScript collection with `with-meta`. The
;; metadata must be a map.
(def plain-data [0 1 2 3 4 5 6 7 8 9])
(def decorated-data (with-meta plain-data {:url "http://lighttable.com"}))
;; Metadata has no effect on equality.
(= plain-data decorated-data)
;; You can access metadata with `meta`.
(meta decorated-data)
;; Error Handling
;; ============================================================================
;; Error handling in ClojureScript is relatively straightforward and more or
;; less similar to what is offered in JavaScript.
;; You can construct an error like this.
(js/Error. "Oops")
;; You can throw an error like this.
(throw (js/Error. "Oops"))
;; You can catch an error like this.
(try
(throw (js/Error. "Oops"))
(catch js/Error e
e))
;; JavaScript unfortunately allows you to throw anything. You can handle
;; this in ClojureScript with the following.
(try
(throw (js/Error. "Oops"))
(catch :default e
e))
;; Mutation
;; ============================================================================
;; Atoms
;; ----------------------------------------------------------------------------
;; A little bit of mutability goes a long way. ClojureScript does not offer
;; any traditional mutable data structures, however it does support identities
;; that can evolve over time via atom.
(def x (atom 1))
;; You can dereference the value of an atom with `@`.
@x
;; This is equivalent to calling `deref`.
(deref x)
;; If you want to change the value of an atom you can use `reset!` which returns
;; the new value. It's idiomatic to add the bang char `!` at the end of function
;; names mutating objects.
(reset! x 2)
x
@x
;; swap!
;; ------------------------------------------------------------------------------
;; if you want to change the value of an atom on the basis of its current value
;; you can use `swap!`. In its simplest form `swap!` accept as first argument
;; the atom itself and as a second argument an updating function of one argument
;; which will be instantiated with the current value of the atom. `swap!` returns
;; the new value of the atom.
(swap! x inc)
x
@x
;; if your updating function needs extra arguments to calculate the new value, you
;; have to pass them as extra arguments to `swap!` after the updating function
;; itself.
(swap! x (fn [old extra-arg]
(+ old extra-arg)) 39)
x
@x
;; as usual when anonymous functions are simple enough it's idiomatic the usage
;; of the condensed form.
(swap! x #(- %1 %2) 42)
x
@x
;; Note that the updating function has to be free of side-effects because a
;; waiting writer could call it more than once in a spin loop.
;; set!
;; ----------------------------------------------------------------------------
;; Sometimes you need to mutate existing JavaScript objects. For this you
;; have `set!`.
(def c (.createElement js/document "canvas"))
(def ctxt (.getContext c "2d"))
;; We can use property access with `set!` to change the fill color of a
;; a canvas rendering context.
(set! (.-fillColor ctxt) "#ffffff")
;; The ClojureScript Standard Library
;; ============================================================================
;; The ClojureScript standard library largely mirrors the Clojure standard
;; library with the exception of functionality that assumes a multithreaded
;; environment, first class namespaces, and Java numerics.
;; Here are some highlights and patterns that newcomers to ClojureScript might
;; find useful. Remember you can type Control-Shift-D at anytime to bring up
;; the documentation panel to see what any of these function do.
(apply str (interpose ", " ["Bob" "Mary" "George"]))
((juxt :first :last) {:first "Bob" :last "Smith"})
(def people [{:first "John" :last "McCarthy"}
{:first "Alan" :last "Kay"}
{:first "Joseph" :last "Licklider"}
{:first "Robin" :last "Milner"}])
(map :first people)
(take 5 (repeat "red"))
(take 5 (repeat "blue"))
(take 5 (interleave (repeat "red") (repeat "blue")))
(take 10 (cycle ["red" "white" "blue"]))
(partition 2 [:a 1 :b 2 :c 3 :d 4 :e 5])
(partition 2 1 [:a 1 :b 2 :c 3 :d 4 :e 5])
(take-while #(< % 5) (range 10))
(drop-while #(< % 5) (range 10))
;; Protocols
;; ============================================================================
;; The ClojureScript language is constructed on a rich set of protocols. The
;; same uniformity provided by ClojureScript collections can be extended to
;; your own types or even types that you do not control!
;; A lot of the uniform power we saw early was because the ClojureScript
;; collections are implemented in terms of protocols. Collections can be
;; coerced in sequences because they implement ISeqable. You can use `get`
;; on vectors and maps because they implement ILookup.
(get {:foo "bar"} :foo)
(get [:cat :bird :dog] 1)
;; Map destructing actually desugars into `get` calls. That means if you extend
;; your type to ILookup it will also support map destructuring!
;; extend-type
;; ----------------------------------------------------------------------------
;; ClojureScript supports custom extension to types that avoid many of the
;; pitfalls that you encounter in other languages. For example imagine we have
;; some awesome polymorphic functionality in mind.
(defprotocol MyProtocol (awesome [this]))
;; Now imagine we want JavaScript strings to participate. We can do this
;; simply.
(extend-type string
MyProtocol
(awesome [_] "Totally awesome!"))
(awesome "Is this awesome?")
;; extend-protocol
;; ----------------------------------------------------------------------------
;; Sometimes you want to extend several types to a protocol at once.
(extend-protocol MyProtocol
js/Date
(awesome [_] "Having an awesome time!")
number
(awesome [_] "I'm an awesome number!"))
(awesome #inst "2014")
(awesome 5)
;; reify
;; ----------------------------------------------------------------------------
;; Sometimes it's useful to make an anonymous type which implements some
;; various protocols.
;; For example say we want JavaScript object to support ILookup. Now we don't
;; want to blindly `extend-type object`, that would pollute the behavior of plain
;; JavaScript objects for everyone.
;; Instead we can provide a helper function that takes an object and returns
;; something that provides this functionality.
(defn ->lookup [obj]
(reify
ILookup
(-lookup [this k]
(-lookup this k nil))
(-lookup [this k not-found]
(let [k (name k)]
(if (.hasOwnProperty obj k)
(aget obj k)
not-found)))))
;; We can then selectively make JavaScript objects work with `get`.
(get (->lookup #js {"foo" "bar"}) :foo)
;; But this also means we get destructuring on JavaScript objects.
(def some-object #js {"foo" "bar" "baz" "woz"})
(let [{:keys [foo baz]} (->lookup some-object)]
[foo baz])
;; specify
;; ----------------------------------------------------------------------------
;; Light Table ships with a older version of ClojureScript and does not yet
;; support specify
;; Macros
;; ============================================================================
;; Types & Records
;; ============================================================================
;; deftype
;; ----------------------------------------------------------------------------
;; Sometimes a map will simply not suffice, in these cases you will want to
;; make your own custom type.
(deftype Foo [a b])
;; It's idiomatic to use CamelCase to name a deftype. You can instantiate a
;; deftype instance using the same constructor pattern we've already discussed.
(Foo. 1 2)
;; You can access properties of a deftype instance using property access
;; syntax.
(.-a (Foo. 1 2))
;; You can implement protocol methods on a deftype. Note that the first
;; argument to any deftype or defrecord method is the instance itself.
;; The dash in `-count` has no special meaning. It's just a convention for
;; the core ClojureScript protocols. You need not adopt it.
(deftype Foo [a b]
ICounted
(-count [this] 2))
(count (Foo. 1 2))
;; Sometimes it's useful to implement methods directly on the deftype.
(deftype Foo [a b]
Object
(toString [this] (str a ", " b)))
(.toString (Foo. 1 2))
;; deftype field are immutable unless specified. The following will not compile.
;; (To prove it to yourself, highlight and evaluate the `deftype` form below.)
(comment
(deftype Foo [a ^:mutable b]
Object
(setA [this val] (set! a val)))
)
;; The following will compile.
(deftype Foo [a ^:mutable b]
Object
(setB [this val] (set! b val)))
;; defrecord
;; ----------------------------------------------------------------------------
;; deftype doesn't provide much out of the box. Often what you want to do is
;; have a domain object that acts more or less like a map. This is what
;; defrecord is for.
;; Like for deftype, it's idiomatic to use CamelCase to name a defrecord.
(defrecord Person [first last])
;; You can construct an instance in the usual way.
(Person. "Bob" "Smith")
;; Or you can use the provided constructors.
(->Person "Bob" "Smith")
(map->Person {:first "Bob" :last "Smith"})
;; It's considered idiomatic and even recommended to define a factory function
;; which returns the created instance of a defrecord/deftype. It's idiomatic to use
;; dash-case for factories names.
(defn person [first last]
(->Person first last))
;; records work like maps
(seq (person "Bob" "Smith"))
(:first (person "Bob" "Smith"))
(keys (person "Bob" "Smith"))
(vals (person "Bob" "Smith"))
;; both deftype and defrecord are open to dynamic extensions (i.e. open class)
(keys (assoc (person "Bob" "Smith") :age 18))
;; Records & Protocols
;; ----------------------------------------------------------------------------
;; You can extend a defrecord to satisfy a protocol as you do with deftype.
(extend-type Person
MyProtocol
(awesome [this]
(str (:last this) ", " (:first this))))
(awesome (person "Bob" "Smith"))
(satisfies? MyProtocol (person "Bob" "Smith"))
;; Or you can extend a protocol on a defrecord.
(extend-protocol MyProtocol
Person
(awesome [this]
(str (:last this) ", " (:first this))))
(awesome (person "Bob" "Smith"))
(satisfies? MyProtocol (person "Bob" "Smith"))
;; If you need a more sophisticated form of polymorphism consider multimethods.
;; If you mix types/records with protocols you are modeling your problem with an
;; object oriented approach, which is sometimes useful.
;; Note ClojureScript does not offer a direct form of inheritance. Instead,
;; reuse/extension by composition is encouraged. It's best to avoid
;; deftype/defrecord and model your problem with plain maps. You can easily
;; switch to records later down the line.
(defrecord Contact [person email])
;; Even if it's not required, remember to define a factory function to create
;; instances of the new Contact record type by internally calling the factory
;; function for the Person record type.
(defn contact [first last email]
(->Contact (person first last) email))
(contact "Bob" "Smith" "bob.smith@acme.com")
;; And extend the protocol on defrecord as well.
(extend-protocol MyProtocol
Contact
(awesome [this]
(str (awesome (:person this)) ", " (:email this))))
(awesome (contact "Bob" "Smith" "bob.smith@acme.com"))
;; To change the value of a nested key you use 'assoc-in', like with maps.
(assoc-in (contact "Bob" "Smith" "bob.smith@acme.com")
[:person :first] "Robert")
;; It you need to use the previous value of a nested field for calculating the
;; new one, you can use 'update-in', like with maps.
(update-in (contact "Bob" "Smith" "bob.smith@acme.com")
[:person :first] #(string/replace %1 #"Bob" %2) "Robert")
;; As said, the main difference with the majority of OO languages is that your
;; instances of deftypes/defrecords are immutable.
(def bob (contact "Bob" "Smith" "bob.smith@acme.com"))
(update-in bob [:person :first] #(string/replace %1 #"Bob" %2) "Robert")
(get-in bob [:person :first])
;; JavaScript Interop
;; ============================================================================
;; Property Access
;; ----------------------------------------------------------------------------
(def a-date (js/Date.))
;; You can access properties with the `.-` property access syntax.
(.-getSeconds a-date)
;; Method Calls
;; ----------------------------------------------------------------------------
;; Methods can be invoke with the `.` syntax.
(.getSeconds a-date)
;; The above desugars into the following.
(. a-date (getSeconds))
;; For example you can write a `console.log` call like so.
(. js/console (log "Interop!"))
;; Primitive Array Operations
;; ----------------------------------------------------------------------------
;; When writing performance sensitive code sometimes dealing with mutable
;; arrays is unavoidable. ClojureScript provides a variety of functions for
;; creating and manipulating JavaScript arrays.
;; You can make an array of specific size with `make-array`
(make-array 32)
;; You can access an element of a array with `aget`.
(aget #js ["one" "two" "three"] 1)
;; You can access nested arrays with `aget`.
(aget #js [#js ["one" "two" "three"]] 0 1)
;; You can set the contents of an array with aset.
(def yucky-stuff #js [1 2 3])
(aset yucky-stuff 1 4)
yucky-stuff
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment