Few times I feel inclined to answer for a post. There are actually lots of posts that say that Clojure needs “the web framework” like Rails or Django or Phoenix so it’ll be better. Sometimes these ideas have value, sometimes don’t, and sometimes they are simply nonsense at all.

I recently was shown a post that was basically nonsense, and to keep things neutral, I’m not going to link it here. I want to expose why I think these ideas are nonsense, and how a person that’s new to Clojure could think about this lack of frameworks, how to handle this situation, and what to expect from the language.

So, first things first: Clojure is made to be a hosted language. Which means it expects you to use Java libraries, or Javascript ones, if you’re in ClojureScript. Sure, in Javascript things are more difficult mostly because of the weird ecosystem that expects everything to be transpiled first, but most of the time you can use interop just fine.

All this to say: you don’t need to wrap libraries. Most people I know, me included, said sometime that Clojure needs more libraries that wrap over other other things, and while I still would love to just stay on “Clojure-land” most of the time, it’s better to have a stable Java library that we can use over Clojure than an old, unstable, and severely lacking behind features that wrap the Java one to have a “Clojure feel”. Most of the time, you can have a “Clojure feel” with few lines of code anyway.

So, it’s hard to back up these with only words, so let’s use some examples: I’ll use Kafka Parallel Consumer to show the same code in Clojure. The code basically reads like this:

Consumer<String, String> kafkaConsumer = getKafkaConsumer(); // (1)
Producer<String, String> kafkaProducer = getKafkaProducer();

var options = ParallelConsumerOptions.<String, String>builder()
        .ordering(KEY)
        .maxConcurrency(1000)
        .consumer(kafkaConsumer)
        .producer(kafkaProducer)
        .build();

ParallelStreamProcessor<String, String> eosStreamProcessor =
        ParallelStreamProcessor.createEosStreamProcessor(options);

eosStreamProcessor.subscribe(of(inputTopic));

return eosStreamProcessor;

So, how is this on Clojure?

(let [kafka-consumer (get-kafka-consumer)
      kafka-producer (get-kafka-producer)

      options (.. (ParallelConsumerOptions/builder)
                  (ordering ordering ParallelConsumerOptions$ProcessingOrder/KEY)
                  (maxConcurrency 1000))
                  (consumer kafka-consumer)
                  (producer kafka-producer)
                  build]

  (doto (ParallelStreamProcessor/createEosStreamProcessor options)
        (.subscribe [input-topic])))

Yes. That is it. How “clumsy” is this? Answering: with the exception of ParallelConsumerOptions$ProcessingOrder/KEY, nothing at all. It actually uses less characters, less lines, less dots, and less parenthesis. Also, uses one less library (the of(inputTopic) uses a library to easily generate an immutable Java List, and guess what? Clojure’s vectors are immutable Java Lists).

Then comes the moment to poll the topics – it uses a Java 11 API that’s not available to Clojure, so in Java it is simple:

parallelConsumer.poll(record ->
        log.info("Concurrently processing a record: {}", record)
);

And in Clojure, we need to wrap this:

(.poll parallel-consumer 
       (reify java.util.function.Consumer 
         (accept [_ record] 
            (println "Concurrently processing a record: " record))))

So, how can we fix this? Either with a function that wraps up the Consumer, or a macro:

(defn- ^Consumer from-consumer [f]
  (reify java.util.function.Consumer (accept [_ t] (f t))))

(.pool parallel-consumer
  (from-consumer (fn [record] (println "Concurrently processing a record: " record)))

;; or
(defmacro consumer-fn [args & body]
   `(reify java.util.function.Consumer (accept [_ ~@args] @~body)))

(.pool parallel-consumer
  (consumer-fn [record] (println "Concurrently processing a record: " record))

Any of these work, and you only have to write your wrapper once – then everything works as expected.

So, why no framework?

Honestly, what should a framework in Clojure look like? Let's take inspiration from Rails, for example, and do some experiments: let's start with the famous "15-minute blog" in Rails and translate the code to Clojure; it's tricky because Rails relies heavily on mutability (including of classes itself) and class-based object oriented programming (and we have no easy way to make these work on Clojure). So in a way, we could make builders:

;; Model:
(def post
  (-> (table-for "posts")
      (validates-presence-of :body :title)))

;; Controller:
(defn index []
  (let [posts (ar/all post)])

(defn create [params]
  (let [post (ar/new post params)]
    (if (ar/save post)
      (controller/redirect-to post)
      (controller/render :new {:status :unprocessable-entity}))))

(def posts-controller
  (-> controller
      (action :index index)
      (action :create create)))

Now, we have some problems: Clojure (thankfully!!!) is not as dynamic and magical as Ruby, so we can’t actually expect things like the local variables posts to magically appear as local variables on the renderer. We may be able to work on a solution, like (expose :posts (ar/all post)) but there’s another catch: this will generate a global-ish thing that’s not REPL-friendly, and most Clojure developers will not want to loose their REPL powers (me included). So let’s try to handle this differently: Clojure have this idea of using collections for everything. So, how would the code above be, if we use maps instead? Well, little things would change, if we keep the same API… instead of -> we may use {:table "posts" :validates {:presence [:body :title]}} but that’s mostly it.

Now, what about a Clojure way? Maybe receive maps as params, return maps as rendering instructions, and query things with maps instead of ActiveRecord-like things?

(defn- index [{:keys [db]}]
  (let [posts (query db {:from ["posts" :select [:*]]})]
    {:status 200
     :body (render-index {:record posts})}))

(defn- create [{:keys [db params]}]
  (let [{keys [success errors]} (coerce params)]
    (if success
      {:status 302
       :headers {"Location"
                 (str "/posts/"
                      (->> {:insert-into "posts" :values [success]}
                           (execute! db)
                           :id))}}
      {:status 422
       :body (render-new {:record params :errors errors})})))

Sounds interesting? Is it possible to make it work? Want to make this framework? Fortunately, it already exists. You query with next/jdbc, you handle SQL transformation with HoneySQL, and the web handler is Pedestal. You can inject database with Pedestal interceptors, and you can coerce data and handle validation errors with Malli.

With a few “helper functions” it can feel like a Rails framework indeed:

(defn- index [{:keys [db]}]
  (let [posts (query db {:from ["posts" :select [:*]]})]
    (render :posts/index posts)))

(defn- create [{:keys [db params]}]
  (let [{keys [success errors]} (coerce params)]
    (if success
      (->> {:insert-into "posts" :values [success]}
           (execute! db)
           (redirect-to :posts/resource))
      (render-error :posts/new params errors))))

These render functions just need to create the right Clojure maps for the rendering, and that’s it. It’s so trivial to write that some people simply don’t do it, because it’ll obscure what the handler returns for little gain (maybe 2 or 3 less lines of code per handler).

The thing is: if you’re scaffolding an app, Rails/Django/Whatever is faster. But when the app grows, adding functionality and making it conform with what Rails/Django/Whatever want, specially if the library is not made to integrate with the “framework’s way” is actually quite difficult. Examples are many: websockets in Rails need Redis and have abysmal performance, and if you want to use messaging systems and somehow “react” to messages on Rails (with server-push or something) things get weird really fast because you need to use a background job that basically duplicates you DB connections, have its own process and memory, etc… all this because the “library” that you are trying to use does not integrate well with Rails way…

Where in Clojure, as you’re passing maps and vectors, things simply work – even when you don’t expect them to work together…

Categories: Clojure