(println hello-world)
+diff --git a/clojurescript-unraveled.pdf b/clojurescript-unraveled.pdf
new file mode 100644
index 0000000..2e72181
Binary files /dev/null and b/clojurescript-unraveled.pdf differ
diff --git a/clojurescript-unraveled.xml b/clojurescript-unraveled.xml
new file mode 100644
index 0000000..ee8655c
--- /dev/null
+++ b/clojurescript-unraveled.xml
@@ -0,0 +1,6892 @@
+
+
+
+
+
+
+
+
+
case
->
(thread-first macro)->>
(thread-last macro)as->
(thread-as macro)some->
, some->>
(thread-some macros)cond->
, cond->>
(thread-cond macros)#?
)#?@
)In the end, keywords are a special case of Symbols which will be explained later.
+Just as keywords are usually used to define keys on a map, namespaced +keywords allow defining attributes dedicated to a specific domain +without colliding with other keywords that may have the same name.
+Don’t worry if you don’t understand right away; symbols are used in almost all of -our examples, which will give you the opportunity to learn more as we go on.
+The symbols themselves are not usually used as data but are used by +the language to resolve function calls. In other words, let’s use this +example:
+(println hello-world)
+In this case, the println
symbol is telling the compiler to look in
+its resolution table to see if it has a bound implementation of a
+function that prints data to standard output.
And by wrapping the symbol in parentheses, you are telling the
+compiler that what you want is to execute that function. All elements
+other than the println
symbol, which is in the first place, are
+considered parameters.
The same for booleans:
+true
+false
+The really interesting part of the booleans is the concepts of logical
+booleans. Is a concept in which we assign a boolean meaning to an expression
+(if it is falsy
or truthy
even if it is not of boolean type).
This is the aspect where each language has its own semantics (mostly wrongly in
+my opinion). The majority of languages consider empty collections, the integer
+0, and other things like this to be false. In ClojureScript, unlike in other
+languages, only two values are considered as logical false: nil
and
+false
. Everything else is treated as logical true
.
(boolean nil) ;; => false
+(boolean 0) ;; => true
+(boolean []) ;; => true
+(boolean :false) ;; => true
+Another big step in explaining a language is to explain its collections and collection abstractions. ClojureScript is not an exception to this rule.
@@ -650,12 +775,13 @@As you can see, all list examples are prefixed with the '
char. This is because
-lists in Lisp-like languages are often used to express things like function or macro
-calls. In that case, the first item should be a symbol that will evaluate to
-something callable, and the rest of the list elements will be function
-arguments. However, in the preceding examples, we don’t want the first item as a
-symbol; we just want a list of items.
As you can see, all list examples are prefixed with the '
char. This is
+because lists in Lisp-like languages are often used to express things like
+function or macro calls (just as we have seen before). In that case, the first
+item should be a symbol that will evaluate to something callable, and the rest
+of the list elements will be function arguments. However, in the preceding
+examples, we don’t want the first item as a symbol; we just want a list of
+items.
The following example shows the difference between a list without and with the @@ -842,7 +968,8 @@
In the example above, inc
is a function and is part of the ClojureScript
-runtime, and 1
is the first argument for the inc
function.
1
is the first argument for the inc
+function.
The way to denote a variadic function is using the &
symbol prefix on its
arguments vector.
The variadic function can also be combined with multiple arities:
+(defn max
+ ([a] a)
+ ([a b] (special-max a b))
+ ([a b c] (special-max a b c))
+ ([a b c d] (special-max a b c d))
+ ([a b c d e] (special-max a b c d e))
+ ([a b c d e f] (special-max a b c d e f))
+ ([a b c d e f & other]
+ (reduce max (special-max a b c d e f) other)))
+You may wonder why I really need this syntax and define all that arities. Well, +sometimes it is for documentation, since it prefers to name the first required +parameters and leave the rest in a sequence. It is also a performance issue, +since calling an arity with fixed variables will always be faster than calling a +variadic function.
+
+ Note
+ |
+
+special-max is an invented function, just for make an example.
+ |
+
In the function parameters you can apply +destructuring, which is explained in detail later.
+Parameters by name in ClojureScript is nothing more than a special case of +destructuring. See the destructuring section to better +understand all the concepts.
+There are times when we want to pass optional parameters to a function, +something like options and it is better to give those options a name to be +concise:
+(defn my-prefix
+ [value & {:keys [default] :or {default "foo-"}}]
+ (str default value))
+When you call this function, you just can omit the options, and the default will +be used:
+(my-prefix "hello")
+;; => "foo-hello"
+Then, provide a different prefix using named parameters:
+(my-prefix "hello" :default "bar-")
+;; => "bar-hello"
+The good part is that the name parameters do not impose a fixed calling +convention. Nothing really prevents you from calling that function with a map as +a second parameter with all the options you want to pass.
+(my-prefix "hello" {:default "bar-"})
+;; => "bar-hello"
+ClojureScript provides a shorter syntax for defining anonymous functions using the
#()
reader macro (usually leads to one-liners). Reader macros are "special"
@@ -1043,13 +1259,18 @@
This way of defining functions is generally useful for defining single line +functions.
+ClojureScript has a very different approach to flow control than languages like -JavaScript, C, etc.
+ClojureScript has a very different approach to flow control than languages +like JavaScript, C, etc. In an expression based language as it is CLJS, flow +control structures also returns values.
if
The :else
keyword has no special meaning in cond
. It just a truthy value
+and in reallity can be any keyword os anything that evaluates to a logical
+true.
Also, cond
has another form, called condp
, that works very similarly to the
simple cond
but looks cleaner when the condition (also called a predicate) is the
same for all conditions:
This is the aspect where each language has its own semantics (mostly wrongly). The
-majority of languages consider empty collections, the integer 0, and other things
-like this to be false. In ClojureScript, unlike in other languages, only two
-values are considered as false: nil
and false
. Everything else is treated as
-logical true
.
In JavaScript, braces {
and }
delimit a block of code that “belongs
+together”. Blocks in ClojureScript are created using the do
expression and are
+usually used for side effects, like printing something to the console or writing a
+log in a logger.
Jointly with the ability to implement the callable protocol (the IFn
, explained
-more in detail later), data structures like sets can be used just as predicates,
-without need of additional wrapping them in a function:
(def valid? #{1 2 3})
-
-(filter valid? (range 1 10))
-;; => (1 2 3)
-A side effect is something that is not necessary for the return value.
This works because a set returns either the value itself for all contained elements
-or nil
:
The do
expression accepts as its parameter an arbitrary number of other
+expressions, but it returns the return value only from the last one:
(valid? 1)
-;; => 1
+(do
+ (println "hello world")
+ (println "hola mundo")
+ (* 3 5) ;; this value will not be returned; it is thrown away
+ (+ 1 2))
-(valid? 4)
-;; => nil
+;; hello world
+;; hola mundo
+;; => 3
ClojureScript does not have the concept of variables as in ALGOL-like languages, but it does have locals. Locals, as per usual, are immutable, and if you try to @@ -1226,48 +1445,18 @@
In the preceding example, the symbol x
is bound to the value (inc 1)
, which
-comes out to 2, and the symbol y
is bound to the sum of x
and 1, which comes out
-to 3. Given those bindings, the expressions (println "Simple message from the body
-of a let")
and (* x y)
are evaluated.
In JavaScript, braces {
and }
delimit a block of code that “belongs
-together”. Blocks in ClojureScript are created using the do
expression and are
-usually used for side effects, like printing something to the console or writing a
-log in a logger.
A side effect is something that is not necessary for the return value.
-The do
expression accepts as its parameter an arbitrary number of other
-expressions, but it returns the return value only from the last one:
(do
- (println "hello world")
- (println "hola mundo")
- (* 3 5) ;; this value will not be returned; it is thrown away
- (+ 1 2))
-
-;; hello world
-;; hola mundo
-;; => 3
-In the preceding example, the symbol x
is bound to the value of executing the
+(inc 1)
expression, which comes out to 2, and the symbol y
is bound to the
+sum of x
and 1, which comes out to 3. Given those bindings, the expressions
+(println "Simple message from the body of a let")
and (* x y)
are evaluated.
The body of the let
expression, explained in the previous section, is very similar
-to the do
expression in that it allows multiple expressions. In fact, the let
-has an implicit do
.
The body of the let
expression has an implicit do
, so you can put there
+multiple expressions, where the last one will be used as return value.
The functional approach of ClojureScript means that it does not have standard,
well-known, statement-based loops such as for
in JavaScript. The loops in
@@ -1634,9 +1823,9 @@
We mentioned before that ClojureScript collections are persistent and immutable, but we didn’t explain what that meant.
@@ -1708,7 +1897,7 @@One of the central ClojureScript abstractions is the sequence which can be thought of as a list and can be derived from any of the collection types. It is @@ -1748,7 +1937,7 @@
We can also get an empty variant of a given collection with the empty
function:
When we have used map
and filter
in the previous examples, the result when
+printed on the screen is evaluated to see the return value, but if we save the
+content without printing it, no operation will be executed until we request the
+first element of the sequence.
Now that we’re acquainted with ClojureScript’s sequence abstraction and some of the generic sequence manipulating functions, it’s time to dive into the concrete @@ -2567,7 +2768,7 @@
Destructuring, as its name suggests, is a way of taking apart structured data such as collections and focusing on individual parts of them. ClojureScript offers a @@ -2799,7 +3000,7 @@
Threading macros, also known as arrow functions, enables one to write more readable code when multiple nested function calls are performed.
@@ -2820,7 +3021,7 @@(f (g (h x)))
is the same as (-> x h g f)
.
->
(thread-first macro)->
(thread-first macro)This is called thread first because it threads the first argument throught the different expressions as first arguments.
@@ -2852,12 +3053,12 @@This threading macro is especially useful for transforming data structures, because ClojureScript (and Clojure) functions for data structures -transformations consistently uses the first argument for receive the data +transformations consistently uses the first argument to receive the data structure.
->>
(thread-last macro)->>
(thread-last macro)The main difference between the thread-last and thread-first macros is that instead of threading the first argument given as the first argument on the following expresions, @@ -2893,7 +3094,7 @@
as->
(thread-as macro)as->
(thread-as macro)Finally, there are cases where neither ->
nor ->>
are applicable. In these
cases, you’ll need to use as->
, the more flexible alternative, that allows
@@ -2920,7 +3121,7 @@
some->
, some->>
(thread-some macros)some->
, some->>
(thread-some macros)Two of the more specialized threading macros that ClojureScript comes with. They work
in the same way as their analagous ->
and ->>
macros with the additional
@@ -2946,9 +3147,9 @@
cond->
, cond->>
(thread-cond macros)cond->
, cond->>
(thread-cond macros)The cond->
and cond->>
macros are analgous to ->
and ->>
that offers
+
The cond->
and cond->>
macros are analogous to ->
and ->>
that offers
the ability to conditionally skip some steps from the pipeline. Let see an example:
This language feature allows different dialects of Clojure to share common code that -is mostly platform independent but need some platform dependent code.
-To use reader conditionals, all you need is to rename your source file with
-.cljs
extension to one with .cljc
, because reader conditionals only work if
-they are placed in files with .cljc
extension.
#?
)There are two types of reader conditionals, standard and splicing. The standard -reader conditional behaves similarly to a traditional cond and the syntax looks -like this:
+The namespace is ClojureScript’s fundamental unit of code modularity. Namespaces
+are analogous to Java packages or Ruby and Python modules and can be defined with
+the ns
macro. If you have ever looked at a little bit of ClojureScript source, you
+may have noticed something like this at the beginning of the file:
(defn parse-int
- [v]
- #?(:clj (Integer/parseInt v)
- :cljs (js/parseInt v)))
+(ns myapp.core
+ "Some docstring for the namespace.")
+
+(def x "hello")
As you can observe, #?
reading macro looks very similar to cond, the difference is
-that the condition is just a keyword that identifies the platform, where :cljs
is
-for ClojureScript and :clj
is for Clojure. The advantage of this approach, is
-that it is evaluated at compile time so no runtime performance overhead exists for
-using this.
Namespaces are dynamic, meaning you can create one at any time. However, the +convention is to have one namespace per file. Naturally, a namespace definition is +usually at the beginning of the file, followed by an optional docstring.
#?@
)The splicing reader conditional works in the same way as the standard and allows
-splice lists into the containing form. The #?@
reader macro is used for that
-and the code looks like this:
(defn make-list
- []
- (list #?@(:clj [5 6 7 8]
- :cljs [1 2 3 4])))
-
-;; On ClojureScript
-(make-list)
-;; => (1 2 3 4)
-The ClojureScript compiler will read that code as this:
-(defn make-list
- []
- (list 1 2 3 4))
-The splicing reader conditional can’t be used to splice multiple top level forms, -so the following code is ilegal:
-#?@(:cljs [(defn func-a [] :a)
- (defn func-b [] :b)])
-;; => #error "Reader conditional splicing not allowed at the top level."
-If you need so, you can use multiple forms or just use do
block for group
-multiple forms together:
#?(:cljs (defn func-a [] :a))
-#?(:cljs (defn func-b [] :b))
-
-;; Or
-
-#?(:cljs
- (do
- (defn func-a [] :a)
- (defn func-b [] :b)))
-https://danielcompton.net/2015/06/10/clojure-reader-conditionals-by-example
-https://github.com/funcool/cuerdas (example small project that uses -reader conditionals)
-The namespace is ClojureScript’s fundamental unit of code modularity. Namespaces
-are analogous to Java packages or Ruby and Python modules and can be defined with
-the ns
macro. If you have ever looked at a little bit of ClojureScript source, you
-may have noticed something like this at the beginning of the file:
(ns myapp.core
- "Some docstring for the namespace.")
-
-(def x "hello")
-Namespaces are dynamic, meaning you can create one at any time. However, the -convention is to have one namespace per file. Naturally, a namespace definition is -usually at the beginning of the file, followed by an optional docstring.
-Previously we have explained vars and symbols. Every var that you define will be -associated with its namespace. If you do not define a concrete namespace, then the -default one called "cljs.user" will be used:
+Previously we have explained vars and symbols. Every var that you define will be +associated with its namespace. If you do not define a concrete namespace, then the +default one called "cljs.user" will be used:
Defining a namespace and the vars in it is really easy, but it’s not very useful if
we can’t use symbols from other namespaces. For this purpose, the ns
macro offers
@@ -3153,8 +3244,8 @@
As you can observe, we are using fully qualified names (namespace + var name) for -access to vars and functions from different namespaces.
+As you can observe, we are using fully qualified names (namespace + var name) to +access vars and functions from different namespaces.
While this will let you access other namespaces, it’s also repetitive and overly @@ -3203,7 +3294,7 @@
Additionally, ClojureScript offers a simple way to refer to specific vars or
functions from a concrete namespace using the :refer
directive, followed by a
-sequence of symbols that will refer to vars in the namespace. Effectively, it is as
+sequence of symbols that will refer to vars in the namespace. Effectively, it’s as
if those vars and functions are now part of your namespace, and you do not need to
qualify them at all.
When you have a namespace like myapp.core
, the code must be in a file named
core.cljs inside the myapp directory. So, the preceding examples with
@@ -3262,7 +3353,7 @@
I’m sure that at more than one time you have found yourself in this situation: you have defined a great abstraction (using interfaces or something similar) for your @@ -3276,7 +3367,7 @@
We can not trust languages that allow you to silently overwrite methods that you are +
You can’t trust languages that allow you to silently overwrite methods that you’re using when you import third party libraries; you cannot expect consistent behavior when this happens.
The ClojureScript primitive for defining "interfaces" is called a protocol. A protocol consists of a name and set of functions. All the functions have at least @@ -3317,7 +3408,7 @@
From the user perspective, protocol functions are simply plain functions defined in -the namespace where the protocol is defined. This enables an easy and simple aproach -for avoid conflicts between different protocols implemented for the same type that +the namespace where the protocol is defined. This enables an easy and simple approach +for avoiding conflicts between different protocols implemented for the same type that have conflicting function names.
Here is an example. Let’s create a protocol called IInvertible
for data that can
+
Here’s an example. Let’s create a protocol called IInvertible
for data that can
be "inverted". It will have a single method named invert
.
One of the big strengths of protocols is the ability to extend existing and maybe +
One of the big strengths of protocols is the ability to extend existing, and possibly, third party types. This operation can be done in different ways.
The majority of time you will tend to use the extend-protocol or the extend-type
-macros. This is how extend-type
syntax looks:
Most of the time you will tend to use the extend-protocol or the extend-type
+macros to do this. This is how extend-type
syntax looks:
So if you want extend your protocol to javascript primitive types, instead of using +
So, if you want to extend your protocol to javascript primitive types, instead of using
js/Number
, js/String
, js/Object
, js/Array
, js/Boolean
and js/Function
you should use the respective special symbols: number
, string
, object
,
array
, boolean
and function
.
We have previously talked about protocols which solve a very common use case of polymorphism: dispatch by type. But in some circumstances, the protocol approach can @@ -3609,7 +3700,7 @@
Hierarchies are ClojureScript’s way to let you build whatever relations that your domain may require. Hierarchies are defined in term of relations between named @@ -3720,7 +3811,7 @@
One of the big advantages of hierarchies is that they work very well together with
-multimethods. This is because multimethods by default use the isa?
function for
+multimethods. This is because multimethods by default use the isa?
function for
the last step of dispatching.
Until now, we have used maps, sets, lists, and vectors to represent our data. And in most cases, this is a really great approach. But sometimes we need to define our own @@ -3823,7 +3914,7 @@
The most low-level construction in ClojureScript for creating your own types is
the deftype
macro. As a demonstration, we will define a type called User
:
The record is a slightly higher-level abstraction for defining types in ClojureScript and should be the preferred way to do it.
@@ -3940,7 +4031,7 @@As we can see, the assoc
function works as expected and returns a new instance of
-the same type but with new key value pair. But take care with dissoc
! Its behavior
+the same type but with new key-value pair. But take care with dissoc
! Its behavior
with records is slightly different than with maps; it will return a new record if
the field being dissociated is an optional field, but it will return a plain map if
you dissociate a mandatory field.
Both type definition primitives that we have seen so far allow inline implementations for protocols (explained in a previous section). Let’s define one @@ -4008,7 +4099,7 @@
The reify
macro is an ad hoc constructor you can use to create objects without
pre-defining a type. Protocol implementations are supplied the same as deftype
@@ -4034,7 +4125,7 @@
specify!
is an advanced alternative to reify
, allowing you to add protocol
implementations to an existing JavaScript object. This can be useful if you want to
@@ -4076,14 +4167,14 @@
ClojureScript, in the same way as its brother Clojure, is designed to be a "guest" language. This means that the design of the language works well on top of an existing ecosystem such as JavaScript for ClojureScript and the JVM for Clojure.
ClojureScript, unlike what you might expect, tries to take advantage of every type that the platform provides. This is a (perhaps incomplete) list of things that @@ -4121,7 +4212,7 @@
ClojureScript comes with a little set of special forms that allows it to interact with platform types such as calling object methods, creating new instances, and @@ -4130,7 +4221,7 @@
ClojureScript has a special syntax for access to the entire platform environment +
ClojureScript has a special syntax to access the entire platform environment
through the js/
special namespace. This is an example of an expression to execute
JavaScript’s built-in parseInt
function:
The inconvenience of the previously explained forms is that they do not make recursive transformations, so if you have nested objects, the nested objects will -not be converted. Consider this example that uses Clojurescript maps, then a +not be converted. Consider this example that uses Clojurescript maps, then a similar one with JavaScript objects:
(clj->js {:foo {:bar "baz"}})
;; => #js {:foo #js {:bar "baz"}}
-(js->clj #js {:country {:code "FR" :name "France"}}))
+(js->clj #js {:country {:code "FR" :name "France"}})
;; => {"country" {:code "FR", :name "France"}}
Keep in mind that clj→js
is not the canonical way to transform data from
+clojurescript to js, that function is for debugging, because it is not exactly
+the most efficient way to do this operation. It is recommended that you build
+the transformation data structures specific to your domain rather than using the
+generic clj→js
.
We’ve learned that one of ClojureScript’s fundamental ideas is immutability. Both scalar values and collections are immutable in ClojureScript, except those mutable @@ -4423,7 +4521,7 @@
Vars can be redefined at will inside a namespace but there is no way to know when they change. The inability to redefine vars from other namespaces is a bit limiting; @@ -4432,7 +4530,7 @@
ClojureScript gives us the Atom
type, which is an object containing a value that
can be altered at will. Besides altering its value, it also supports observation
@@ -4511,7 +4609,7 @@
Volatiles, like atoms, are objects containing a value that can be altered. However, they don’t provide the observation and validation capabilities that atoms @@ -4561,23 +4659,23 @@
At this point, you are surely very bored with the constant theoretical explanations -about the language itself and will want to write and execute some code. The goal of -this section is to provide a little practical introduction to the ClojureScript -compiler.
+At this point, you are surely very bored with the constant theoretical +explanations about the language itself and will want to write and +execute some code. The goal of this section is to provide a little +practical introduction to the ClojureScript compiler.
The ClojureScript compiler takes the source code that has been split over numerous -directories and namespaces and compiles it down to JavaScript. Today, JavaScript has -a great number of different environments where it can be executed - each with its own +
The ClojureScript compiler takes the source code that has been split +over numerous directories and namespaces and compiles it down to +JavaScript. Today, JavaScript has a great number of different +environments where it can be executed - each with its own peculiarities.
This chapter intends to explain how to use ClojureScript without any additional
-tooling. This will help you understand how the compiler works and how you can use it
-when other tooling is not available (such as leiningen
-cljsbuild or
-boot).
For this case we are going to use shadow-cljs as a frontend for the +compiler. It is possible to use the compiler directly or use other +frontends, but in this case we will focus on just one, which in my +opinion best resolves usability.
The fastest way to obtain all the tooling needed for ClojureScript compilaition is -installing the Clojure CLI Tools:
-curl -O https://download.clojure.org/install/linux-install-1.10.1.462.sh
-chmod +x linux-install-1.10.1.462.sh
-sudo ./linux-install-1.10.1.462.sh
-This chapter supposes you have a properly installed nodejs (v20.10.0) +and JVM (JDK21). Other versions probably work but all this is tested +under specified versions.
If you are using other operating system other than linux, refer to -https://clojure.org/guides/getting_started page.
-Although the ClojureScript is self hosted, in this book we will use -the JVM implementation, with in turn requires JDK8 or JDK11 -installed. You can obtain it using your distribution default package -manager or download it from -Oracle -or Azul
+It also will be nice to have the rlwrap
tool, which can be installed
+this way on a debian like linux distributions:
sudo apt-get install openjdk-8-jdk rlwrap
-sudo apt-get install rlwrap
Let’s start with a practical example compiling code that will target Node.js -(hereafter simply "nodejs"). For this example, you should have nodejs installed.
-There are different ways to install nodejs, but the recommended way is using nvm -("Node.js Version Manager"). You can read the instructions on how to install and use -nvm on its home page.
When you have installed nvm, follow installing the latest version of nodejs:
+So, lets create our directory stucture and the first package.json
file:
nvm install v10.16.0
-nvm alias default v10.16.0
+mkdir -p mynodeapp/src/mynodeapp
+touch mynodeapp/package.json
+touch mynodeapp/shadow-cljs.edn
+touch mynodeapp/src/mynodeapp/main.cljs
You can test if nodejs is installed in your system with this command:
+Then, lets define our package.json
content:
$ node --version
-v10.16.0
+{
+ "name": "mynodeapp",
+ "version": "1.0.0",
+ "description": "",
+ "scripts": {
+ "server": "shadow-cljs server",
+ "watch": "shadow-cljs watch app",
+ "compile": "shadow-cljs compile app"
+ },
+ "author": "",
+ "license": "MPL-2.0",
+ "devDependencies": {
+ "shadow-cljs": "^2.26.2"
+ }
+}
For the first step of our practical example, we will create our application directory -structure and populate it with example code.
-Start by creating the directory tree structure for our “hello world” application:
-mkdir -p myapp/src/myapp
-touch myapp/src/myapp/core.cljs
-And execute the npm install
for correctly install shadow-cljs dependency
Resulting in this directory tree:
+Then, setup the basic shadow-cljs.edn confguration:
myapp
-└── src
- └── myapp
- └── core.cljs
+{:dependencies
+ []
+
+ :source-paths
+ ["src"]
+
+ :builds
+ {:app {:target :node-script
+ :output-to "target/app.js"
+ :main mynodeapp.main/main}}}
Second, write the example code into the previously created
-myapp/src/myapp/core.cljs
file:
And finally, put the following content on the mynodeapp/main.cljs
file:
(ns myapp.core
- (:require [cljs.nodejs :as nodejs]))
+(ns mynodeapp.main)
-(nodejs/enable-util-print!)
-
-(defn -main
+(defn main
[& args]
- (println "Hello world!"))
-
-(set! *main-cli-fn* -main)
+ (println "Hello world!"))
And finally, lets create the myapp/deps.edn
declaring the clojure
-and clojurescript versions:
{:deps {org.clojure/clojurescript {:mvn/version "1.10.520"}
- org.clojure/clojure {:mvn/version "1.10.1"}}
- :paths ["src"]}
-In order to compile that source code, we need a simple build script that tells the -ClojureScript compiler the source directory and the output file. ClojureScript -has a lot of other options, but at this moment we can ignore that.
-Let’s create the myapp/build.clj
file with the following content:
Now it’s time to compile the project:
(require '[cljs.build.api :as b])
+$ npm run compile
-(b/build "src" {:main 'myapp.core
- :output-to "main.js"
- :output-dir "out"
- :target :nodejs
- :verbose true})
-
This is a brief explanation of the compiler options used in this example:
-The :output-to
parameter indicates to the compiler the destination of the compiled
-code, in this case to the "main.js" file.
The :main
property indicates to the compiler the namespace that will act as the entry
-point of your application when it’s executed.
The :target
property indicates the platform where you want to execute the compiled
-code. In this case, we are going to use nodejs. If you omit this
-parameter, the source will be compiled to run in the browser environment.
To run the compilation, just execute the following command:
-cd myapp
-clojure build.clj
+> mynodeapp@1.0.0 compile
+> shadow-cljs compile app
+
+shadow-cljs - config: /home/user/playground/mynodeapp/shadow-cljs.edn
+[:app] Compiling ...
+[:app] Build completed. (45 files, 1 compiled, 0 warnings, 1.65s)
And when it finishes, execute the compiled file using node:
+And when it finishes, execute the result using node binary:
$ node main.js
+$ node target/main.js
Hello world!
The shadow-cljs guide is very extensive, and you can read more detailed documentation +here: https://shadow-cljs.github.io/docs/UsersGuide.html#target-node
In this section we are going to create an application similar to the "hello world" example from the previous section to run in the browser environment. The minimal requirement for this application is just a browser that can execute JavaScript.
The process is almost the same, and the directory structure is the same. The only -things that changes is the entry point of the application and the build script. So, -start re-creating the directory tree from previous example in a different directory.
+The process is almost the same, and the directory structure is very similar, with few +additions.
mkdir -p mywebapp/src/mywebapp
-touch mywebapp/src/mywebapp/core.cljs
+mkdir -p mywebapp/public
+mkdir -p mywebapp/src/mywebapp
+touch mywebapp/public/index.html
+touch mywebapp/src/mywebapp/main.cljs
+touch mywebapp/package.json
+touch mywebapp/shadow-cljs.edn
Resulting in this directory tree:
+Set setup the package.json
file:
mywebapp
-└── src
- └── mywebapp
- └── core.cljs
+{
+ "name": "mywebapp",
+ "version": "1.0.0",
+ "description": "",
+ "scripts": {
+ "server": "shadow-cljs server",
+ "watch": "shadow-cljs watch app",
+ "compile": "shadow-cljs compile app"
+ },
+ "author": "",
+ "license": "MPL-2.0",
+ "devDependencies": {
+ "shadow-cljs": "^2.26.2"
+ }
+}
+Execute the npm install
for correctly install the shadow-cljs dependency.
Then, write new content to the mywebapp/src/mywebapp/core.cljs
file:
Then, setup shadow-cljs configuration on shadow-cljs.edn
file:
(ns mywebapp.core)
+{:dev-http {8888 "public"}
-(enable-console-print!)
+ :dependencies
+ []
-(println "Hello world!")
-
In the browser environment we do not need a specific entry point for the application, -so the entry point is the entire namespace.
In order to compile the source code to run properly in a browser, overwrite the
-mywebapp/build.clj
file with the following content:
Write new content to the src/mywebapp/main.cljs
file:
(require '[cljs.build.api :as b])
+(ns mywebapp.core)
-(b/build "src" {:output-to "main.js"
- :source-map true
- :output-dir "out/"
- :main 'mywebapp.core
- :verbose true
- :optimizations :none})
-
This is a brief explanation of the compiler options we’re using:
+(defn main + [] + (println "Hello world!"))The :output-to
parameter indicates to the compiler the destination of the
-compiled code, in this case the "main.js" file.
The :main
property indicates to the compiler the namespace that will act as
-the entry point of your application when it’s executed.
:source-map
indicates the destination of the source map. (The source map
-connects the ClojureScript source to the generated JavaScript so that error
-messages can point you back to the original source.)
:output-dir
indicates the destination directory for all file sources used in a
-compilation. It is just for making source maps work properly with the rest of the
-code, not only your source.
:optimizations
indicates the compilation optimization. There are different
-values for this option, but that will be covered in subsequent sections in
-more detail.
To run the compilation, just execute the following command:
+Once all this is ready, instead of just compiling it, we start a watch process:
cd mywebapp;
-clojure build.clj
-$ npm run watch
+
+> mywebapp@1.0.0 watch
+> shadow-cljs watch app
+
+shadow-cljs - config: /home/user/playground/mywebapp/shadow-cljs.edn
+shadow-cljs - HTTP server available at http://localhost:8888
+shadow-cljs - server version: 2.26.2 running at http://localhost:9630
+shadow-cljs - nREPL server started on port 44159
+shadow-cljs - watching build :app
+[:app] Configuring build.
+[:app] Compiling ...
+[:app] Build completed. (119 files, 0 compiled, 0 warnings, 2.58s)
This process can take some time, so do not worry; wait a little bit. The JVM -bootstrap with the Clojure compiler is slightly slow. In the following sections, we -will explain how to start a watch process to avoid constantly starting and stopping -this slow process.
While waiting for the compilation, let’s create a dummy HTML file to make it easy to -execute our example app in the browser. Create the index.html file with the -following content; it goes in the main mywebapp directory.
+The watch also starts a development http server at http://localhost:8888 for access our +brand new web application. But we still need a last step: we need to create an index.html +where we’re going to load our recently compiled js.
public/index.html
<!DOCTYPE html>
<html>
@@ -4907,526 +4929,162 @@ <title>Hello World from ClojureScript</title>
</header>
<body>
- <script src="main.js"></script>
+ <script src="js/main.js"></script>
</body>
</html>
Now, when the compilation finishes and you have the basic HTML file you can just open -it with your favorite browser and take a look in the development tools console. The -"Hello world!" message should appear there.
+So, open the browser on http://localhost:8888 and open the devconsole with F12 key and
+see the Hello World
printed on the console tab.
The shadow-cljs guide is very extensive, and you can read more detailed documentation +here: https://shadow-cljs.github.io/docs/UsersGuide.html#target-browser
You may have already noticed the slow startup time of the ClojureScript -compiler. To solve this, the ClojureScript standalone compiler comes with a -tool to watch for changes in your source code, and re-compile modified files as -soon as they are written to disk.
-Let’s start converting our build.clj script to something that can accept
-arguments and execute different tasks. Let’s create a tools.clj
script file
-with the following content:
Although you can create a source file and compile it every time you want to try +something out in ClojureScript, it’s easier to use the REPL. REPL stands for:
(require '[cljs.build.api :as b])
-
-(defmulti task first)
-
-(defmethod task :default
- [args]
- (let [all-tasks (-> task methods (dissoc :default) keys sort)
- interposed (->> all-tasks (interpose ", ") (apply str))]
- (println "Unknown or missing task. Choose one of:" interposed)
- (System/exit 1)))
-
-(def build-opts
- {:output-to "main.js"
- :source-map true
- :output-dir "out/"
- :main 'mywebapp.core
- :verbose true
- :optimizations :none})
-
-(defmethod task "build"
- [args]
- (b/build "src" build-opts))
-
-(defmethod task "watch"
- [args]
- (b/watch"src" build-opts))
-
-(task *command-line-args*)
+Read - get input from the keyboard
+Evaluate the input
+Print the result
+Loop back for more input
+In other words, the REPL lets you try out ClojureScript concepts and get immediate +feedback.
Now you can start the watch process with the following command:
+ClojureScript comes with support for executing the REPL in different execution +environments, each of which has its own advantages and disadvantages. For example, you can +run a REPL in nodejs but in that environment you don’t have any access to the DOM. Which +REPL environment is best for you depends on your specific needs and requirements.
clojure tools.clj watch
+$ shadow-cljs node-repl
Go back to the mywebapp.core
namespace, and change the print text to "Hello World,
-Again!"
. You’ll see that the file src/mywebapp/core.cljs
the file is immediately
-recompiled, and if you reload index.html
in your browser the new text is displayed
-in the developer console.
This starts a blank CLJS REPL with an already connected node process.
+
+ Important
+ |
++If you exit the Node REPL the node process is also killed! + | +
You also can start the simple build with:
+The node-repl
lets you get started without any additional configuration. It has access
+to all your code via the usual means, ie. (require '[your.core :as x]). Since it is not
+connected to any build it does not do any automatic rebuilding of code when your files
+change and does not provide hot-reload.
clojure tools.clj build
+$ shadow-cljs browser-repl
And finally, if you execute the build.clj
script with no params, a help message
-with available "tasks" will be printed:
This starts a blank CLJS REPL and will open an associated Browser window where the code +will execute. Besides running in the Browser this has all the same functionality as the +above node-repl.
$ clojure tools.clj
-Unknown or missing task. Choose one of: build, watch
+
+ Important
+ |
++If you close the Browser window the REPL will stop working. + | +
The ClojureScript compiler has different levels of optimization. Behind the scenes, -those compilation levels are coming from the Google Closure Compiler.
+The node-repl
and browser-repl
work without any specific build configuration. That
+means they will only do whatever you tell them to do but nothing on their own.
A simplified overview of the compilation process is:
+But there is also an option to connect to a build specific repl, for which to work +correctly you need 2 things:
The reader reads the code and does some analysis. This compiler may raise some -warnings during this phase.
-Then, the ClojureScript compiler emits JavaScript code. The result is one -JavaScript output file for each ClojureScript input file.
+a running watch for your build
The generated JavaScript files are passed through the Google Closure Compiler -which, depending on the optimization level and other options (sourcemaps, output -dir output to, …), generates the final output file(s).
+connect the JS runtime of the :target. Meaning if you are using the :browser target you +need to open a Browser that has the generated JS loaded. For node.js builds that means +running the node process.
The final output format depends on the optimization level chosen:
+This optimization level causes the generated JavaScript to be written into separate -output files for each namespace, without any additional transformations to the code.
+Once you have both you can connect to the CLJS REPL via the command line or from the Clojure REPL.
# One terminal
+$ npx shadow-cljs watch app
+...
+
+# different terminal
+$ npx shadow-cljs cljs-repl app
+shadow-cljs - connected to server
+[3:1]~cljs.user=>
+REPL
This optimization level causes the generated JavaScript files to be concatenated into -a single output file, in dependency order. Line breaks and other whitespace are -removed.
This reduces compilation speed somewhat, resulting in a slower compilations. However, -it is not terribly slow and it is quite usable for small-to-medium sized -applications.
+
+ Tip
+ |
++type :repl/quit to exit the REPL. This will only exit the REPL, the watch will remain running. +TIP: You may run multiple watch "workers" in parallel and connect/disconnect to their REPLs at any given time. + | +
The simple compilation level builds on the work from the whitespace
optimization
-level, and additionally performs optimizations within expressions and functions, such
-as renaming local variables and function parameters to have shorter names.
Compilation with the :simple
optimization always preserves the functionality of
-syntactically valid JavaScript, so it does not interfere with the interaction between
-the compiled ClojureScript and other JavaScript.
The advanced compilation level builds on the simple
optimization level, and
-additionally performs more aggressive optimizations and dead code elimination. This
-results in a significantly smaller output file.
The :advanced
optimizations only work for a strict subset of JavaScript which
-follows the Google Closure Compiler rules. ClojureScript generates valid
-JavaScript within this strict subset, but if you are interacting with third party
-JavaScript code, some additional work is required to make everything work as
-expected.
This interaction with third party javascript libraries will be explained in later -sections.
-Although you can create a source file and compile it every time you want to try -something out in ClojureScript, it’s easier to use the REPL. REPL stands for:
-Read - get input from the keyboard
-Evaluate the input
-Print the result
-Loop back for more input
-In other words, the REPL lets you try out ClojureScript concepts and get immediate -feedback.
-ClojureScript comes with support for executing the REPL in different execution -environments, each of which has its own advantages and disadvantages. For example, -you can run a REPL in nodejs but in that environment you don’t have any access to the -DOM. Which REPL environment is best for you depends on your specific needs and -requirements.
-The Nashorn REPL is the easiest and perhaps most painless REPL environment because it -does not require any special stuff.
-Let’s start creating a new script file for our repl playground called
-tools.clj
in a new directory (in our case repl_playground/tools.clj
):
(require '[cljs.repl :as repl])
-(require '[cljs.repl.nashorn :as nashorn])
-
-(defmulti task first)
-
-(defmethod task :default
- [args]
- (let [all-tasks (-> task methods (dissoc :default) keys sort)
- interposed (->> all-tasks (interpose ", ") (apply str))]
- (println "Unknown or missing task. Choose one of:" interposed)
- (System/exit 1)))
-
-(defmethod task "repl:nashorn"
- [args]
- (repl/repl (nashorn/repl-env)
- :output-dir "out/nashorn"
- :cache-analysis true))
-
-(task *command-line-args*)
-Create the repl_playground/deps.edn
file with the following content (identical
-from previous examples):
{:deps {org.clojure/clojurescript {:mvn/version "1.10.520"}
- org.clojure/clojure {:mvn/version "1.10.1"}}
- :paths ["src"]}
-And now, we can execute the REPL:
-$ clj tools.clj repl:nashorn
-ClojureScript 1.10.520
-cljs.user=> (prn "Hello world")
-"Hello world"
-nil
-You may have noticed that in this example we have used clj
command instead of
-clojure
. That two commands are practically identical, the main difference is
-that clj
executes clojure
command wrapped in rlwrap
. The rlwrap
tool
-gives the "readline" capabilities which enables history, code navigation, and
-other shell-like features that will make your REPL experience much more
-pleasant.
If you don’t have installed it previously, you can install it with sudo apt
-install -y rlwrap
.
- Note
- |
--This is a basic repl, in the following chapters we will explain how to -have a more advanced repl experience with code-highlighting, code-completion and -multiline edition. - | -
You must, of course, have nodejs installed on your system to use this REPL.
-You may be wondering why we might want a nodejs REPL, when we already have the -nashorn REPL available which doesn’t have any external dependencies. The answer is -very simple: nodejs is the most used JavaScript execution environment on the backend, -and it has a great number of community packages built around it.
-The good news is that starting a nodejs REPL is very easy once you have it installed
-in your system. Start adding the following content into tools.clj
script:
(require '[cljs.repl.node :as node])
-
-(defmethod task "node:repl"
- [args]
- (repl/repl (node/repl-env)
- :output-dir "out/nodejs"
- :cache-analysis true))
-And start the REPL:
-$ clj tools.clj repl:node
-ClojureScript 1.10.520
-cljs.user=> (prn "Hello world")
-"Hello world"
-nil
-This REPL is the most laborious to get up and running. This is because it uses a -browser for its execution environment and it has additional requirements.
-Let’s start by adding the following content to the tools.clj
script file:
(require '[cljs.build.api :as b])
-(require '[cljs.repl.browser :as browser])
-
-(defmethod task "repl:browser"
- [args]
- (println "Building...")
- (b/build "src"
- {:output-to "out/browser/main.js"
- :output-dir "out/browser"
- :source-map true
- :main 'myapp.core
- :optimizations :none})
-
- (println "Launching REPL...")
- (repl/repl (browser/repl-env :port 9001)
- :output-dir "out/browser"))
-The main difference with the previous examples, is that browser REPL requires -that some code be execution in the browser before the REPL gets working. To do -that, just re-create the application structure very similar to the one that we -have used in previous sections:
-mkdir -p src/myapp
-touch src/myapp/core.cljs
-Then, write new content to the src/myapp/core.cljs
file:
(ns myapp.core
- (:require [clojure.browser.repl :as repl]))
-
-(defonce conn
- (repl/connect "http://localhost:9001/repl"))
-
-(enable-console-print!)
-
-(println "Hello, world!")
-And finally, create the missing index.html file that is going to be used as -the entry point for running the browser side code of the REPL:
-<!DOCTYPE html>
-<html>
- <header>
- <meta charset="utf-8" />
- <title>Hello World from ClojureScript</title>
- </header>
- <body>
- <script src="out/browser/main.js"></script>
- </body>
-</html>
-Well, that was a lot of setup! But trust us, it’s all worth it when you see it in
-action. To do that, just execute the tools.clj
in the same way that we have done
-it in previous examples:
$ clj tools.clj repl:browser
-Building...
-Launching REPL...
-ClojureScript 1.10.520
-cljs.user=>
-And finally, open your favourite browser and go to http://localhost:9001/. Once the -page is loaded (the page will be blank), switch back to the console where you have -run the REPL and you will see that it is up and running:
-[...]
-To quit, type: :cljs/quit
-cljs.user=> (+ 14 28)
-42
-One of the big advantages of the browser REPL is that you have access to everything
-in the browser environment. For example, type (js/alert "hello world")
in the
-REPL. This will cause the browser to display an alert box. Nice!
- Note
- |
--This is just a preview of how to use the builtin REPL capabilities of the -ClojureScript compiler. There are better and more user/developer friendly repl -environments with code-highlighting, code-completion and multiline edition (and -in case of web development, also with code hot reloading) that will be explained -in the following chapters. - | -
This is a library that adds more advanced features to the Clojure(Script) -builtin REPL and enables code-highlighting, code-completion and multiline -edition.
-Let’s start adding rebel dependency into deps.edn
file:
{:deps {org.clojure/clojurescript {:mvn/version "1.10.520"}
- org.clojure/clojure {:mvn/version "1.10.1"}
- com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"}
- com.bhauman/rebel-readline {:mvn/version "0.1.4"}}
- :paths ["src"]}
-And adding the followin code to the tools.clj
script file:
(require '[rebel-readline.core]
- '[rebel-readline.clojure.main]
- '[rebel-readline.clojure.line-reader]
- '[rebel-readline.cljs.service.local]
- '[rebel-readline.cljs.repl])
-
-(defmethod task "repl:rebel:node"
- [args]
- (rebel-readline.core/with-line-reader
- (rebel-readline.clojure.line-reader/create
- (rebel-readline.cljs.service.local/create))
- (repl/repl (node/repl-env)
- :prompt (fn [])
- :read (rebel-readline.cljs.repl/create-repl-read)
- :output-dir "out/nodejs"
- :cache-analysis true)))
-And start the REPL:
-$ clojure tools.clj repl:rebel:node
-ClojureScript 1.10.520
-cljs.user=> (println
-cljs.core/println: ([& objs])
-You can find that while you writing in the repl, it automatically suggest and -shows se function signature that you want to execute.
-You can find more information about all rebel-readline capabilities on -https://github.com/bhauman/rebel-readline
-The Google Closure Library is a javascript library developed by Google. It has a modular architecture, and provides cross-browser functions for DOM manipulations and @@ -5475,142 +5133,12 @@
You can found the reference to all namespaces in the closure library here: http://google.github.io/closure-library/api/
Until now, we have used the builtin Clojure(Script) toolchain to compile our -source files to JavaScript. Now this is a time to understand how manage external -and/or third party dependencies.
-The best way to show how a tool works is by creating a toy project with it. In -this case, we will create a small application that determines if a year is a -leap year or not.
-Let’s start creating the project layout:
-mkdir -p leapyears/src/leapyears
-mkdir -p leapyears/target/public
-touch leapyears/target/public/index.html
-touch leapyears/src/leapyears/core.cljs
-touch leapyears/tools.cljs
-touch leapyears/deps.edn
-The project has the following structure:
-leapyears -├── deps.edn -├── src -│ └── leapyears -│ └── core.cljs -├── target -│ └── public -│ └── index.html -└── tools.clj-
The deps.edn
file contains information about all the packaged dependencies
-needed to build or execute the application. Packaged dependencies are libraries
-packaged as jar files and uploaded to clojars/maven repository.
- Note
- |
-
-
-
-But ClojureScript can consume external code in many different ways: -
-
-
-
-This will be explained in the following sections. - |
-
Let’s start with a simple deps.edn
file:
{:deps {org.clojure/clojurescript {:mvn/version "1.10.520"}
- org.clojure/clojure {:mvn/version "1.10.1"}
- com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"}
- com.bhauman/rebel-readline {:mvn/version "0.1.4"}}
- :paths ["src" "target"]}
-And simple build script (tools.clj
file):
(require '[cljs.build.api :as b])
-(require '[cljs.repl :as repl])
-(require '[cljs.repl.node :as node])
-
-(defmulti task first)
-
-(defmethod task :default
- [args]
- (let [all-tasks (-> task methods (dissoc :default) keys sort)
- interposed (->> all-tasks (interpose ", ") (apply str))]
- (println "Unknown or missing task. Choose one of:" interposed)
- (System/exit 1)))
-
-(def build-opts
- {:output-to "target/public/js/leapyears.js"
- :source-map true
- :output-dir "target/public/js/leapyears"
- :main 'leapyears.core
- :verbose true
- :optimizations :none})
-
-(defmethod task "repl"
- [args]
- (repl/repl (node/repl-env)
- :output-dir "target/nodejs"
- :cache-analysis true))
-
-(defmethod task "build"
- [args]
- (b/build "src" build-opts))
-
-(defmethod task "watch"
- [args]
- (b/watch "src" build-opts))
-
-(task *command-line-args*)
-To properly understand how we can use the "batteries included" of google closure library, +lets add some functionality to our mywebapp example application.
Then, write the following content into target/public/index.html
file:
Lets update our public/index.html
with the following content:
The next step consist in add some code to make the form interactive. Put the
-following code into the src/leapyears/core.cljs
:
And then, update the src/mywebapp/main.cljs
file with:
(ns leapyears.core
- (:require [goog.dom :as dom]
- [goog.events :as events]
- [cljs.reader :refer (read-string)]))
-
-(enable-console-print!)
-
-(def input (dom/getElement "year"))
-(def result (dom/getElement "result"))
+(ns mywebapp.main
+ (:require
+ [goog.dom :as dom]
+ [goog.events :as events]
+ [cljs.reader :refer [read-string]]))
(defn leap?
[year]
@@ -5662,47 +5185,85 @@ 4.4.1. First projec
(defn on-change
[event]
(let [target (.-target event)
+ result (dom/getElement "result")
value (read-string (.-value target))]
(if (leap? value)
(set! (.-innerHTML result) "YES")
(set! (.-innerHTML result) "NO"))))
-(events/listen input "keyup" on-change)
+(defn main
+ []
+ (let [input (dom/getElement "year")]
+ (events/listen input "keyup" on-change)))
Now, we can compile the project with:
+Now, we can compile the project in the same way as previously
clojure tools.clj watch
+npm run watch
+Finally, open the http://locahost:8888
in a browser. Typing a year in the textbox should
+display an indication of its leap year status.
Until now, we have used the builtin Clojure(Script) toolchain to compile our source
+files to JavaScript. Now this is a time to understand how manage external and/or third
+party dependencies. Lets use our mywebapp
to add some functionality with help of an
+external dependencies.
Finally, open the target/public/index.html
file in a browser. Typing a year in the textbox
-should display an indication of its leap year status.
With shadow-cljs
we have the following approaches to add dependencies:
Adding a native/cljs dependency
+Adding a npm dependency
+Adding a global dependency
+In this example we will use the Cuerdas (a string +manipulation library build especifically for Clojure(Script)) for improve the previous +functionality of the mywebapp.
+Until now we have used only the batteries included in the ClojureScript runtime, -let improve our project including a native dependency. In this example we will -use the Cuerdas (a string manipulation -library build especifically for Clojure(Script)).
+Let’s update our shadow-cljs.edn
file with the dependency:
{:dependencies
+ [[funcool/cuerdas "2022.06.16-403"]]
+
+ ;; [...]
+ }
+Add funcool/cuerdas {:mvn/version "2.2.0"}
into the :deps
section inside the
-deps.edn
file. And add the corresponding modifications to the
-leapyears/core.cljs
file:
And the following modifications to the src/mywebapp/main.cljs
file:
(ns leapyears.core
- (:require [goog.dom :as dom]
- [goog.events :as events]
- [cuerdas.core :as str]
- [cljs.reader :refer (read-string)]))
+(ns mywebapp.main
+ (:require
+ [goog.dom :as dom]
+ [goog.events :as events]
+ [cuerdas.core :as str]
+ [cljs.reader :refer [read-string]]))
;; [...]
@@ -5722,320 +5283,212 @@
-Clojure packages are often published on Clojars. You can
-also find many third party libraries on the
-ClojureScript Wiki.
-
In some circumstances you may found yourself that you need some library but that -does not exists in ClojureScript but it is already implemented in javascript -and you want to use it on your project.
-There are many ways that you can do it mainly depending on the library that you
-want to include. Many of that libraries are packaged and uploaded to clojars, so
-you can declare them in the deps.edn
in the same way as native dependencies
-(with some peculirities in usage, see below).
If you have a library that is written to be compatible with google closure
-module system and you want to include it on your project: put the source into
-the classpath (inside src
or vendor
directory in leapyears project) and
-access it like any other clojure namespace.
This is the most simplest case, because google closure modules are directly -compatible and you can mix your clojure code with javascript code written using -google closure module system without any additional steps.
+On the other hand, not all dependencies are available as native packages. But one of the +advantages of ClojureScript is that it embraces being able to integrate with the host +language, in this case JS. For this case, shadow-cljs integrates quite well with the NPM +ecosystem.
Reusing the leapyears project, lets implement the leap?
function in
-a javascript using google closure module format. Start creating the
-directory structure:
Lets add the date-fns
NPM dependency:
touch src/leapyears/util.js
+$ npm add date-fns
And add the implementation using closure module format:
+And then, lets modify our code to use that library:
goog.provide("leapyears.util");
-
-goog.scope(function() {
- var module = leapyears.util;
-
- module.isLeap = function(val) {
- return (0 === (val % 400)) || (((val % 100) > 0) && (0 === (val % 4)));
- };
-});
-Now, if you open the repl, you can import the namespace and use the isLeap
-function
(require '[leapyears.util :as util])
-
-(util/isLeap 2112)
-;; => true
+(ns mywebapp.main
+ (:require
+ ["date-fns" :as df]
+ [goog.dom :as dom]
+ [cuerdas.core :as str]
+ [goog.events :as events]))
-(util/isLeap 211)
-;; => false
-
- Note
- |
-
-you can open the nodejs repl just executing clj tools.clj repl in the
-root of the project.
- |
-
- Note
- |
--this is the approach used by many projects to implement some performance -sensitive logic directly in javascript and export it in an easy way to -ClojureScript - | -
This is the most extended and the most reliable way to consume external -javascript libraries from ClojureScript and it has many facilities.
-The fastest way to include a javascript library is looking if it is available in
-CLJSJS. If it is available, just include the
-dependency in the deps.edn
file and use it.
That libraries has two ways of use it, let’s see some examples.
-Start adding moment
dependency to deps.edn
file:
cljsjs/moment {:mvn/version "2.24.0-0"}
-Then, open the repl and try the following:
-js/
special namespace(require '[cljsjs.moment]) ;; just require, no alias
+(defn on-change
+ [event]
+ (let [result (dom/getElement "result")
+ target (.-target event)
+ value (.-value target)]
-(.format (js/moment) "LLLL")
-;; => "Monday, July 15, 2019 5:32 PM"
-(require '[moment :as m])
+ (if (str/blank? value)
+ (set! (.-innerHTML result) "---")
+ (if (df/isLeapYear value)
+ (set! (.-innerHTML result) "YES")
+ (set! (.-innerHTML result) "NO")))))
-(.format (m) "LLLL")
-;; => "Monday, July 15, 2019 5:33 PM"
-Behind the scenes that packages uses the ClojureScript compiler facilities -descibed -here for provide the compiler with enough information about the files and -global exports to use.
So, if don’t find a library in cljsjs, we can include it using the same -facilities. Let’s assume that moment is not available on cljsjs and we need it -on our project.
+You can observe that, NPM depenencies are declared as strings on the ns
section. Then,
+you just use the isLeapYear
exported function from the date-fns library like any other
+function.
For include an foreign dependency we need to pass :foreign-libs
and :externs
-params to the ClojureScript compiler, and we have two ways:
You can read the whole detail on how it works here: +https://shadow-cljs.github.io/docs/UsersGuide.html#npm
passing them to the build
or repl
functions.
inside the deps.cljs
file located on the root of the classpath.
The deps.cljs
approach requires that files should be localted on the local
-directories, but the first approach allows specify directly external urls. We
-will use the first approach on our example.
There’s not much to really explain here, in this case, include whatever library is needed
+from the CDN or elsewhere in the index.html and access the global export that that library
+uses using clojurescript’s js/
syntax to access globally defined things. Just as you
+would do, for example, to access the location
object.
This is how looks the deps.edn
file with the changes applied:
Example:
;; [...]
-
-(def foreign-libs
- [{:file "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.js"
- :file-min "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"
- :provides ["vendor.moment"]}])
-
-
-(def build-opts
- {:output-to "target/public/js/leapyears.js"
- :source-map true
- :output-dir "target/public/js/leapyears"
- :main 'leapyears.core
- :verbose true
- :optimizations :none
- :foreign-libs foreign-libs})
-
-(defmethod task "repl"
- [args]
- (repl/repl (node/repl-env)
- :foreign-libs foreign-libs
- :output-dir "target/node"
- :cache-analysis true))
-
-;; [...]
+(js/console.log (.-href js/location))
Now, if you excute the repl, you will be able to import vendor.moment
in the same way
-if you are using the cljsjs package.
Finally, there are the :externs
option, that will be needed only for the
-production build, and the externs file consists in plain javascript files that
-declares the public API of the included foreign libraries and make them
-understandable to the google closure compiler.
There are also circumstances in which it is better to use the host’s native language to +implement some feature or you already have a piece of code implementing some algorithm and +you want to be able to use it from CLJS.
The moment externs are available
-here
-and if you include some library that you want to use, and then want to compile
-your app with advanced optimizations you will need to include a file similar to
-the moment.ext.js on the project and referenciate it with :externs
option to
-the ClojureScript compiler.
For this case, we have two options: an ESM module or a Closure module.
More info on clojurescript.org.
+Lets start with a Closure module definition.
The main advantage of this approach is that there are no distinction between a
+clojurescript namespace and a file defined using closure module style. You import it in
+the same way independently on how it is implemented. Also if you have a library that is
+written to be compatible with closure module system and you want to include it on your
+project. And finally, it is the standard way how clojurescript compiler generates code
+from your .cljs
files, so it works with or without shadow-cljs tooling.
Google Closure Compiler has an advanced feature that allows convert from -different module types (commonjs and ES) into google closure module -type. Although this feature is still experimental. With simple modules it works, -but with more complex modules (many submodules and directories) still doest -not complies correctly.
+Obviously, the main disadvantage is that is uses a custom, not very common approach for +defining it.
In any way I invite you to experiment with it. You can found more documentation -on -clojurescript.org.
+So, for understand it correctly, lets use our mywebapp
application and add an util
+module. This is the most simplest case, because closure modules are directly compatible
+with cljs and you can mix your clojurescript code with javascript code without any
+additional steps.
touch src/mywebapp/util.js
+The best way to use ES6 and/or CommonJS module is combining a javascript bundler -like rollup or webpack to generate a single build with external dependencies -and thn use the global exports method to use it in ClojureScript. An example -of this is explained on clojurescript.org.
+And then, we implement the isLeapYear in javascript:
goog.provide("mywebapp.util");
+
+goog.scope(function() {
+ let module = mywebapp.util;
+
+ module.isLeapYear = function(val) {
+ val = parseInt(val, 10);
+ return (0 === (val % 400)) || (((val % 100) > 0) && (0 === (val % 4)));
+ };
+});
And finally we will introduce figwheel, that enables fully interactive, -REPL-based and hot reloading enabled development environment.
+Then modify the src/mywebapp/main.cljs
file to use our util module:
(ns mywebapp.main
+ (:require
+ [mywebapp.util :as util]
+ [goog.dom :as dom]
+ [cuerdas.core :as str]
+ [goog.events :as events]))
+
+(defn on-change
+ [event]
+ (let [result (dom/getElement "result")
+ target (.-target event)
+ value (.-value target)]
+
+ (if (str/blank? value)
+ (set! (.-innerHTML result) "---")
+ (if (util/isLeapYear value)
+ (set! (.-innerHTML result) "YES")
+ (set! (.-innerHTML result) "NO")))))
+
+(defn main
+ []
+ (let [input (dom/getElement "year")]
+ (events/listen input "keyup" on-change)))
+We will reuse the leapyears project structure for the following examples.
+As you can observe, the require entry is indistinguible from any other, because it +integrates 100% with the cljs build process.
- Note
- |
--Although we use figwheel here for web application, it works in the same -way on the application that targets nodejs as execution environment. - | -
This is other way to define an companion modules using JS language; is the recommended way +to do it on shadow-cljs and the main advantage of it is the familiar ESM syntax.
As first step, we need to add figwheel dependency to the deps.edn
file:
So, lets proceed to define/overwrite the same module used in previous example to use the +ESM syntax.
com.bhauman/figwheel-main {:mvn/version "0.2.1"}
+export function isLeapYear (val) {
+ val = parseInt(val, 10);
+ return (0 === (val % 400)) || (((val % 100) > 0) && (0 === (val % 4)));
+}
Then, add new task to the tools.clj
script:
Then, change the require on the src/mywebapp/main.cljs
file to:
(def figwheel-opts
- {:open-url false
- :load-warninged-code true
- :auto-testing false
- :ring-server-options {:port 3448 :host "0.0.0.0"}
- :watch-dirs ["src"]})
+(ns mywebapp.main
+ (:require
+ ["./util.js" :as util]
+ [goog.dom :as dom]
+ [cuerdas.core :as str]
+ [goog.events :as events]))
-(defmethod task "figwheel"
- [args]
- (figwheel/start figwheel-opts {:id "main" :options build-opts}))
-
And then, run clojure tools.clj fighweel
. This will start the figwheel process
-that automatically will launch a http server that will serve the
-target/public/
directory and index to index.html
file.
If you update the code, that code will be automatically loaded to the browser, -without page reload.
+You can read a more detailed information about this here: https://shadow-cljs.github.io/docs/UsersGuide.html#classpath-js
For more info: figwheel.org.
As you might expect, testing in ClojureScript consists of the same concepts widely used by other language such as Clojure, Java, Python, JavaScript, etc.
@@ -6052,7 +5505,7 @@The "official" ClojureScript testing framework is in the "cljs.test" namespace. It is a very simple library, but it should be more than enough for our purposes.
@@ -6064,152 +5517,80 @@We will reuse the leapyears
project structure and we will add testing to it. Let’s create
-the test related files and directories:
mkdir -p test/leapyears/test
-touch test/leapyears/test/main.cljs
-Also we will need to create new tasks on our tools.clj
file for build, watch and run
-tests:
We will reuse the mywebapp
project structure and we will add testing to it. Let’s create
+the test file src/mywebapp/util_test.cljs
:
(require '[clojure.java.shell :as shell])
-
-;; [...]
-
-(defmethod task "build:tests"
- [args]
- (b/build (b/inputs "src" "vendor" "test")
- (assoc build-opts
- :main 'leapyears.test.main
- :output-to "out/tests.js"
- :output-dir "out/tests"
- :target :nodejs)))
+(ns mywebapp.util-test
+ (:require
+ [cljs.test :as t]
+ ["./util.js" :as util]))
-(defmethod task "watch:test"
- [args]
- (letfn [(run-tests []
- (let [{:keys [out err]} (shell/sh "node" "out/tests.js")]
- (println out err)))]
- (println "Start watch loop...")
- (try
- (b/watch (b/inputs "src", "test")
- (assoc build-opts
- :main 'leapyears.test.main
- :watch-fn run-tests
- :output-to "out/tests.js"
- :output-dir "out/tests"
- :target :nodejs))
-
- (catch Exception e
- (println "Error on running tests:" e)
- ;; (Thread/sleep 2000)
- (task args)))))
+(t/deftest is-leap
+ (t/is (true? (util/isLeapYear "2024"))))
Next, put some test code in the test/leapyears/test/main.cljs
file:
Now, lets add a new build target to our shadow-cljs.edn configuration file:
(ns leapyears.test.main
- (:require [cljs.test :as t]))
-
-(enable-console-print!)
+{:dev-http {8888 "public"}
-(t/deftest my-first-test
- (t/is (= 1 2)))
+ :dependencies
+ [[funcool/cuerdas "2022.06.16-403"]]
-(set! *main-cli-fn* #(t/run-tests))
+ :source-paths
+ ["src" "test"]
-;; This extension is required for correctly set the return code
-;; depending if the test passes or not.
-(defmethod t/report [:cljs.test/default :end-run-tests]
- [m]
- (if (t/successful? m)
- (set! (.-exitCode js/process) 0)
- (set! (.-exitCode js/process) 1)))
+ :builds
+ {:app
+ {:target :browser
+ :output-dir "public/js"
+ :asset-path "/js"
+ :modules {:main {:entries [mywebapp.main]
+ :init-fn mywebapp.main/main}}}
+ :test
+ {:target :node-test
+ :output-to "target/test.js"
+ :ns-regexp "-test$"
+ :autorun true}}}
The relevant part of that code snippet is:
+Now, lets comple it and run tests:
(t/deftest my-first-test
- (t/is (= 1 2)))
-~~~~
+$ npx shadow-cljs watch test
+shadow-cljs - config: /home/user/playground/mywebapp/shadow-cljs.edn
+shadow-cljs - HTTP server available at http://localhost:8888
+shadow-cljs - server version: 2.26.2 running at http://localhost:9630
+shadow-cljs - nREPL server started on port 40217
+shadow-cljs - watching build :test
+[:test] Configuring build.
+[:test] Compiling ...
The deftest
macro is a basic primitive for defining our tests. It takes a name as
-its first parameter, followed by one or multiple assertions using the is
macro. In
-this example, we try to assert that (= 1 2)
is true.
Let’s try to run this:
-$ clojure tools build:tests
-$ node out/tests.js
-Testing mytestingapp.core-tests
-
-FAIL in (my-first-test) (cljs/test.js:374:14)
-expected: (= 1 2)
- actual: (not (= 1 2))
-
-Ran 1 tests containing 1 assertions.
-1 failures, 0 errors.
-Testing mywebapp.util-test
You can see that the expected assert failure is successfully printed in the
-console. To fix the test, just change the =
with not=
and run the file again:
$ clojure tools build:tests
-$ node out/mytestingapp.js
-
-Testing mytestingapp.core-tests
-
-Ran 1 tests containing 1 assertions.
-0 failures, 0 errors.
-Ran 1 tests containing 1 assertions. +0 failures, 0 errors.
It is fine to test these kinds of assertions, but they are not very -useful. Let’s go to test some application code. For this, we will use a function -to check if a year is a leap year or not:
-(ns leapyears.test.main
- (:require [cljs.test :as t]
- [leapyears.vendor.util-closure :as util]))
-
-;; [...]
-
-(t/deftest my-second-test
- (t/is (util/isLeap 1980))
- (t/is (not (util/isLeap 1981))))
-
-;; [...]
-[:test] Build completed. (52 files, 2 compiled, 0 warnings, 2.38s) +~~
Run the compiled file again to see that there are now two tests running. The -first test passes as before, and our two new leap year tests pass as well.
+It will recompile and rerun the tests on each code change.
One of the peculiarities of ClojureScript is that it runs in an asynchronous, single-threaded execution environment, which has its challenges.
@@ -6270,8 +5651,119 @@This language feature allows different dialects of Clojure to share common code that +is mostly platform independent but need some platform dependent code.
+To use reader conditionals, all you need is to rename your source file with
+.cljs
extension to one with .cljc
, because reader conditionals only work if
+they are placed in files with .cljc
extension.
#?
)There are two types of reader conditionals, standard and splicing. The standard +reader conditional behaves similarly to a traditional cond and the syntax looks +like this:
+(defn parse-int
+ [v]
+ #?(:clj (Integer/parseInt v)
+ :cljs (js/parseInt v)))
+As you can observe, #?
reading macro looks very similar to cond, the difference is
+that the condition is just a keyword that identifies the platform, where :cljs
is
+for ClojureScript and :clj
is for Clojure. The advantage of this approach, is
+that it is evaluated at compile time so no runtime performance overhead introduced for
+using this.
#?@
)The splicing reader conditional works in the same way as the standard but also allows
+splicing lists into the containing form. The #?@
reader macro is used for that
+and the code looks like this:
(defn make-list
+ []
+ (list #?@(:clj [5 6 7 8]
+ :cljs [1 2 3 4])))
+
+;; On ClojureScript
+(make-list)
+;; => (1 2 3 4)
+The ClojureScript compiler will read that code as this:
+(defn make-list
+ []
+ (list 1 2 3 4))
+The splicing reader conditional can’t be used to splice multiple top level forms, +so the following code is illegal:
+#?@(:cljs [(defn func-a [] :a)
+ (defn func-b [] :b)])
+;; => #error "Reader conditional splicing not allowed at the top level."
+If you need so, you can use multiple forms or just use do
block to group
+multiple forms together:
#?(:cljs (defn func-a [] :a))
+#?(:cljs (defn func-b [] :b))
+
+;; Or
+
+#?(:cljs
+ (do
+ (defn func-a [] :a)
+ (defn func-b [] :b)))
+https://danielcompton.net/2015/06/10/clojure-reader-conditionals-by-example
+https://github.com/funcool/cuerdas (example small project that uses +reader conditionals)
+ClojureScript offers a rich vocabulary for data transformation in terms of the sequence abstraction, which makes such transformations highly general and @@ -6352,7 +5844,7 @@
The process of mapping, filtering or mapcatting isn’t necessarily tied to a concrete type, but we keep reimplementing them for different types. Let’s see how we can @@ -6559,7 +6051,7 @@
Some of the ClojureScript core functions like map
, filter
and mapcat
support an arity 1 version that returns a transducer. Let’s revisit our
@@ -6623,7 +6115,7 @@
In the last example we provided an initial value to the transduce
function
([]
) but we can omit it and get the same result:
So far we’ve only seen purely functional transducer; they don’t have any implicit state and are very predictable. However, there are many data @@ -6881,7 +6373,7 @@
Eductions are a way to combine a collection and one or more transformations that can be reduced and iterated over, and that apply the transformations every time @@ -6903,7 +6395,7 @@
We learned about map
, filter
, mapcat
, take
and partition-all
, but there are
a lot more transducers available in ClojureScript. Here is an incomplete list of some
@@ -6929,7 +6421,7 @@
There a few things to consider before writing our own transducers so in this section we will learn how to properly implement one. First of all, we’ve learned @@ -6968,7 +6460,7 @@
A transducible process is any process that is defined in terms of a succession of steps ingesting input values. The source of input varies from one process to @@ -7179,7 +6671,7 @@
Although ClojureScript’s immutable and persistent data structures are reasonably performant there are situations in which we are transforming large data @@ -7327,7 +6819,7 @@
ClojureScript symbols, vars and persistent collections support attaching metadata to them. Metadata is a map with information about the entity it’s @@ -7342,7 +6834,7 @@
Let’s define a var and see what metadata is attached to it by default. Note that this code is executed in a REPL, and thus the metadata of a var defined in a @@ -7437,7 +6929,7 @@
We learned that vars can have metadata and what kind of metadata is added to
them for consumption by the compiler and the cljs.test
testing
@@ -7513,7 +7005,7 @@
The ClojureScript reader has syntactic support for metadata annotations, which can be written in different ways. We can prepend var definitions or collections @@ -7590,7 +7082,7 @@
We’ve learned about meta
and with-meta
so far but ClojureScript offers a few
functions for transforming metadata. There is vary-meta
which is similar to
@@ -7643,14 +7135,14 @@
One of the greatest qualities of the core ClojureScript functions is that they are implemented around protocols. This makes them open to work on any type that we extend with such protocols, be it defined by us or a third party.
As we’ve learned in previous chapters not only functions can be invoked. Vectors are functions of their indexes, maps are functions of their keys and sets are functions @@ -7685,7 +7177,7 @@
For learning about some of the core protocols we’ll define a Pair
type, which will
hold a pair of values.
In a previous section we learned about sequences, one
of ClojureScript’s main abstractions. Remember the first
and rest
functions for
@@ -7802,7 +7294,7 @@
Collection functions are also defined in terms of protocols. For this section examples we will make the native JavaScript string act like a collection.
@@ -7908,7 +7400,7 @@There are many data structures that are associative: they map keys to values. We’ve encountered a few of them already and we know many functions for working with them @@ -8061,7 +7553,7 @@
For checking whether two or more values are equivalent with =
we must implement
the IEquiv
protocol. Let’s do it for our Pair
type:
The meta
and with-meta
functions are also based upon two protocols: IMeta
and
IWithMeta
respectively. We can make our own types capable of carrying metadata
@@ -8163,7 +7655,7 @@
Since ClojureScript is hosted in a JavaScript VM we often need to convert ClojureScript data structures to JavaScript ones and viceversa. We also may want to @@ -8341,7 +7833,7 @@
The reduce
function is based on the IReduce
protocol, which enables us to make
our own or third-party types reducible. Apart from using them with reduce
they
@@ -8441,7 +7933,7 @@
There are some types that have the notion of asynchronous computation, the value they represent may not be realized yet. We can ask whether a value is realized using @@ -8479,7 +7971,7 @@
The ClojureScript state constructs such as the Atom and the Volatile have different
characteristics and semantics, and the operations on them like add-watch
, reset!
@@ -8766,7 +8258,7 @@
In the section about transients we learned about the mutable counterparts of the immutable and persistent data structures that ClojureScript @@ -8868,7 +8360,7 @@
CSP stands for Communicating Sequential Processes, which is a formalism for describing concurrent systems pioneered by C. A. R. Hoare in 1978. It is a @@ -8886,7 +8378,7 @@
core.async
installed to run the examples presented in this section.
Channels are like conveyor belts, we can put and take a single value at a time from them. They can have multiple readers and writers, and they are the fundamental @@ -9331,7 +8823,7 @@
We learned all about channels but there is still a missing piece in the puzzle: processes. Processes are pieces of logic that run independently and use channels @@ -9620,7 +9112,7 @@
Now that we’re acquainted with channels and processes it’s time to explore some
interesting combinators for working with channels that are present in core.async
.
@@ -9955,7 +9447,7 @@
We’ve learned the about the low-level primitives of core.async
and the combinators
that it offers for working with channels. core.async
also offers some useful,
@@ -10386,80 +9878,9 @@