Logo

dev-resources.site

for different kinds of informations.

How to create a library that works with Clojure and ClojureScript

Published at
8/21/2022
Categories
clojure
clojurescript
Author
kozieiev
Categories
2 categories in total
clojure
open
clojurescript
open
Author
8 person written this
kozieiev
open
How to create a library that works with Clojure and ClojureScript


this article originally posted at https://kozieiev.com

Content:

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:

  1. 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.
  2. Create a Clojure app that uses the logger library
  3. Modify logger library to work with ClojureScript as well
  4. 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
Enter fullscreen mode Exit fullscreen mode

It’s deps.edn can be an empty map:

{}
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

Here is a folder structure for cljapp:

cljapp
β”œβ”€β”€ deps.edn
└── src
    └── core.clj
Enter fullscreen mode Exit fullscreen mode

In deps.edn we’ll reference logger library by location on the filesystem:

{:deps {vkjr/logger {:local/root "../logger"}}}
Enter fullscreen mode Exit fullscreen mode

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]))
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

On ClojureScript host Reader will return this datastructure:

(+ 3 4)
Enter fullscreen mode Exit fullscreen mode

The splicing reader starts with #?@ and looks like this:

#?@(:clj  [vector of elements]
    :cljs [another vector of elements])
Enter fullscreen mode Exit fullscreen mode

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]))
Enter fullscreen mode Exit fullscreen mode

on Clojure platform Reader will return the datastructure:

(print 1 2)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}))))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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      
Enter fullscreen mode Exit fullscreen mode

Project structure:

cljsapp
β”œβ”€β”€ deps.edn
└── src
    └── core.cljs
Enter fullscreen mode Exit fullscreen mode

deps.edn content:

{:deps {org.clojure/clojurescript {:mvn/version "1.11.60"}
        vkjr/logger {:local/root "../logger"}}}
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Perfect, now we have a library that works for both Clojure and ClojureScript :)

Links

Complete code for on github

Official documentation on reader conditionals

clojurescript Article's
30 articles in total
Favicon
Querido Yo del Futuro: Hoy intentaremos configurar una aplicaciΓ³n fullstack en Clojure
Favicon
Why I chose Clojure/Script for building Vade Studio
Favicon
Converting JS Libraries to Clojure: Part 1
Favicon
Deploy your ClojureScript App to Cloudflare Workers
Favicon
shadow-cljs and running tests
Favicon
Giving new life to existing Om legacy SPAs with re-om
Favicon
Is Clojure the only language you need?
Favicon
Building an Application with ClojureScript
Favicon
How to Set up A Clojure Script and Phoenix Project
Favicon
How to create a library that works with Clojure and ClojureScript
Favicon
Setup shadow-cljs react project
Favicon
Logging readable ClojureScript (.cljs) errors to sentry!!
Favicon
How can I create a ClojureScript web app from scratch with Reagent and npm?
Favicon
Set up SSL/TLS for shadow-cljs https server
Favicon
ClojureScript on Cloudflare Workers
Favicon
Storybook.JS with Shadow-CLJS
Favicon
World Statistics Exercise
Favicon
The Pleasure of Clojure(Script): Part 1
Favicon
Using Specter on tree data structures in Clojure
Favicon
Clojure Re-Frame Exercise
Favicon
ΒΏPor quΓ© Clojure?
Favicon
Implementing the feed
Favicon
Try something new this week
Favicon
ClojureScript async MVU
Favicon
ClojureScript simple MVU loop
Favicon
Integrating ClojureScript with JavaScript tooling
Favicon
Understanding Transducers in JavaScript
Favicon
Casting visual spells with p5.js and ClojureScript, part 1
Favicon
ClojureScript REPL Workflow
Favicon
Developing, Testing and Deploying AWS Lambda Functions written in ClojureScript

Featured ones: