this article originally posted at https://kozieiev.com
Content:
- Creating Clojure version of logger library
- Creating Clojure app to use logger library
- Introducing reader conditionals
- Making the logger library work with ClojureScript
- Creating ClojureScript app to use logger library
Clojure and ClojureScript are forms of the same language targeting different hosts - JVM and JavaScript respectively. If you are creating a library, there is a big chance that a significant part of the code will work for both hosts but a part of it will be host-dependent.
Here we will discuss how to isolate the host-dependent parts of code to be used only when appropriate in order to write a single library that works for both Clojure and ClojureScript.
Steps that we are going to do:
- Create a simple logger Clojure library. Its
(log)
function will print the passed object with added timestamp and information from what language it was invoked. - Create a Clojure app that uses the
logger
library - Modify
logger
library to work with ClojureScript as well - Create a ClojureScript app that uses the
logger
library to check that our modifications worked correctly
Creating Clojure version of logger library
Here is a folder structure for our logger
library:
logger
├── deps.edn
└── src
└── vkjr
└── logger.clj
It’s deps.edn
can be an empty map:
{}
And here is a code of the logger.clj
:
(ns vkjr.logger
(:require [clojure.pprint :as pprint]))
(defn- timestamp []
(.format (java.text.SimpleDateFormat. "d/M/yyyy, HH:mm:ss") (new java.util.Date)))
(defn log [arg]
(pprint/cl-format true
"Time: ~S | Host: ~S | Object: ~S\n"
(timestamp)
"Clojure"
arg))
(timestamp)
is a helper function that uses Java host features to get a formatted timestamp.
(log)
is the function visible to the library users. It takes the user argument and using (cl-format)
prints it prepended with timestamp and language name (”Clojure” in this case).
The first argument of (cl-format)
- true
, means that printing should be done to the default output. You can read more about this function in the official documentation.
Creating Clojure app to use logger library
Now let’s create a Clojure app to use the library. It will be called cljapp
and put on the same lever with the logger:
playground
├── logger <- logger library
└── cljapp <- our new app
Here is a folder structure for cljapp
:
cljapp
├── deps.edn
└── src
└── core.clj
In deps.edn
we’ll reference logger
library by location on the filesystem:
{:deps {vkjr/logger {:local/root "../logger"}}}
And here is the code inside core.clj
:
(ns core
(:require [vkjr.logger :as logger]))
(defn -main [& _]
(logger/log "Hi there!")
(logger/log {:a 1 :b 2})
(logger/log [1 2 3 4]))
We required the namespace of the logger library and used (logger/log)
inside the (main)
to print different arguments.
Now let’s run the main function using Clojure CLI (from cljapp
folder) to make sure it works correctly:
$ clj -M -m core
Time: "18/8/2022, 16:39:30" | Host: "Clojure" | Object: "Hi there!"
Time: "18/8/2022, 16:39:30" | Host: "Clojure" | Object: {:a 1, :b 2}
Time: "18/8/2022, 16:39:30" | Host: "Clojure" | Object: [1 2 3 4]
Nice, as we see, it does)
Introducing reader conditionals
There is a part of Clojure tooling called Reader. It takes a textual code representation and turns it into the Clojure data structures. When you compile Clojure code, Reader will be responsible for processing your sources.
Reader supports two reader conditionals which allow you to specify different pieces of code and choose between them depending on the platform where the reader is invoked.
The standard reader starts with #?
and looks like:
#?(:clj (any Clojure expression)
:cljs (any ClojureScript expression)
:default (default expression))
When Reader encounters such a conditional, it will leave only one expression in the result data structure - the one corresponding to the current host or the default one if the current host is not listed.
So after reading this code:
#?(:clj (+ 1 2) :cljs (+ 3 4))
On ClojureScript host Reader will return this datastructure:
(+ 3 4)
The splicing reader starts with #?@
and looks like this:
#?@(:clj [vector of elements]
:cljs [another vector of elements])
When it encountered, Reader will choose the vector depending on the host and will put the content of vector in the surrounding context. Not the vector itself! It’s content.
And after reading this code:
(print #?@(:clj [1 2] :cljs [3 4]))
on Clojure platform Reader will return the datastructure:
(print 1 2)
Note: in the source code reader conditionals work only in files with *.cljc
file extension!
To grasp reader conditionals better you can experiment in REPL by feeding different code pieces to the read-string
function (with {:read-cond :allow}
as a first argument) and inspecting the output.
$ clj <- run repl
user=> (read-string {:read-cond :allow} "#?(:clj (+ 1 2) :cljs (+ 3 4))")
(+ 1 2)
Making the logger library work with ClojureScript
Now with all that knowledge about reader conditionals, it is time to revamp logger
to make it work for ClojureScript.
First, we need to rename logger.clj
→ logger.cljc
to enable reader conditionals.
Folder structure now:
logger
├── deps.edn
└── src
└── vkjr
└── logger.cljc
Next we need to add ClojureScript-related code in (comment)
function in logger.cljc
. It will be wrapped with standard reader conditional:
(defn- timestamp []
#?(:clj
(.format (java.text.SimpleDateFormat. "d/M/yyyy, HH:mm:ss") (new java.util.Date))
:cljs
(let [now (new js/Date)]
(.toLocaleString now "en-US" #js{:hour12 false}))))
And as the last step, we modify (log)
function to display the correct language name depending on the host. We use splicing reader conditional on doing this:
(defn log [arg]
(pprint/cl-format true
"Time: ~S | Host: ~S | Object: ~S\n"
(timestamp)
#?@(:clj ["Clojure"]
:cljs ["ClojureScript"])
arg))
Full content of logger.cljc
now:
(ns vkjr.logger
(:require [clojure.pprint :as pprint]))
(defn- timestamp []
#?(:clj
(.format (java.text.SimpleDateFormat. "d/M/yyyy, HH:mm:ss") (new java.util.Date))
:cljs
(let [now (new js/Date)]
(.toLocaleString now "en-US" #js{:hour12 false}))))
(defn log [arg]
(pprint/cl-format true
"Time: ~S | Host: ~S | Object: ~S\n"
(timestamp)
#?@(:clj ["Clojure"]
:cljs ["ClojureScript"])
arg))
Now we need to check that changes didn’t affect the work of existing cljapp
Calling core namespace again from cljapp
folder:
$ clj -M -m core
Time: "18/8/2022, 16:50:39" | Host: "Clojure" | Object: "Hi there!"
Time: "18/8/2022, 16:50:39" | Host: "Clojure" | Object: {:a 1, :b 2}
Time: "18/8/2022, 16:50:39" | Host: "Clojure" | Object: [1 2 3 4]
Creating ClojureScript app to use logger library
And finally, we need to check that the library also works for the ClojureScript project.
Let’s create one, called cljsapp
on the same level as logger
and cljapp
:
playground
├── logger
├── cljsapp <- ClojureScript app
└── cljapp
Project structure:
cljsapp
├── deps.edn
└── src
└── core.cljs
deps.edn
content:
{:deps {org.clojure/clojurescript {:mvn/version "1.11.60"}
vkjr/logger {:local/root "../logger"}}}
core.cljs
content:
(ns core
(:require [vkjr.logger :as logger]))
(defn -main [& _]
(logger/log "Hi there!")
(logger/log {:a 1 :b 2})
(logger/log [1 2 3 4])
(logger/log (new js/Date)))
And the actual check using Clojure CLI:
clj -M -m cljs.main -re node -m core
Time: "8/19/2022, 13:45:03" | Lang: "ClojureScript" | Object: "Hi there!"
Time: "8/19/2022, 13:45:03" | Lang: "ClojureScript" | Object: {:a 1, :b 2}
Time: "8/19/2022, 13:45:03" | Lang: "ClojureScript" | Object: [1 2 3 4]
Time: "8/19/2022, 13:45:03" | Lang: "ClojureScript" | Object: #inst "2022-08-19T12:45:03.775-00:00"
Perfect, now we have a library that works for both Clojure and ClojureScript :)
Top comments (0)