Functional Bytes Clojure, Scala en Java specialist

Introducing lifted

Jun 11, 2020 • Arnout Roemers

Firstly, Clojure is a dynamicly typed language. Being dynamic, having unit tests is even more important than in statically typed languages. Secondly, Clojure is mostly revolving around functions. To support polymorphism, these functions can be declared inside a protocol.

You probably already knew that. I mention it though, because I like functions over protocol implementations. It fits my REPL-oriented workflow better and I find it easier to just churn out functions, without having to think about how it could be “abstracted” into a protocol yet. As long as one develops the functions to not refer to global state, this is not an issue. By doing this, you will find that it is rather easy to transform them into a protocol if necessary.

This necessity is often seen at the boundaries of an application, i.e. the IO parts, such as database connections and REST clients. This is because unit tests often need test or mock variants of these boundaries. Protocols let you do that, again, as long as your functions take these protocol implementations as an argument.

When I introduce a protocol in my projects for said boundaries, I still dislike implementing it in the protocol implementation itself. I try to resort to functions as soon as possible. In many cases this leads to protocol implementations that just forward invocations to plain functions. In that case the protocol itself is also just repeating the plain function names, albeit slightly different.

For the sake of this blog post, let’s build a basic jsonbox.io client:

(defn- rest-call [{:keys [box api-key]} method path data]
  (-> {:method       method
       :url          (str "https://jsonbox.io/" box "/" path)
       :form-params  data
       :content-type :json
       :as           :json
       :headers      {"Authorization" (str "API-KEY " api-key)}}
      http/request
      :body))

(defn create
  "Create a new record in the given collection. Returns the
  ID of the new record."
  [client collection record]
  (:_id (rest-call client :post collection record)))

(defn read-by-id
  "Retrieve a record by ID from the given collection."
  [client collection record-id]
  (rest-call client :get (str collection "/" record-id) nil))

(defn read-all
  "Retrieve all records from the given collection."
  [client collection]
  (rest-call client :get collection nil))

;;; Using the API

(def config
  {:jsonbox {:box     "box_5d94c4843b556f8b4c23"
             :api-key "7b3b910b-a7ad-41e8-89d6-5e28e2e34e70"}})

(def jsonbox (:jsonbox config))

(create jsonbox "users" {:name "Alice"})

As you can see, all the functions take a “client” as its first parameter. In this case, it is simply a map with a box name and a api-key secret. This “production” code works fine as it is and is also easily lifted into a protocol. A choice has to be made on what level to put this protocol however, i.e. at the level of rest-call or at the level of the public functions. Both are valid choices and it depends a bit on your personal taste and your use case. Let’s go for the latter choice, and see what it looks like if we make this protocol and a forwarding protocol implementation.

(defprotocol JsonBox
  (create [this collection record]
    "Create a new record in the given collection. Returns the
    ID of the new record.")

  (get-by-id [this collection record-id]
    "Retrieve a record by ID from the given collection.")

  (get-all [this collection]
    "Retrieve all records from the given collection."))

(defrecord JsonBoxImpl [box api-key]
  JsonBox
  (create [this collection record]
    (-create this collection record))
  (get-by-id [this collection record-id]
    (-get-by-id this collection record-id))
  (get-all [this collection]
    (-get-all this collection)))

;;; Using the API

(def jsonbox
  (map->JsonBoxImpl (:jsonbox config)))

Note that there is a lot of repetition here. Also note that for this to work, the original functions had to be renamed a bit; they are now prefixed with a dash. Still, the invocation of the API is still the same, i.e. (create jsonbox "users" {:name "Alice"}) still looks and works the same.

But this is Clojure we’re talking about, and Clojure has macros. Why not let Clojure create these “forwarding” protocol implementations for me? This is exactly what I did in more than one project. Instead of implementing it again, I’ve now put it in a small library: lifted.

Introducing: lifted

The library is quite small, it only offers two macros and a function.

(require '[lifted.core :refer [lift-as lift-on lifted]])

The lift-as macro defines a protocol. Its sole argument is the name of the protocol to declare. The macro “lifts” the functions in the current namespace which names are prefixed with the - character into the protocol. The prefix is stripped from the protocol function names. Only those functions which take at least one argument are lifted.

So instead of writing the JsonBox protocol ourselves, we can write the following instead:

(lift-as JsonBox)

Next, the lift-on macro creates a forwarding protocol implementation. Its two arguments are the protocol to implement and the object to use as this. The returned implementation simply forwards the calls to the prefixed functions, passing the “this” object as the first argument.

So instead of the writing the JsonBoxImpl record, we can write the following instead:

(def jsonbox (lift-on JsonBox (:jsonbox config)))

Finally, there is the lifted function. This simply returns the “this” object that was passed to a lift-on protocol implementation. If called on the jsonbox from the former example, one would see this:

(lifted jsonbox)
;=> {:box     "box_5d94c4843b556f8b4c23"
;=>  :api-key "7b3b910b-a7ad-41e8-89d6-5e28e2e34e70"}

And there you have it. The production code is still written in plain functions. Testing those plain functions individually has not changed either. Yet, adding two lines lifted it into a protocol and created a “production” implementation of it. The invocation of the functions has not changed, but it did open the door for creating test, mock or other variants of it. In some cases these variants can also be created using lift-on, if it is only the “this” parameter that needs to be different.

I hope you find this as useful as I do. You can find more documentation here.

As always, enjoy!

P.S. The config and jsonbox global bindings in the examples are not required of course and maybe even undesirable depending on your stance on global state; they are here for demo purposes.


Clojure - Scala - Java - JavaEE - Datomic - Reagent - Figwheel - HugSQL - JavaScript - Node.js - Maven - SBT - XML - XSD - XSLT - JSON - jQuery - HTML - HTMX - React - Redux - OAuth - REST - GraphQL - ZooKeeper - Kafka - Akka HTTP - PostgreSQL - ElasticSearch - Cassandra - Redis - Mule - RabbitMQ - MQTT - SOAP - Linux - macOS - Git - Scrum - Emacs - Docker - Kubernetes - Ansible - Terraform - Jenkins - GitHub - GitLab - Devops - Raspberry Pi - Event Sourcing - Functional Reactive Programming - Ports and Adapters (Hexagonal)


Functional Bytes, 2013-2024

Boekelo

06 267 145 02

KvK: 59562722

Algemene voorwaarden