How to create a library that works with Clojure and ClojureScript
Tags: clojure, clojurescript
Published: 2022-08-21 16:14:54


{% embed https://youtu.be/KVWxr9dvGZ8 %}
this article originally posted at [https://kozieiev.com](https://kozieiev.com/blog/how-to-create-library-for-clojure-and-clojurescript/)

## Content:
- [Creating Clojure version of logger library](#creating-clojure-version-of-logger-library)
- [Creating Clojure app to use logger library](#creating-clojure-app-to-use-logger-library)
- [Introducing reader conditionals](#introducing-reader-conditionals)
- [Making the logger library work with ClojureScript](#making-the-logger-library-work-with-clojurescript)
- [Creating ClojureScript app to use logger library](#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:**

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:

```bash
logger
├── deps.edn
└── src
    └── vkjr
        └── logger.clj
```

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

```clojure
{}
```

And here is a code of the `logger.clj`:

```clojure
(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](https://clojuredocs.org/clojure.pprint/cl-format). 

## 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:

```bash
playground
├── logger      <- logger library
└── cljapp      <- our new app
```

Here is a folder structure for `cljapp`:

```bash
cljapp
├── deps.edn
└── src
    └── core.clj
```

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

```clojure
{:deps {vkjr/logger {:local/root "../logger"}}}
```

And here is the code inside `core.clj`:

```clojure
(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:

```bash
$ 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:

```clojure
#?(: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:

```clojure
#?(:clj (+ 1 2) :cljs (+ 3 4))
```

On ClojureScript host Reader will return this datastructure:

```clojure
(+ 3 4)
```

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

```clojure
#?@(: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:

```clojure
(print #?@(:clj [1 2] :cljs [3 4]))
```

on Clojure platform Reader will return the datastructure:

```clojure
(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. 

```bash
$ 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:

```bash
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:

```clojure
(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:

```clojure
(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:

```clojure
(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:

```bash
$ 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`:

```bash
playground
├── logger
├── cljsapp   <- ClojureScript app
└── cljapp      
```

Project structure:

```bash
cljsapp
├── deps.edn
└── src
    └── core.cljs
```

`deps.edn` content:

```clojure
{:deps {org.clojure/clojurescript {:mvn/version "1.11.60"}
        vkjr/logger {:local/root "../logger"}}}
```

`core.cljs` content:

```clojure
(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:

```bash
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 :)



### Links

[Complete code for on github](https://github.com/vkjr/code-examples/tree/main/005-crosshost-library-example)

[Official documentation on reader conditionals](https://clojure.org/guides/reader_conditionals)