~20m skim, 4,119 words, updated Jan 13, 2025
Enterprise grade magick.
Programming is tough. Especially at first.
Whatever you are learning or writing, keep in mind that every language was created with a set of principles and use cases in mind - some more than others.
Learn to reject your ego. Learn to love burning code.
Name things what they are. Begin with your program’s final state in mind!
Video: How startups can move fast with Clojure (by Bradford Cross)
Startups should focus on:
Rebuilding things from scratch sucks and writing stateless functions that transform data potentially enables better code reuse.
Programmers know the benefits of everything and the tradeoffs of nothing
– Rich Hickey
I’ve got a particular friend who has an obsession with Clojure - he is baffled as to why the entire world doesn’t use it. When asked, he may say something like “they’re all idiots!” While no sage myself, I do believe that the mentors I have had have wisely guided me regarding tool choice, and a variety of factors must be considered along with a problem statement.
First, if you don’t know: Video: What is the Curse of Lisp?
The curse of LISP is that it is too powerful, promoting hyper customized solutions and individualism. A solo developer now has the power to bend and flex reality to his whims. The drawback is that this causes fewer common tools to emerge - developers can just build them themselves fairly easily, leading to a divergence of methods.
Corporations would not want to enable this - preferring replaceable cogs. This is detailed in the intro to Simply Scheme , and below. The Corporations have a very valid point here that must be stressed: The goal is to reliably get real work done, not to navel gaze.
I’ve heard rumours that Clojure subverts this curse with two measures: understanding of the problem, and the ability to leverage Java libraries. We’re giving it a go regardless, as I want to work in a LISP for a minute.
The the intro to Simply Scheme provides caricatures of the two major schools of thought regarding the teaching of computer science:
1 - The conservative view: Computer programs have become too large and complex to encompass in a human mind. Therefore, the job of computer science education is to teach people how to discipline their work in such a way that 500 mediocre programmers can join together and produce a program that correctly meets its specification.
2 - The radical view: Computer programs have become too large and complex to encompass in a human mind. Therefore, the job of computer science education is to teach people how to expand their minds so that the programs can fit, by learning to think in a vocabulary of larger, more powerful, more flexible ideas than the obvious ones. Each unit of programming thought must have a big payoff in the capabilities of the program.
This is posted here for the reader to ponder without comment.
I’ll be using org-babel-clojure to write and run code within this manual directly. Learning, remembering, and teaching now mix. This approach is one of Donald Knuth’s methodologies1
The Literate programming idea has regained popularity these days in the form of data notebooks, and it is certainly to my taste.
If the evaluated result is simple, it’ll have a little arrow ‘=>’ beside it in the css, though not in the text document on disk.
Like so:
(+ 1 2 3 4 5)
;; => 15
By integrating
ox-hugo
I have been able to somewhat improve the
default results output and provide scalar value
results, which render
much nicer than the tables used by Hugo by default. If that didn’t
make sense, no worries - I’m just happy that the result below looks
like a lisp list rather than a table.
(vals {:a 1 :b 2})
;; => (1 2)
Full talk: infoq.com/presentations/clojure-core-async/
In the smallest nutshell, here’s how you can hit the ground running with Clojure.
Types
123 ; number (long)
"a string"
:keywords
'symbols
Data Structures
'(1 2 3) ; list
[1 2 3] ; vector
#{1 2 3} ; set
{:a 1, :b 2} ; map
It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.
– Alan Perlis2
Syntax
(operator operand operand operand)
“All Clojure operations have the same syntax: opening parenthesis, operator, operands, closing parenthesis”
– Daniel Higginbotham3
In this guide, I’ll be including libraries as-used with the `require` function as needed.
(require '[clj-http.client :as client])
In your projects, you’ll need to use a project/dependency manager like lein or deps to download dependencies and make them available in your Clojure project and REPL. After installing dependencies, they can be included within your Clojure namespaces like so:
(ns my-app.core
(:require [clj-http.client :as client]
[my-app.readers :refer [rss qr-img]]
[my-app.nuclear :as n]
[my-app.platform.sidewinder :as sw]))
This manual does load a few libraries by default, but generally I will use the former requirement format when demonstrating the use of a new library.
It is easy to fetch data using the clj-http library.
(require '[clj-http.client :as client])
(client/head "https://ryanfleck.ca")
;; Result:
'(:cached :request-time 197 :repeatable? false
:protocol-version (:name "HTTP" :major 1 :minor 1)
;; ... more stuff ...
:headers ("referrer-policy" "strict-origin-when-cross-origin"
"Server" "cloudflare"
"Content-Type" "text/html; charset=utf-8"
"x-content-type-options" "nosniff" "alt-svc" "h3=\":443\"; ma=86400"
"NEL" "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"
"Connection" "close" "cf-cache-status" "DYNAMIC" "CF-RAY" "8fedb5dbee3cebbe-SEA"
;; ... more stuff ...
"Cache-Control" "public, max-age=0, must-revalidate")
:orig-content-encoding "gzip" :status 200
:length 0 :body :trace-redirects ())
The amazing group-by
function allows you to group data by a common
key. My use case for this was grouping articles in different
languages collected over time. Here’s what the incoming data looked
like:
{:count 260, :hour 2025-01-07T21:00, :language "bn"}
{:count 100, :hour 2025-01-07T21:00, :language "de"}
{:count 1041, :hour 2025-01-07T21:00, :language "es"}
{:count 211, :hour 2025-01-07T21:00, :language "fa"}
{:count 1, :hour 2025-01-07T21:00, :language "fi"}
{:count 268, :hour 2025-01-07T21:00, :language "fr"}
{:count 63, :hour 2025-01-07T21:00, :language "gu"}
;; ... data truncated ...
Here is how the data looks after using group-by:
(group-by :language (db/get-items-by-hour-72h-langs))
{"nl" [{:count 3, :hour #object[java.time.LocalDateTime 0x2e063d23 "2025-01-07T21:00"], :language "nl"}
{:count 2, :hour #object[java.time.LocalDateTime 0x5080c1d3 "2025-01-09T11:00"], :language "nl"}
{:count 1, :hour #object[java.time.LocalDateTime 0x2cef6527 "2025-01-09T21:00"], :language "nl"}],
"pt" [{:count 188, :hour #object[java.time.LocalDateTime 0x6e9352c2 "2025-01-07T21:00"], :language "pt"}
{:count 175, :hour #object[java.time.LocalDateTime 0x41f9af3f "2025-01-08T11:00"], :language "pt"}
{:count 62, :hour #object[java.time.LocalDateTime 0x71df170a "2025-01-09T15:00"], :language "pt"}
{:count 96, :hour #object[java.time.LocalDateTime 0x58aa8fa8 "2025-01-09T21:00"], :language "pt"}],
"en" [{:count 4412, :hour #object[java.time.LocalDateTime 0x74f18d18 "2025-01-07T21:00"], :language "en"}
{:count 2552, :hour #object[java.time.LocalDateTime 0x3fd9a0d6 "2025-01-09T11:00"], :language "en"}
{:count 227, :hour #object[java.time.LocalDateTime 0x6fa4cc34 "2025-01-09T13:00"], :language "en"}
{:count 856, :hour #object[java.time.LocalDateTime 0x4a64b22a "2025-01-09T21:00"], :language "en"}],
"ur" [{:count 100, :hour #object[java.time.LocalDateTime 0x552a7e60 "2025-01-07T21:00"], :language "ur"}
;; ... data truncated ...
We can use core.memoize to cache values with a variety of methods.
(defn get-72h-data []
(let [data (c/extract-series {:x :hour :y :count} (db/get-items-by-hour-72h))
series {"Collected Items" [(map #(localDateTime->Date %) (:x data)) (:y data)]}]
series))
(def one-minute-in-ms (* 60 1000))
(def get-72h-data-memoized (memo/ttl get-72h-data {} :ttl/threshold one-minute-in-ms))
By calling the variable we have defined, we can see the dramatic reduction in time on the second execution.
(time (get-72h-data-memoized)) ; => "Elapsed time: 17.726885 msecs"
(time (get-72h-data-memoized)) ; => "Elapsed time: 0.05838 msecs"
Here’s a short guide on one method of many to render charts in web
apps. The Apache echarts
library has a
getting started
graph we can
use as an example.
(defn get-72h-echart-body []
(log/debug "Attempting to return HTML for new EChart")
(parser/render-file "graphs/72h-echart.html"
{:width 800
:height 500
:data {:title {:text "ECharts Getting Started Example"}
:tooltip {}
:legend {:data ["sales"]}
:xAxis {:data ["Shirts" "Cardigans" "Chiffons" "Pants" "Heels" "Socks"]}
:yAxis {}
:series [{:name "Sales" :type "bar" :data [5 20 36 10 10 20]}]}}))
Using Selmer with the template below yields the same chart as the one in the demo.
<h3>72h Echart</h3>
<div id="72h-echart-main" style="width: {{ width }}px; height: {{ height }}px;"></div>
<script type="text/javascript">
// Initialize the echarts instance based on the prepared dom
var myChart = echarts.init(document.getElementById('72h-echart-main'));
// Specify the configuration items and data for the chart
var option = {{ data|json|safe }};
// Display the chart using the configuration items and data just specified.
myChart.setOption(option);
</script>
I realized that this library is fairly popular in the Clojure community after discovering it myself, and for good reason - using Echarts is a highly data-driven experience that requires only a touch of client javascript and can otherwise be entirely created with Clojure data structures. Here are some other places Apache Echarts are used:
Using HTMX provides a myriad of benefits and few drawbacks.
You do need to write a backend that returns html, as text, and not JSON. The benefit of this - you don’t need to spend an additional few steps transforming and interpreting JSON on your frontend.
Load and swap out something small, like a clock, every second:
<span hx-get="/api/now" hx-trigger="load, every 1s" hx-swap="innerHTML"></span>
On the backend, this is the code - essentially we just send back some
text in a span
.
["/now"
{:get {:summary "returns the current time as a span"
:responses {200 {:body string?}}
:headers {"Content-Type" "text/html"}
:handler (fn [_params]
{:status 200
:body (str "<span>" (time/dateline-utc) "</span>")})}}]
Notably on hx-swap
any scripts included on a page won’t run, but those
included during an hx-get
will be as long as they aren’t in the root
in which case they will be commented out. I learned this the hard way.
<!-- *None* of these will run if a page is just swapped in. -->
<div>
<script>console.log("in a div, this runs.");</script>
</div>
<span>
<script>console.log("in a span, this runs.");</script>
</span>
<script>console.log("outside, this runs.");</script>
All quotes in this section are from this material.
Do Things: A Clojure Crash Course
Clojure uses the familiar LISP S-Expressions. Literals are valid forms - each of these will just return itself. All of these types build off common Java primitives and data structures.
1
"a string"
["a" "vector" "of" "strings"]
{ :a "map" :of "stuff"}
Clojure uses whitespace to separate operands, and it treats commas as whitespace.
Good old s-expressions:
(operator operand1 operand2 etc)
Clojure’s structural uniformity is probably different from what you’re used to. In other languages, different operations might have different structures depending on the operator and the operands. For example, JavaScript employs a smorgasbord of infix notation, dot operators, and parentheses. Clojure’s structure is very simple and consistent by comparison. […] No matter which operator you’re using or what kind of data you’re operating on, the structure is the same.
Key Functions:
(def boolean-value true)
(if boolean-value "It's true!" "Lol nope") ; "It's true!"
(when boolean-value "Yes") ; "Yes"
(when-not boolean-value "Nope")
When and when-not enable execution of a form when a value is true (or false for when-not) without providing a false-case like an if statement.
Do enables the combination of multiple forms - it will return the result of the final form. This is very useful for logging or running multiple simple statements within an if expression.
(do (+ 1 2) (+ 3 4) (+ 5 6))
;; => 11
Key concepts:
nil
and false
are both interpreted as false(nil? 1) ;; => false
(nil? nil) ;; => true
(true? true) ;; => true
(false? true) ;; => false
(true? nil) ;; => false - nil is falsey
Or returns the first truthy value or the last value:
(or nil false :cry :rage :fight :death)
;; => :cry
And returns the first falsey value or the last truthy value:
(and true 123 :kick :drown false)
;; => false
Use clojure.core/def to bind names in Clojure.
Notice that I’m using the term bind, whereas in other languages you’d say you’re assigning a value to a variable. Those other languages typically encourage you to perform multiple assignments to the same variable.
However, changing the value associated with a name like this can make it harder to understand your program’s behavior because it’s more difficult to know which value is associated with a name or why that value might have changed. Clojure has a set of tools for dealing with change, which you’ll learn about in Chapter 10. As you learn Clojure, you’ll find that you’ll rarely need to alter a name/value association.
(def status :my-body-is-ready)
;; => #'org.core/status
{:numbers [ 1 2/3 4.5 ]
:strings ["Yep" "With escapes! -> \""] }
:numbers | (1 2/3 4.5) | :strings | (Yep With escapes! -> “) |
:keywords
'symbols
Clojure supports four literal collection types:
'(1 2 3) ; list
[1 2 3] ; vector
#{1 2 3} ; set
{:a 1, :b 2} ; map
Maps
clojure.core/get allows you to grab keys, and can return nil or a default:
(get {:x 1 :y 2} :y) ;; => 2
(get {:x 1 :y 2} :z) ;; => nil
(get {:x 1 :y 2} :z 3) ;; => 3
clojure.core/get-in allows you to dig into nested maps:
(get-in
{:head 1 :chest {:ribs 10 :cavity {:heart "pumpin'" :lungs 2}}}
[:chest :cavity :heart])
;; => pumpin'
You can use a map like a function:
({:what "in" :tar "nation?"} :tar)
;; => nation?
…and keywords can be used the same way with a few data structures:
(:tar {:what "in" :tar "nation?"})
;; => nation?
(:far {:what "in" :tar "nation?"} "no far")
;; => no far
Vectors - clojure.core/vec
Vectors are zero-indexed collections like arrays.
(def vec1 [1 2 3 4 5])
(get vec1 0) ;; => 1
You can use vector to make vectors and conj to add to them:
(def vec2 (vector :weather :is :nice))
(conj vec2 :today) ;; => [:weather :is :nice :today]
Lists - clojure.core/list
Recall that Clojure is a LISP. Lists can hold anything. Use a tick mark to indicate that a sexp is a list.
(def list1 '(1 2 3 4 5))
(nth list1 3) ;; => 4
Using clojure.core/conj on a list adds items to the beginning, and on a vector will add items to the end. A bit of a footgun.
(conj list1 0) ;; => (0 1 2 3 4 5)
Hash Sets and Sorted Sets
(def hs1 #{"this is a hash-set" 19 :testing})
A hash set can only store unique values. Using conj to add to a hash-set will combine unique values.
(conj hs1 19)
;; => #{"this is a hash set" 19 :testing}
(hash-set 1 2 3 4 1 2 3 4 5 6)
;; => #{1 4 6 3 2 5}
Usefully, set can be used to derive all the unique values from another collection.
(set [1 2 3 4 1 2 3 3 4 1 2 3 4 2 3 2 1 2])
;; => #{1 4 3 2}
Use clojure.core/get and clojure.core/contains? with hash sets:
(contains? hs1 18) ; false
(contains? hs1 19) ; true
(get hs1 18) ;; => nil
(get hs1 19) ; 19
Because of Clojure’s Lisp syntax, with the humble and incredibly simple s-expression as the core building block of a Lisp program, we can do some pretty incredible things to simplify complex operations.
(operator operand operand operand)
“All Clojure operations have the same syntax: opening parenthesis, operator, operands, closing parenthesis”
– Daniel Higginbotham3
Also recall that we can return functions:
(or + - * /)
;; => #function[clojure.core/+]
Which means we can return a function and call it on more data:
((or + -) 1 2 3)
;; => 6
The error cannot be cast to clojure.lang.IFn
indicates you are trying
to use a number, string, or other type as a function.
("why" 1 2 3)
;; => class java.lang.String cannot be cast to class clojure.lang.IFn
Also see macro calls and special forms.
Upon creating and generating a new Luminus project and running it once in the REPL, here is part of the tree of directories and files that is generated:
guestbook/
│
├── project.clj
│
├── resources
│ ├── docs
│ │ └── docs.md
│ ├── html
│ │ ├── about.html
│ │ ├── base.html
│ │ ├── error.html
│ │ └── home.html
│ ├── migrations
│ │ ├── 20240223181041-add-users-table.down.sql
│ │ └── 20240223181041-add-users-table.up.sql
│ ├── public
│ │ ├── css
│ │ │ └── screen.css
│ │ ├── favicon.ico
│ │ ├── img
│ │ │ └── warning_clojure.png
│ │ └── js
│ └── sql
│ └── queries.sql
├── src
│ └── clj
│ └── guestbook
│ ├── config.clj
│ ├── core.clj
│ ├── db
│ │ └── core.clj
│ ├── handler.clj
│ ├── layout.clj
│ ├── middleware
│ │ └── formats.clj
│ ├── middleware.clj
│ ├── nrepl.clj
│ └── routes
│ └── home.clj
├── test
│ └── clj
│ └── guestbook
│ ├── db
│ │ └── core_test.clj
│ └── handler_test.clj
│
└── test-config.edn
Emacs is my editor of choice. It has unbeatable support for LISPs.
My personal configuration is based off of the sensible defaults provided in the Clojure for the Brave and True textbook. Using the initialization files mentioned on the linked page is a great way to start using Emacs in general.
Command | Action |
---|---|
M-x cider | Prompts for more options |
M-x cider-jack-in | Jacks in to current Clojure (clj) project |
C-c C-z | Jump cursor to REPL |
C-u C-c C-z | Jump cursor to REPL and switch to file namespace |
C-c C-d a | cider-apropos to remember var names |
C-x 5 2 | Pop out buffer into new window |
C-c C-k | Evaluate buffer (handy) |
C-c C-e | Evaluate preceding form |
C-c C-c or C-M-x | Evaluate current top-level form |
C-u C-c C-c | Evaluate current top-level form in debug mode |
C-c C-v r | Evaluate highlighted region |
C-c C-b | Interrupt evaluation |
M-. | cider-find-var: Warp to definition under cursor |
C-c C-d d | Look up documentation for current form |
C-c C-m | macroexpand-1: Macroexpand the form at point |
C-c M-z | Eval current buffer and switch to relevant REPL |
C-c M-n r | Reload all files on classpath |
M-, | Return to your pre-jump location |
M-TAB | Complete the symbol at point |
C-c C-q | Quit CIDER |
Sources:
CIDER is an interactive programming environment for Clojure.
Traditional programming languages and development environments often use a Edit, Compile, Run Cycle. In this environment, the programmer modifies the code, compiles it, and then runs it to see if it does what she wants. The program is then terminated, and the programmer goes back to editing the program further. This cycle is repeated over and over until the program behavior conforms to what the programmer desires. Using CIDER’s interactive programming environment, a programmer works in a very dynamic and incremental manner. Instead of repeatedly editing, compiling, and restarting an application, the programmer starts the application once and then adds and updates individual Clojure definitions as the program continues to run.4
It looks like this when run:
;; Connected to nREPL server - nrepl://localhost:36099
;; CIDER 1.13.0-snapshot (package: 20231127.825), nREPL 1.0.0
;; Clojure 1.11.1, Java 17.0.9
;; Docs: (doc function-name)
;; (find-doc part-of-name)
;; Source: (source function-name)
;; Javadoc: (javadoc java-object-or-class)
;; Exit: <C-c C-q>
;; Results: Stored in vars *1, *2, *3, an exception in *e;
;; ======================================================================
;; If you’re new to CIDER it is highly recommended to go through its
;; user manual first. Type <M-x cider-view-manual> to view it.
;; In case you’re seeing any warnings you should consult the manual’s
;; "Troubleshooting" section.
;;
;; Here are a few tips to get you started:
;;
;; * Press <C-h m> to see a list of the keybindings available (this
;; will work in every Emacs buffer)
;; * Press <,> to quickly invoke some REPL command
;; * Press <C-c C-z> to switch between the REPL and a Clojure file
;; * Press <M-.> to jump to the source of something (e.g. a var, a
;; Java method)
;; * Press <C-c C-d C-d> to view the documentation for something (e.g.
;; a var, a Java method)
;; * Print CIDER’s refcard and keep it close to your keyboard.
;;
;; CIDER is super customizable - try <M-x customize-group cider> to
;; get a feel for this. If you’re thirsty for knowledge you should try
;; <M-x cider-drink-a-sip>.
;;
;; If you think you’ve encountered a bug (or have some suggestions for
;; improvements) use <M-x cider-report-bug> to report it.
;;
;; Above all else - don’t panic! In case of an emergency - procure
;; some (hard) cider and enjoy it responsibly!
;;
;; You can remove this message with the <M-x cider-repl-clear-help-banner> command.
;; You can disable it from appearing on start by setting
;; ‘cider-repl-display-help-banner’ to nil.
;; ======================================================================
There is some setup required to execute Clojure inline within this org-mode file and cleanly transform it to markdown.
Ox-Hugo must be added to Emacs.
There is a minimal amount of emacs lisp added to my editor to support
this. The remainder of setup is completed in the front matter of the
org file, clj.org
, in /content-org/languages/clj.org
.
(setup (:package ox-hugo)
(:load-after ox))
(defun clojuredoc-string-to-url (str)
"In a url, ? becomes _q, replace these in the url part of STR in clojuredocs links."
(s-replace "?" "_q" str))
(defun org-link-to-clojuredocs ()
"Insert a link to clojuredocs.org."
(interactive)
(let ((str (read-string "Function (ex. clojure.core/when) >> ") ))
(insert (s-concat "[[https://clojuredocs.org/"
(clojuredoc-string-to-url str) "][" str "]]"))))
(defun org-link-to-core-clojuredocs ()
"Insert a link to clojuredocs.org in the clojure.core namespace."
(interactive)
(let ((str (read-string "clojure.core function (ex. when) >> ") ))
(insert (s-concat "[[https://clojuredocs.org/clojure.core/"
(clojuredoc-string-to-url str) "][clojure.core/" str "]]"))))
;; Make these easier to type
(global-set-key (kbd "C-c o C") 'org-link-to-clojuredocs)
(global-set-key (kbd "C-c o c") 'org-link-to-core-clojuredocs)
A bunch of additional front matter must be added in order for ox-hugo to correctly move the file and add the appropriate front matter. Care has been taken to ensure the output format is readable.
#+LAYOUT: docs-manual
#+TITLE: Clojure
#+SUMMARY: Enterprise grade magick.
#+hugo_base_dir: ../../
#+hugo_section: languages
#+hugo_custom_front_matter: :warning "THIS FILE WAS GENERATED BY OX-HUGO, DO NOT EDIT!!!"
#+hugo_custom_front_matter: :toc true :summary "Enterprise grade magick." :chapter true
#+hugo_custom_front_matter: :aliases '("/clj/" "/clojure/" "/clj" "/cljd" "/cljs")
#+PROPERTY: header-args :eval no :exports both
#+hugo_level_offset: 0
…apart from an issue where I can’t have headers beyond level 3, which I still must resolve, ox-hugo has happily enabled me to leverage all the enhanced markdown processing features provided by hugo (render hooks in particular) while still writing and executing code in ORG.
Websites:
Books:
(Remember to buy books to support good authors.)
“Literate Programming”, Donald E. Knuth, stanford.edu ↩︎
“Clojure for the Brave and True” page 48. ↩︎
“Clojure for the Brave and True” by Daniel Higginbotham, braveclojure.com ↩︎ ↩︎
Documentation for CIDER: Interactive Programming ↩︎
Pages are organized by last modified.
Title: Clojure
Word Count: 4119 words
Reading Time: 20 minutes
Permalink:
→
https://manuals.ryanfleck.ca/clj/