Working with ClojureScript

Introduction

Clojure Robust, practical, and fast programming language with a set of useful features that together form a simple, coherent, and powerful tool.

Leiningen for automating Clojure projects without setting your hair on fire

Boot Build tooling for Clojure

Keechma Frontend micro framework for ClojureScript and Reagent

Reagent Minimalistic React for ClojureScript

Lightmod An all-in-one tool for full stack Clojure

Installation

Installation on MacOS

$ brew cask install adoptopenjdk/openjdk/adoptopenjdk8

Use OpenJDK Version

JAVA_HOME=/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home
PATH=$JAVA_HOME/bin:$PATH
$ which java
/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/bin/java
$ java -version
openjdk version "1.8.0_232"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_232-b09)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.232-b09, mixed mode)
$ brew install clojure

Now install a build System. We will use Leiningen

$ brew install leiningen

Installation in Vagrant Box

Install Vagrant

Create new Vagrant environment

$ vagrant init

Edit Vagantfile

Vagrant.configure("2") do |config|
  config.vm.box = "hashicorp/precise64"

  config.vm.box_check_update = false

  # config.vm.network "forwarded_port", guest: 80, host: 8080
  # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
  # config.vm.network "private_network", ip: "192.168.33.10"

  config.vm.network "public_network"
  config.vm.synced_folder ".", "/HERE"

end

Quickstart

Inspired from https://clojurescript.org/guides/quick-start

Create directory structure

hello-world        # Our project folder
├─ src             # The CLJS source code for our project
│  └─ hello_world  # Our hello_world namespace folder
│     └─ core.cljs # Our main file
└─ deps.edn        # MacOS/Linux only: Listing our dependencies

If you are on macOS or Linux your deps.edn file should contain the following:

{:deps {org.clojure/clojurescript {:mvn/version "1.10.520"}}}

In your favorite text editor edit the src/hello_world/core.cljs to look like the following:

(ns hello-world.core)

(println "Hello world!")

Now that we have a simple program, let’s build and run some ClojureScript:

$ clj --main cljs.main --compile hello-world.core --repl

Your default web browser will open to a page that looks like the following:

$ make run
Hello world!
ClojureScript 1.10.520
cljs.user=> (inc 1)
2
cljs.user=> (map inc [1 2 3])
(2 3 4)
cljs.user=> (.getElementById js/document "app")
#object[HTMLDivElement [object HTMLDivElement]]
cljs.user=>

Build production version

clj --main cljs.main --optimizations advanced -c hello-world.core
$ clj --main cljs.main --serve

Working with Metadata

First Steps with Leiningen

Create new demo app

$ lein new todoapp
$ cd todoapp
$ lein new figwheel hello-world -- --om       ;; for an om based project
$ lein new figwheel hello-world -- --reagent  ;; for a reagent based project 

Modify project.cli

(defproject todoapp "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :dependencies [[org.clojure/clojure "1.10.0"]
  ]

  :repl-options {:init-ns todoapp.core}
)

Edit src/todoapp/core.clj

(ns todoapp.core)

(println "Hello World.")
(println "This is my first Clojure App!")

Run App

$ lein deps
$ lein check
Compiling namespace todoapp.core
Hello World!

Keep your lein project up-to-date

Use the lei plugin lein ancient to show dependencies, with are outdated.

Install the plugin by adding it to the :plugins vector in project.cli

    :plugins   [[lein-figwheel "0.5.19"]
                [lein-cljsbuild "1.1.7" :exclusions [[org.clojure/clojure]]]
                [lein-ancient "0.6.15"]]

Then, check for new dependencies

$ lein ancient
[binaryage/devtools "0.9.11"] is available but we use "0.9.10"

Do the upgrade

$ lein ancient upgrade
[binaryage/devtools "0.9.11"] is available but we use "0.9.10"

running test task ["test"] ...
Retrieving binaryage/devtools/0.9.11/devtools-0.9.11.pom from clojars
Retrieving binaryage/devtools/0.9.11/devtools-0.9.11.jar from clojars

lein test user

Ran 0 tests containing 0 assertions.
0 failures, 0 errors.
1/1 artifacts were upgraded.

Next steps

More to read

ClojureScript Unraveled (2nd edition)

ClojureScript Core.Async and ToDos

CheatSheet

Transforming Data with ClojureScript

Tutorial

https://lambdaisland.com/blog/2018-04-26-d3-clojurescript (GitHub)

https://clojurecademy.com/

Blogs

REPL adventures – Mostly about Clojure and ClojureScript

Samples and Code Snippets

Values

Logical Values

In Clojure, only nil and false represent the values of logical falsity in conditional tests – anything else is logical truth.

(str "if     nil   = " (if nil	 		"TRUE" "FALSE"))
(str "if-not nil   = " (if-not nil 		"TRUE" "FALSE"))

(str "if true      = " (if true 		"TRUE" "FALSE"))
(str "if false     = " (if false 		"TRUE" "FALSE"))
(str "if-not false = " (if-not false 	"TRUE" "FALSE"))

(str "if []        = " (if [] 			"TRUE" "FALSE"))
(str "if [0]       = " (if [0] 			"TRUE" "FALSE"))
(str "if 0         = " (if 0 			"TRUE" "FALSE"))
(str "if 1         = " (if 1 			"TRUE" "FALSE"))

=> if     nil   = FALSE
=> if-not nil   = TRUE
=> if true      = TRUE
=> if false     = FALSE
=> if-not false = TRUE
=> if []        = TRUE
=> if [0]       = TRUE
=> if 0         = TRUE
=> if 1         = TRUE

Loops

recur

Clojure only has one non-stack-consuming looping construct: recur.Either a function or a loop can be used as the recursion point.

Either way, recur rebinds the bindings of the recursion point to the values it is passed.Recur must be called from the tail-position, and calling it elsewhere will result in an error.

(loop [param  5
       result []]
    (if (> param 0)
      (recur (dec param) (conj result (+ 2 param)))
      result)
)
=> [7 6 5 4 3]

Using Java Functions

(.toUpperCase "hello world"))
=> HELLO WORLD

Data Structures

Sets

(set '(:a :a :b :c :c :c :c :d :d))
=> #{:c :b :d :a}

(clojure.set/union #{:a :b :c} #{:b :c :d})
=> #{:c :b :d :a}

When operating on a set, the conj function returns a new set with one or more keys added

(conj #{1 4 3} 2)
=> #{1 4 3 2}

Set A is a subset of set B, or equivalently B is a superset of A, if A is contained inside B. A and B may coincide.

(clojure.set/superset? 	#{1 2} 		#{2})
=> true

(clojure.set/superset? 	#{2}	 	#{1 2})
=> false

(clojure.set/superset? 	#{1 2}	 	#{1 2})
=> true

(clojure.set/superset? 	#{3 4}	 	#{1 2})
=> false

(clojure.set/subset? 	#{1} 		#{1 2})
=> true

(clojure.set/subset? 	#{1 2} 		#{1 2})
=> true

Lists

'(:a :b :c)
=> (:a :b :c)
(conj '(2 3 4) 1)
=> (1 2 3 4)

(conj '(3 4) 1 2)))
=> (2 1 3 4)

(conj '(3 4) '(1 2))
=> ((1 2) 3 4)

Vectors

(list :a :b :c)
=> (:a :b :c)

(vec '(:a :b :c)) 
=> [:a :b :c]

(vector :a :b :c))
=> [:a :b :c]
(conj [1 2 3] 4)
=> [1 2 3 4]

(conj [1 2] 3 4)
=> [1 2 3 4]

Map

Maps store key-value pairs. Both maps and keywords can be used as lookup functions. Commas can be used to make maps more readable, but they are not required.

((hash-map :a 10, :b 20, :c 30) :b)
=> 20

(:b {:a 10, :b 20, :c 30})
=> 20

When operating on a map, the conj function returns a new map with one or more key-value pairs added.

{:a 1, :b 2, :c 3}
=> {:a 1, :b 2, :c 3}

(conj {:a 1} {:b 2} [:c 3])
=> {:a 1, :b 2, :c 3}

When retrieving values from a map, you can specify default values in case the key is not found:

(:foo {:bar 0 :baz 1} 2)

Write an anonymous function which, given a key and map, returns true if the map contains an entry with that key and its value is nil.

(defn f [k m]
  	(println "k=" k)
  	(println "m=" m)
  	(= nil (get m k))
 )
(true?  (f :a {:a nil :b 2}))
k= :a
m= {:a nil, :b 2}
=> true

Create a map from vector of keys and values

(zipmap [:year :month :day] (clojure.string/split "2013-02-18" #"-"))
;= {:day "18", :month "02", :year "2013"}
(def data
  [{:id 1 :request-count 10 :val1 1 :val2 2}
   {:id 2 :request-count 15}]  )

(defn make-id-req-map [map-seq]
  (vec (for [curr-map map-seq]
         (let [{:keys [id request-count]} curr-map]
           {id request-count}))))
(make-id-req-map data)
=> [{1 10} {2 15}]

Sequencing

(first '(3 2 1))
=> 3

(second [2 3 4])
=> 3

(last (list 1 2 3))
=> 3

The rest function will return all the items of a sequence except the first.

(rest [10 20 30 40])
=> (20 30 40)

Functions

Clojure has many different ways to create functions.

((fn add-five [x] (+ x 5)) 3)
=> 8

((fn add-five [x] (+ x 5)) 3)
=> 8

((fn [x] (+ x 5)) 3)
=> 8

(#(+ % 5) 3)
=> 8

((partial + 5) 3)
=> 8

Write an anonymous function which doubles a number.

((fn [val] (+ val val)) 7)
=> 14
((fn [name] (str "Hello, " name "!")) "World")
=> Hello, World!

map

(map #(+ % 5) '(1 2 3))
=> (6 7 8)

filter

(filter #(> % 5) '(3 4 5 6 7))
=> (6 7)

reduce

(defn add [sum val]
    (+ sum val))

(reduce add [1 2 3 4 5])
=> 15

some

The some function takes a predicate function and a collection. It returns the first logical true value of (predicate x) where x is an item in the collection.

(some #{2 7 6} [5 6 7 8])
=> 6

(some #{2 7 8} [5 6 7 8 2])
=> 7

Recursive Functions

A recursive function is a function which calls itself. This is one of the fundamental techniques used in functional programming.

((fn foo [x] 
   (when (> x 0) 
     (conj (foo (dec x)) x))
) 5)
=> (5 4 3 2 1)

Variables

let

Clojure lets you give local names to values using the special let form.

(let [x 5] (+ 2 x))
=> 7

(let [x 3, y 10] (- y x))
=> 7

(let [x 21] (let [y 3] (/ x y)))
=> 7

Let bindings and function parameter lists support destructuring.

(let [[a b c d e] [0 1 2 3 4]]
     [c e]
) 
=> [2 4]

RegEx Patterns

Regex patterns are supported with a special reader macro.

(apply str (re-seq #"[A-Z]+" "bA1B3Ce "))
=> ABC

Macros

->

The -> macro threads an expression x through a variable number of forms.First, x is inserted as the second item in the first form, making a list of it if it is not a list already.

Then the first form is inserted as the second item in the second form, making a list of that form if necessary.This process continues for all the forms. Using -> can sometimes make your code more readable.

(sort (rest (reverse [2 5 4 1 3 6])))
=> (1 2 3 4 5)

(-> [2 5 4 1 3 6] (reverse) (rest) (sort))
=> (1 2 3 4 5)

->>

The ->> macro threads an expression x through a variable number of forms.First, x is inserted as the last item in the first form, making a list of it if it is not a list already.

Then the first form is inserted as the last item in the second form, making a list of that form if necessary.This process continues for all the forms. Using ->> can sometimes make your code more readable.

(map inc (take 3 (drop 2 [2 5 4 1 3 6])))
=> (5 2 4)

(->> [2 5 4 1 3 6] (drop 2) (take 3) (map inc) (map +))
=> (5 2 4)

for

(for [x (range 40)
     :when (= 1 (rem x 4))] 
     x
)
; => (1 5 9 13 17 21 25 29 33 37)