Configure REPL on Startupλ︎
A Clojure REPL starts in the user
namespace and automatically loads common tools to support REPL based development.
When interacting with the REPL prompt directly, use require
expressions to include additional functions into the user
nameapace rather than use potentially complex commands to set the namespace.
Clojure REPL only starts in user namespace
The Clojure REPL only guarantees startup in the user
namespace. There is no specific mechanism to start the REPL in any other namespace than user
.
Clojure CLI could use the general --eval
flag as a hack to set a different namespace with an in-ns
expression, although this may affect other tools and add complexity to the startup process.
Default REPL Tools
The Clojure REPL automatically loads common tools to support the foundation of a REPL driven workflow:
clojure.repl namespace loads:
- apropos - function names fuzzy matching a given regex pattern
- dir - sorted list of public vars (functions) in a given namespace
- doc - doc-string of a give Clojure function / symbol
- find-doc - doc-string of matching functions, given a string or regex pattern
- source - source code of a given function
- pst print stack trace, optionally setting depth
clojure.java.javadoc loads javadoc to show the doc-string of Java methods
clojure.pprint namepace loads pp & pprint to return pretty printed (human friendly format) evaluation results
Custom user namespaceλ︎
Add a custom user
namespace to further enhance the Clojure REPL workflow:
- load code into the REPL by requiring namespaces
- call functions to start services that support development, e.g. logging publisher, print REPL command help menu
- launch development tools - e.g. portal data inspector
- start components (i.e for mount, component, integrant)
- hotload libraries into the REPL process without restart (Clojure 1.12 onward)
Create a project with custom user namespace
Projects created with Practicalli Project Templates contain a dev/user.clj
file for configuring the REPL at start up.
Practicalli custom user namespace supports the Practicalli REPL Reloaded workflow
Start the REPL with either the :dev/env
, :dev/reloaded
or :repl/reloaded
alias from Practicalli Clojure CLI Config to include dev
directory on the class path and automatically load dev/user.clj
code on REPL startup.
Define user namespaceλ︎
A custom user.clj
is typically placed in a dev
folder within the root of the project, with the dev
path defined in an alias to keep it separated from production code.
Create a dev/user.clj
file with a namespace called user
.
Create an alias to include the dev
path when running a REPL process
Practicalli Clojure CLI Config includes aliases that add dev
directory to the class path
:dev/env
alias only adds thedev
directory to the classpath:dev/reloaded
adds library hotload, namespace reload, porta data inspector and testing libraries &test
:repl/reloaded
adds Rebel rich terminal UI to the tools provided by:dev/reloaded
Add an alias to the user deps.edn
configuration, i.e. $XDG_CONFIG_HOME/clojure/deps.edn
or $HOME/.clojure/deps.edn
Run a Clojure REPL with the :repl/reloaded
alias (or :dev/reloaded
:dev/env
) to add the dev
directory to the class path and load the code in dev/user.clj
file into the REPL.
Keep user.clj
separate
The user.clj
code should not be included in live deployments, such as a jar or uberjar. Including the dev/
directory via an alias separates the user.clj
from deployment actions.
Requiring namespacesλ︎
Namespaces required in the user
ns form will also be loaded. If a required namespace also requires namespaces, they will also be loaded into the REPL during startup.
Functions (defn)
and data (def)
are immediately available.
Require namespace in user ns expression
Add a require expression to the namespace definition in dev/user.clj
Requiring a large number of libraries may slow REPL start up time
Require namespace in require expression
If the library is not always required, place a require
within a (comment ,,,)
expression to be evaluated by the developer any time after REPL startup.
Calling functionsλ︎
Use the fully qualified function name from the required namespace can be called, to start the application for example.
Example
An alias can be used in the require expression, useful if multiple functions from a namespace are to be called
REPL Help menuλ︎
Printing a menu of functions provided by the custom user namespace helps with the usability of a project.
Define a help
function that prints out commands with a breif explination of their purpose.
Add a (help)
expression to call the help function on REPL startup, displaying the help menu.
REPL Help menu for custom user namespace
;; ---------------------------------------------------------
;; Help
(println "---------------------------------------------------------")
(println "Loading custom user namespace tools...")
(println "---------------------------------------------------------")
(defn help
[]
(println "---------------------------------------------------------")
(println "System components:")
(println "(start) ; starts all components in system config")
(println "(restart) ; read system config, reloads changed namespaces & restarts system")
(println "(stop) ; shutdown all components in the system")
;; (println "(system) ; show configuration of the running system")
;; (println "(config) ; show system configuration")
(println)
(println "Hotload libraries: ; Clojure 1.12.x")
(println "(add-lib 'library-name)")
(println "(add-libs '{domain/library-name {:mvn/version \"v1.2.3\"}})")
(println "(sync-deps) ; load dependencies from deps.edn")
(println "- deps-* lsp snippets for adding library")
(println)
(println)
(println "Portal Inspector:")
(println "- portal started by default, listening to all evaluations")
(println "(inspect/clear) ; clear all values in portal")
(println "(remove-tap #'inspect/submit) ; stop sending to portal")
(println "(inspect/close) ; close portal")
(println)
(println "(help) ; print help text")
(println "---------------------------------------------------------"))
(help)
;; End of Help
;; ---------------------------------------------------------
Log publisherλ︎
mulog is a very effective event log tool that also provides a range of log publishers. A custom user namespace can be used to start mulog log publishers to directly support the development workflow
- pretty print console output for easier to read event messages
- custom tap-publisher to send all log message to a
tap>
source, e.g. Portal data inspector
Mulog configuration and publishers
;; ---------------------------------------------------------
;; Mulog Global Context and Custom Publisher
;;
;; - set event log global context
;; - tap publisher for use with Portal and other tap sources
;; - publish all mulog events to Portal tap source
;; ---------------------------------------------------------
(ns mulog-events
(:require
[com.brunobonacci.mulog :as mulog]
[com.brunobonacci.mulog.buffer :as mulog-buffer]))
;; ---------------------------------------------------------
;; Set event global context
;; - information added to every event for REPL workflow
(mulog/set-global-context! {:app-name "todo-basic Service",
:version "0.1.0", :env "dev"})
;; ---------------------------------------------------------
;; ---------------------------------------------------------
;; Mulog event publishing
(deftype TapPublisher
[buffer transform]
com.brunobonacci.mulog.publisher.PPublisher
(agent-buffer [_] buffer)
(publish-delay [_] 200)
(publish [_ buffer]
(doseq [item (transform (map second (mulog-buffer/items buffer)))]
(tap> item))
(mulog-buffer/clear buffer)))
#_{:clj-kondo/ignore [:unused-private-var]}
(defn ^:private tap-events
[{:keys [transform] :as _config}]
(TapPublisher. (mulog-buffer/agent-buffer 10000) (or transform identity)))
(def tap-publisher
"Start mulog custom tap publisher to send all events to Portal
and other tap sources
`mulog-tap-publisher` to stop publisher"
(mulog/start-publisher!
{:type :custom, :fqn-function "mulog-events/tap-events"}))
#_{:clj-kondo/ignore [:unused-public-var]}
(defn stop
"Stop mulog tap publisher to ensure multiple publishers are not started
Recommended before using `(restart)` or evaluating the `user` namespace"
[]
tap-publisher)
;; Example mulog event message
;; (mulog/log ::dev-user-ns :message "Example event message" :ns (ns-publics *ns*))
;; ---------------------------------------------------------
Reload Namespacesλ︎
The REPL state can become 'stale' and contain vars (data and function names) that are no longer part of the source code, especially after a code refactor.
Rather than restart the repl, clojure.tools.namespace.repl provides functions that can clean the REPL state and reload changed namespaces from source code.
Clojure Namespace Tools - reload
Require the clojure.tools.namespace.repl
namespace to access the refresh
and set-refresh-dirs
functions to support reloading of source code into a clean REPL state.
(ns user
"Tools for REPL Driven Development"
(:require
[clojure.tools.namespace.repl :refer [set-refresh-dirs]]))
Use the set-refresh-dirs
function to define directories to reload when calling refresh
, effectively excluding dev
and other directories by not including their names as arguments.
;; ---------------------------------------------------------
;; Avoid reloading `dev` code
;; - code in `dev` directory should be evaluated if changed to reload into repl
(println
"Set REPL refresh directories to "
(set-refresh-dirs "src" "resources"))
;; ---------------------------------------------------------
Hotload librariesλ︎
Hotload is a way to add libraries to a running REPL process which were not include as a dependency during REPL startup.
Hotload libraries is SNAPSHOT feature - this guide will change when Clojure 1.12 is released
Functions to hotload libraries are part of the Clojure 1.12 development releases and an official feature as of the stable 1.12 release.
For Clojure 1.11 and similar functions are available in the add-libs3 branch of the now deprecated clojure.tools.deps.alpha
library.
clojure/tools.deps is the official library for all released functions from the alpha library
This guide will be significantly rewritten once Clojure 1.12 is released.
:repl/reloaded
and dev/reloaded
aliases in Practicalli Clojure CLI Config provide the add-libs
function.
Edit the project deps.edn
configuration and add an :lib/hotload
alias for the clojure.tools.deps.alpha.repl
library. Or add an alias to the user level configuration for use with any Clojure CLI project.
The add-libs
code is on a separate add-libs3 branch, so requires the SHA from the head of add-libs3 branch
:lib/hotload
{:extra-deps {org.clojure/tools.deps.alpha
{:git/url "https://github.com/clojure/tools.deps.alpha"
:git/sha "e4fb92eef724fa39e29b39cc2b1a850567d490dd"}}}
Alias example from Practicalli Clojure CLI Config
Start a REPL session using Clojure CLI with :repl/reloaded
, dev/reloaded
or :lib/hotload
aliases
Require and refer add-libs function
Require the clojure.tools.deps.alpha
library and refer the add-libs
function. The add-libs
function can then be called without having to use an alias or the fully qualified name.
Hotload one or more libraries into the REPL using the add-lib
function, including the fully qualified name of the library and version string.
Hotload the hiccup library
The hiccup library converts clojure structures into html, where vectors represent the scope of keywords that represent html tags. Load the hiccup library using add-libs
Require the hiccup library so its functions are accessible from the current namespace in the REPL.
Enter an expression using thehiccup/html
function to convert a clojure data structure to html.
System Componentsλ︎
Clojure has several library to manage the life-cycle of components that make up the Clojure system, especially those components with state. The order in which components are started and stopped can be defined to keep the system functioning correctly.
Components can include an http server, routing, persistence, logging publisher, etc.
Example system component management libraries included
- mount - manage system state in an atom
- donut-party system
- integrant and Integrant REPL - data definition of system and init & halt defmethod interface
- component
Require system namespace in user ns expression
Require the system namespace and use start
, restart
and stop
functions to manage the components in the system
Define code in the dev/system.clj
file which controls the component life-cycle services library for the project.
Create a dev/system.clj
to manage the components, optionally using one of the system component management libraries.
life-cycle librariesλ︎
Start, stop and restart the components that a system is composed of, e.g. app server, database pool, log publisher, message queue, etc.
Clojure web services run ontop of an HTTP server, e.g. http-kit, Jetty.
A Clojure aton can be used to hold a reference to the HTTP server, allowing commands to stop that server.
Use clojure.tools.namespace.repl/refresh
when restarting the server (in between stop
and start
) to remove stale information in the REPL state.
Restart an HTTP server for Clojure Web Service & Refresh namespaces
;; ---------------------------------------------------------
;; System REPL - Atom Restart
;;
;; Tools for REPl workflow with Aton reference to HTTP server
;; https://practical.li/clojure-web-services/app-servers/simple-restart/
;; ---------------------------------------------------------
(ns system-repl
(:require
[clojure.tools.namespace.repl :refer [refresh]]
[practicalli.todo-basic.service :as service]))
;; ---------------------------------------------------------
;; HTTP Server State
(defonce http-server-instance
(atom nil)) ; (1)!
;; ---------------------------------------------------------
;; ---------------------------------------------------------
;; REPL workflow commands
(defn stop
"Gracefully shutdown the server, waiting 100ms.
Check if an http server isntance exists and
send a `:timeout` key and time in milliseconds to shutdown the server.
Reset the atom to nil to indicate no http server is running."
[]
(when-not (nil? @http-server-instance)
(@http-server-instance :timeout 100) ; (2)!
(reset! http-server-instance nil) ; (3)!
(println "INFO: HTTP server shutting down...")))
(defn start
"Start the application server and run the application,
saving a reference to the https server in the atom."
[& port]
(let [port (Integer/parseInt
(or (first port)
(System/getenv "PORT")
"8080"))]
(println "INFO: Starting server on port:" port)
(reset! http-server-instance
(service/http-server-start port)))) ; (4)!
(defn restart
"Stop the http server, refresh changed namespace and start the http server again"
[]
(stop)
(refresh) ; (5)!
(start))
;; ---------------------------------------------------------
-
A Clojure Aton holds a reference to the http server instance
-
Shut down http server instance without stopping the Clojure REPL
-
Reset the value in the atom to mil, indicating that no http server instance is running
-
Reset the value in the atom to a reference for the running http server. The reference is returned when starting the http server.
-
Refresh the REPL state and reload changed namespaces from source code using
clojure.tools.namespace.repl/refresh
Define a dev.clj
file with go
, stop
and restart
functions that manage the life-cycle of mount components. A start
function contains the list of components with optional state.
Require the mount namespace and the main namespace for the project, which should contain all the code to start and stop services.
Define a start function to start all services
(defn start []
(with-logging-status)
(mount/start #'practicalli.app.conf/environment
#'practicalli.app.db/connection
#'practicalli.app.www/business-app
#'practicalli.app.service/nrepl))
The go
function calls start
and marks all components as ready.
The stop
function stops all components, removing all non-persistent state.
The reset function that calls stop
, refreshes the namespaces so that stale definitions are removed and starts all components (loading in any new code).
(defn reset
"Stop all states defined by defstate.
Reload modified source files and restart all states"
[]
(stop)
(namespace/refresh :after 'dev/go))
Example dev.clj file for mount
Use dev
namespace during development
Require practicalli.app.dev
namespace rather than main, to start components in a development environment.
Mount project on GitHub Mount - collection of Clojure/Script mount apps
donut.system is a dependency injection library for Clojure and ClojureScript using system and component abstractions to organise and manage startup & shutdown behaviour.
Configuration is a Clojure hash-map with functions to start and stop components.
Practicalli Gameboard Service - REPL tooling
;; ---------------------------------------------------------
;; Donut System REPL
;;
;; Tools for REPl workflow with Donut system components
;; ---------------------------------------------------------
(ns system-repl
"Tools for REPl workflow with Donut system components"
(:require
[donut.system :as donut]
[donut.system.repl :as donut-repl]
[donut.system.repl.state :as donut-repl-state]
[practicalli.gameboard.system :as system]))
(defmethod donut/named-system :donut.system/repl
[_] system/main)
(defn start
"Start system with donut, optionally passing a named system"
([] (donut-repl/start))
([system-config] (donut-repl/start system-config)))
(defn stop
"Stop the currently running system"
[] (donut-repl/stop))
(defn restart
"Restart the system with donut repl,
Uses clojure.tools.namespace.repl to reload namespaces
`(clojure.tools.namespace.repl/refresh :after 'donut.system.repl/start)`"
[] (donut-repl/restart))
(defn system
"Return: fully qualified hash-map of system state"
[] donut-repl-state/system)
Practicalli Gameboard Service - System configuration
;; ---------------------------------------------------------
;; practicalli.gameboard
;;
;; TODO: Provide a meaningful description of the project
;;
;; Start the service using donut configuration and an environment profile.
;; ---------------------------------------------------------
(ns practicalli.gameboard.system
"Service component lifecycle management"
(:gen-class)
(:require
;; Application dependencies
[practicalli.gameboard.router :as router]
;; Component system
[donut.system :as donut]
;; [practicalli.gameboard.parse-system :as parse-system]
;; System dependencies
[org.httpkit.server :as http-server]
[com.brunobonacci.mulog :as mulog]))
;; ---------------------------------------------------------
;; Donut Party System configuration
(def main
"System Component management with Donut"
{::donut/defs
;; Option: move :env data to resources/config.edn and parse with aero reader
{:env
{:http-port 8080
:persistence
{:database-host (or (System/getenv "POSTGRES_HOST") "http://localhost")
:database-port (or (System/getenv "POSTGRES_PORT") "5432")
:database-username (or (System/getenv "POSTGRES_USERNAME") "clojure")
:database-password (or (System/getenv "POSTGRES_PASSWORD") "clojure")
:database-schema (or (System/getenv "POSTGRES_SCHEMA") "clojure")}}
;; mulog publisher for a given publisher type, i.e. console, cloud-watch
:event-log
{:publisher
#::donut{:start (fn mulog-publisher-start
[{{:keys [publisher]} ::donut/config}]
(mulog/log ::log-publish-component
:publisher-config publisher
:local-time (java.time.LocalDateTime/now))
(mulog/start-publisher! publisher))
:stop (fn mulog-publisher-stop
[{::donut/keys [instance]}]
(mulog/log ::log-publish-component-shutdown :publisher instance :local-time (java.time.LocalDateTime/now))
;; Pause so final messages have chance to be published
(Thread/sleep 250)
(instance))
:config {:publisher {:type :console :pretty? true}}}}
;; HTTP server start - returns function to stop the server
:http
{:server
#::donut{:start (fn http-kit-run-server
[{{:keys [handler options]} ::donut/config}]
(mulog/log ::http-server-component
:handler handler
:port (options :port)
:local-time (java.time.LocalDateTime/now))
(http-server/run-server handler options))
:stop (fn http-kit-stop-server
[{::donut/keys [instance]}]
(mulog/log ::http-server-component-shutdown
:http-server-instance instance
:local-time (java.time.LocalDateTime/now))
(instance))
:config {:handler (donut/local-ref [:handler])
:options {:port (donut/ref [:env :http-port])
:join? false}}}
;; Function handling all requests, passing system environment
;; Configure environment for router application, e.g. database connection details, etc.
:handler (router/app (donut/ref [:env :persistence]))}}})
;; End of Donut Party System configuration
;; ---------------------------------------------------------
Component framework for managing the lifecycle and dependencies of software components which have runtime state, using a style of dependency injection using immutable data structures.
Clojure services may be composed of stateful processes that must be started and stopped in a particular order. The component model makes those relationships explicit and declarative,
seancorfield/usermanager-example Component project
A tutorial - Stuart Sierra's Component
Referenceλ︎
- Refactoring to Components - Walmart Labs Lacinia
- Integrant
- Compojure and Integrant
- Build a Clojure web app using Duct - CircleCI
- Reloading Woes - Lambda island