Skip to content

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.

dev/user.clj
(ns 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 the dev 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

Clojure User Config
 :env/dev
  {:extra-paths ["dev"]}
Review Practicalli Clojure CLI Config for further alias examples.

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.

clojure -M:repl/reloaded

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

dev/user.clj
(ns user
  (:require [practicalli.project-namespace]))

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.

dev/user.clj
(ns user)

(comment
  (require '[practicalli.project-namespace])
#_())

Calling functionsλ︎

Use the fully qualified function name from the required namespace can be called, to start the application for example.

Example

dev/user.clj
(ns user
  (:require [practicalli.project-namespace]))

(practicalli.project-namespace/-main)

An alias can be used in the require expression, useful if multiple functions from a namespace are to be called

Example

dev/user.clj
(ns user
  (:require [practicalli.service :as service]))

(service/-main)

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

dev/user.clj
;; ---------------------------------------------------------
;; 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

dev/mulog_events.clj
;; ---------------------------------------------------------
;; 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.

dev/user.clj
(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.

dev/user.clj
;; ---------------------------------------------------------
;; 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

clojure -M:repl/reloaded

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.

(require '[clojure.tools.deps.alpha.repl :refer [add-libs]])

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

(add-libs '{hiccup/hiccup {:mvn/version "2.0.0-alpha2"}})

Require the hiccup library so its functions are accessible from the current namespace in the REPL.

(require '[hiccup.core :as hiccup])
Enter an expression using the hiccup/html function to convert a clojure data structure to html.
(hiccup/html [:div {:class "right-aligned"}])

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

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

dev/user.clj
(ns user
  (:require [system]))

(comment
  (system/start)
  (system/restart)
  (system/stop)
  )

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

dev/system_repl.clj
;; ---------------------------------------------------------
;; 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))
;; ---------------------------------------------------------
  1. A Clojure Aton holds a reference to the http server instance

  2. Shut down http server instance without stopping the Clojure REPL

  3. Reset the value in the atom to mil, indicating that no http server instance is running

  4. Reset the value in the atom to a reference for the running http server. The reference is returned when starting the http server.

  5. 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.

dev/user.clj
(ns user
  :require [mount.core :refer [defstate]]
           [practicalli.app.main])

Define a start function to start all services

dev/user.clj
(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.

dev/user.clj
(defn go
  "Start all states defined by defstate"
  []
  (start)
  :ready)

The stop function stops all components, removing all non-persistent state.

(defn stop [] (mount/stop))

The reset function that calls stop, refreshes the namespaces so that stale definitions are removed and starts all components (loading in any new code).

dev/user.clj
(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.

Basic usage guide

donut-party/system

Practicalli Gameboard Service - REPL tooling

dev/system_repl.clj
;; ---------------------------------------------------------
;; 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

src/gameboard/system.clj
;; ---------------------------------------------------------
;; 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λ︎