Clojure
Introduction
Interesting code/styles I’ve found in various Clojure open-source codebases.
Exploring the Core
The built-in clojure.repl
namespace has useful functions to explore Clojure, and we can use it to explore the core namespaces.
(ns clojurexplore
(:require [msync.utils :refer :all]
[clojure.repl :refer [doc dir dir-fn]]))
(take 20 (dir-fn 'clojure.core))
What do docs look like?
(doc +)
-------------------------
clojure.core/+
([] [x] [x y] [x y & more])
Returns the sum of nums. (+) returns 0. Does not auto-promote
longs, will throw on overflow. See also: +'
Java Facilities :1.12+:
The new namespace clojure.java.basis
gives access to underlying JVM information that can be used for inspecting the runtime infrastructure. The runtime needs to have been started using the Clojure CLI.
(require 'clojure.java.basis)
(dir clojure.java.basis)
(doc clojure.java.basis/initial-basis)
(doc clojure.java.basis/current-basis)
The clojure.java.process
namespace wraps the Java process API and provides some convenience functions for making use of the APIs.
(dir clojure.java.process)
Interesting Styles style
Optional data
This is from the Sente library by Peter Taoussanis. Taking an example from sente itself - a server event message is a map expected to have multiple keys, as below. Since we are in Lisp-land, almost every character on the keyboard is available for identifiers.
{:keys [event id ?data send-fn ?reply-fn uid ring-req client-id]}
We start the identifier with a ?
- something that we use to indicate optional.
So, keys ?data
and ?reply-fn
represent optional entries in the map.
Splitting a namespace across files
When your namespace is growing too large for a single file, you can split it across multiple files.
For example, take the clojure.core
namespace, which is split across multiple files. As a more concrete piece, core_print.clj
contains some clojure.core
functions. Here’s how it is done - notice that the base file-name, without the .clj
suffix, is used.
;; In core.clj
(load "core_print")
Inside core_print.clj
, use the in-ns
directive to tell the clojure reader/compiler how to treat the code in here.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; In core_print.clj, the beginning line is
(in-ns 'clojure.core)
;; And the implementation follows
Smart Tricks snippets
Convert a map (keyed by integers) into a correspondingly ordered vector
;; From clojure.edn.data
(ns user
(:require [msync.utils :refer :all]))
(defn- vectorize
"Convert an associative-by-numeric-index collection into
an equivalent vector, with nil for any missing keys"
[m]
(when (seq m)
(reduce
(fn [result [k v]] (assoc result k v)) ;; 1
(vec (repeat (apply max (keys m)) nil)) ;; 2
m)))
(def m {0 :A 2 :C 9 :J})
(vectorize m)
;; Output #'user/vectorize[:A nil :C nil nil nil nil nil nil :J]
On line marked 2, you initialize a vector of the required length with nil
-s, using max
(apply max (keys m))
And reduce m
with the function defined on line marked 1 into this vector.
Temporarily descending into mutable-land. performance
Immutability is great, but it’s okay to mutate within the private confines of some scope
(defn filterv
"Returns a vector of the items in coll for which
(pred item) returns true. pred must be free of side-effects."
{:added "1.4"
:static true}
[pred coll]
(-> (reduce (fn [v o] (if (pred o) (conj! v o) v))
(transient [])
coll)
persistent!))
conj!
adds an element to the transient vector - notice the ! at the end. Also, the transient-functions always return a reference that you are expected to use for the next step. This is important - even though it mutates in place, we are not supposed to hold on to any old reference while building the transient datastructure.
And finally, the collection is made persistent with persistent!
.
Control Flow
An interesting discussion can be found at How are clojurians handling control flow on their projects?
Uncommon Stuff
deftype
and Mutable Members
deftype
allows for the definition of mutable members using the ^:volatile-mutable
tag on a member. But they become private to the object, and we need to implement methods on them (via interfaces or protocols) to access these private members for modification.
(ns msync.deftype
(:require [msync.utils :refer :all]))
(defprotocol BarBazOperators
(get-bar [this])
(bar-inc! [this])
(baz [this])
(baz-inc! [this]))
(deftype FooBarBaz [foo ^:volatile-mutable bar ^:volatile-mutable baz]
BarBazOperators
(get-bar [_] bar)
(bar-inc! [_] (set! bar (inc bar)))
(baz [_] baz)
(baz-inc! [_] (set! baz (inc baz))))
(def foo-bar-baz (FooBarBaz. 10 20 30))
;; We have a problem
(.bar foo-bar-baz)
These are method calls.
(.get-bar foo-bar-baz)
(.baz foo-bar-baz)
So are these too - method calls.
(print-all
(bar-inc! foo-bar-baz)
(get-bar foo-bar-baz)
(baz-inc! foo-bar-baz)
(baz foo-bar-baz))
But foo
is different.
(.foo foo-bar-baz)
There is no method foo
(foo foo-bar-baz)
&env
in Macros
Inside of macros, you have access to the context in which you are being evaluated.
(defmacro show-env []
(into [] (map class (keys &env))))
(defn show-env-wrapper [x y] (show-env))
(show-env-wrapper 10 20)
Although, it is an extremely tricky place to be in. map
is lazy, so let’s try to return the result of the map
operation rather than the call to (into []..)
(defmacro show-env []
(map class (keys &env)))
(defn show-env-wrapper [x y] (show-env))
(show-env-wrapper 10 20)
Nope. Did not go well.
Let’s try getting the keys out.
(defmacro show-env []
(into [] (keys &env)))
(defn show-env-wrapper [x y] (show-env))
(show-env-wrapper 10 20)
Wut!? The keys
actually turn out to be the vals
! What if we used vals
instead of keys (non-lazy)?
(defmacro show-env []
(into [] (vals &env)))
(defn show-env-wrapper [x y] (show-env))
#'user/show-env-wrapper
Nope - we can’t get the vals
to escape. What are those?
(defmacro show-env []
(into [] (map class (vals &env))))
(defn show-env-wrapper [x y] (show-env))
(show-env-wrapper 10 20)
Turns out - a core Compiler
artifact - an inner class LocalBinding
.
The keys
are Symbol
objects. Printing them gets us the values held in them. Ok, we can get all we want from the keys.
Here’s the culmination (final version of show-env
via @codesmith on the Clojurians Slack, which was also the inspiration for this section on &env
)
(require '[msync.utils :refer :all])
(defmacro show-env []
(into {} (map (juxt (comp keyword name) identity) (keys &env))))
(defn show-env-wrapper [x y] (show-env))
(print-all
(show-env-wrapper 10 20)
(let [a-for "Apple"
b-for "Bat"]
(show-env-wrapper 100 200))
(let [a-for "Apple"
b-for "Bat"]
((fn [x y] (show-env)) 1000 2000)))
As you can notice above, macros do their things at compile time. The first let
block bindings aren’t available to the show-env-wrapper
function. The second let
block’s bindings, which uses an inline function, are available via the &env
processing.
Java Interop
Some useful libraries
- Dynamic Object - leverage Clojure datastructures from Java
Leiningen
It’s macro-land, after all
If you’d like to declare some constants at the top of your project.clj for reuse within defproject
(def my-version "1.2.3")
(defproject org.msync/my-pet-project ~my-version
;; And all your directives
)
The eval-reader for dynamism
Some self-evident example code. Notice the use of the eval reader #=.
:source-paths [#=(eval (str (System/getProperty "user.home") "/.lein/dev-srcs"))
"src"]
You can unlimit your imagination and find quite a few uses.
Gotchas
with-redefs
with-redefs
and inlined-annotated vars don’t play well.
(with-redefs [+ -] (+ 10 20 30))
+
, -
and other functions (many math operations, among others) fall under this category. For reference, here is the source of +
in Clojure 1.10.3
(defn +
"Returns the sum of nums. (+) returns 0. Does not auto-promote
longs, will throw on overflow. See also: +'"
{:inline (nary-inline 'add 'unchecked_add)
:inline-arities >1?
:added "1.2"}
([] 0)
([x] (cast Number x))
([x y] (. clojure.lang.Numbers (add x y)))
([x y & more]
(reduce1 + (+ x y) more)))
Monads
Since no article is complete without an attempt at monads, we refer to Monads for completeness’ sake.
This is only a re-run of 05-monads.
Monads refer to a category of values associated with a pair of functions
unit
bind
which have certain properties, as described below.
First, let us introduce some terminology and symbols
Category
- The category to which our monads belong.
- x
- Any value
- m
- A value belonging to the
Category
- f
- An arbitrary function, that returns a value belonging to the
Category
(unit x)
results in a value that belongs to this category, given any x
(bind f m)
returns a value in the said category