Mount
managing Clojure and ClojureScript app state since (reset)
Install / Use
/learn @tolitius/MountREADME
I think that it's extraordinarily important that we in computer science keep fun in computing
Alan J. Perlis from Structure and Interpretation of Computer Programs
mount <img src="doc/img/mount-logo.png" width="70px">
any questions or feedback: #mount clojurians slack channel <img src="doc/img/slack-icon.png" width="15px"> (or just open an issue)
Table of Contents generated with DocToc
- Why?
- How
- Dependencies
- Value of Values
- The Importance of Being Reloadable
- Start and Stop Order
- Composing States
- Start and Stop Parts of Application
- Start an Application Without Certain States
- Swapping Alternate Implementations
- Stop an Application Except Certain States
- ClojureScript is Clojure
- cljc mode
- Packaging
- Affected States
- Recompiling Namespaces with Running States
- Cleaning up Deleted States
- Logging
- Exception Handling
- Clojure Version
- Mount and Develop!
- Web and Uberjar
- Runtime Arguments
- License
Why?
Clojure is
- powerful
- simple
- and fun
Depending on how application state is managed during development, the above three superpowers can either stay, go somewhat, or go completely.
If Clojure REPL (i.e. lein repl, boot repl) fired up instantly, the need to reload application state
inside the REPL would go away. But at the moment, and for some time in the future, managing state by making it
reloadable within the same REPL session is important to retain all the Clojure superpowers.
Here is a good breakdown on the Clojure REPL startup time, and it is not because of JVM.
mount is here to preserve all the Clojure superpowers while making the application state enjoyably reloadable.
There is another Clojure superpower that mount is made to retain: Clojure community.
Pull request away, let's solve this thing!
Differences from Component
mount is an alternative to the component approach with notable differences.
How
(require '[mount.core :refer [defstate]])
Creating State
Creating state is easy:
(defstate conn :start (create-conn))
where the create-conn function creates a connection (for example to a database) and is defined elsewhere, can be right above it.
In case this state needs to be cleaned / destroyed between reloads, there is also :stop
(defstate conn :start (create-conn)
:stop (disconnect conn))
That is pretty much it. But wait, there is more.. this state is a top level being, which means it can be simply
required by other namespaces or in REPL:
dev=> (require '[app.db :refer [conn]])
nil
dev=> conn
#object[datomic.peer.LocalConnection 0x1661a4eb "datomic.peer.LocalConnection@1661a4eb"]
Using State
For example let's say an app needs a connection above. No problem:
(ns app
(:require [above :refer [conn]]))
where above is an arbitrary namespace that defines the above state / connection.
Documentation String
As in any definition (i.e. def, defn) a documentation string can be added to better describe a state:
(defstate answer
"answer to the ultimate question of life universe and everything"
:start (+ 1 41))
(doc answer)
-------------------------
dev/answer
answer to the ultimate question of life universe and everything
Dependencies
If the whole app is one big application context (or system), cross dependencies with a solid dependency graph
is an integral part of the system.
But if a state is a simple top level being, these beings can coexist with each other and with other
namespaces by being required instead.
If a managing state library requires a whole app buy-in, where everything is a bean or a component, it is a framework, and dependency graph is usually quite large and complex, since it has everything (every piece of the application) in it.
But if stateful things are kept lean and low level (i.e. I/O, queues, threads, connections, etc.), dependency graphs are simple and small, and everything else is just namespaces and functions: the way it should be.
Talking States
There are of course direct dependencies that mount respects:
(ns app.config
(:require [mount.core :refer [defstate]]))
(defstate config
:start (load-config "test/resources/config.edn"))
this config, being top level, can be used in other namespaces, including the ones that create states:
(ns app.database
(:require [mount.core :refer [defstate]]
[app.config :refer [config]]))
(defstate conn :start (create-connection config))
here is an example of a web server that "depends" on a similar config.
(the example load-config function above comes from cprop, but could of course be a custom function that loads configuration from a file)
Value of values
Lifecycle functions start/stop can take both functions and values. This is "valuable" and also works:
(defstate answer-to-the-ultimate-question-of-life-the-universe-and-everything :start 42)
While it would be useful in REPL and for testing, real application states would usually have start / stop logic, in other words, the real lifecycle.
Besides scalar values, lifecycle functions can take anonymous functions, partial functions, function references, etc.. Here are some examples:
(defn f [n]
(fn [m]
(+ n m)))
(defn g [a b]
(+ a b))
(defn- pf [n]
(+ 41 n))
(defn fna []
42)
(defstate scalar :start 42)
(defstate fun :start #(inc 41))
(defstate with-fun :start (inc 41))
(defstate with-partial :start (partial g 41))
(defstate f-in-f :start (f 41))
(defstate f-no-args-value :start (fna))
(defstate f-no-args :start fna)
(defstate f-args :start g)
(defstate f-value :start (g 41 1))
(defstate private-f :start pf)
Check out fun-with-values-test for more details.
The Importance of Being Reloadable
mount has start and stop functions that will walk all the states created with defstate and start / stop them
accordingly: i.e. will call their :start and :stop defined functions. Hence the whole application state can be reloaded in REPL e.g.:
dev=> (require '[mount.core :as mount])
dev=> (mount/stop)
dev=> (mount/start)
While it is not always necessary, mount lifecycle can be easily hooked up to tools.namespace, to make the whole application reloadable with refreshing the app namespaces. Here is a dev.clj as an example, that sums up to:
(defn go []
(start)
:ready)
(defn reset []
(stop)
(tn/refresh :after 'dev/go))
the (reset) is then used in REPL to restart / reload application state without the need to restart the REPL itself.
Start and Stop Order
Since dependencies are "injected" by requireing on the namespace level, mount trusts the Clojure compiler to
maintain the start and stop order for all the defstates.
The "start" order is then recorded and replayed on each (reset).
The "stop" order is simply (reverse "start order"):
dev=> (reset)
08:21:39.430 [nREPL-worker-1] DEBUG mount - << stopping.. nrepl
08:21:39.431 [nREPL-worker-1] DEBUG mount - << stopping.. conn
08:21:39.432 [nREPL-worker-1] DEBUG mount - << stopping.. config
:reloading (app.config app.nyse app.utils.datomic app)
08:21:39.462 [nREPL-worker-1] DEBUG mount - >> starting.. config
08:21:39.463 [nREPL-worker-1] DEBUG mount - >> starting.. conn
08:21:39.481 [nREPL-worker-1] DEBUG mount - >> starting.. nrepl
:ready
You can see examples of start and stop flows in the example app.
Composing States
Besides calling (mount/start) there are other useful ways to start an application:
- starting parts of an application
- [starting an application without certain states](README.md#start-an-appli
