Skip to content
Tips and snippets for a robust Reloaded workflow
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
README.md

README.md

Rehardened

This repo aims to contain useful snippets and advice for never having to restart a JVM again.

It requires quite some head scratching to make the Reloaded workflow avoid a variety of pitfalls. I've observed that Clojure programmers keep encountering these, so I decided to share some knowledge in this repo.

PRs with further advice are absolutely welcome!

General advice

Use an external library which defines your refresh/reset functions:

A very important point of those is that they are outside of your source-paths/refresh-paths, so they don't ever get unloaded.

Don't create any helper function for refreshing

At least not in your source-paths/refresh-paths; author instead a library similar to the ones mentioned above.

Else, those functions could get lost after a bad refresh, and you wouldn't be able to automatically recover from that situation.

Use a reset/reload snippet that requires zero context or state (loaded code, current ns, etc)

Normally your IDE can be assigned a keyboard shortcut to a custom reload command. Don't bind (reset) to that shortcut, since it depends on the user ns to be loaded. Something like (com.stuartsierra.component.repl/reset) needs less context.

Catch errors

  • Switching between branches can confuse c.t.n.r.refresh. You should catch that FileNotFoundException and issue a c.t.n.r.clear.

  • Sierra's Component doesn't perform any cleanup of unsuccessfully started systems. Catch any errors and attempt a cleanup of your half-started system.

    • Else you'll see things like HTTP servers encountering a port-already-taken situation.

Distinguish between refresh and reset

It's tempting to simplify things and just always reset. But it's useful to also refresh, since it's vastly faster and sufficient for developing code and running tests.

  • I use Command+Option+Enter for refresh, Control+Enter for reset.

Avoid stateful or side-effectful namespaces to the extreme

If you are familiar with Reloaded, probably you already avoid side-effectful namespaces:

  • No top-level state: (def x (atom {}))
  • No top-level side effects: (register! a-thing)

You can take this philosophy to the extreme and isolate:

  • The clojure.core/extend* family of functions
  • defrecords (and similar) containing protocol implementations
    • Make your defrecords anemic, extend them later
  • defmethod
  • Avoid side-effectful imports: e.g. a :require of a namespace only containing defmethods

Launch all of those in your Component start method. Similarly, undo all state in your stop method, e.g. undefine the multimethod implementations.

It's also worth looking into metadata-based protocol extension, for both defrecords, and plain maps.

In particular, plain maps instead of defrecords are drastically less likely to have code loading issues: classes are not guaranteed to be garbage-collected, while objects are.

Have a dedicated Lein profile

At least for Leiningen users, it is possible to define a global profile in ~/.lein/profiles with the requires necessary for your Reloaded workflow:

:reloaded {:dependencies [[org.clojure/tools.namespace "0.3.0-alpha4"]
                          [com.stuartsierra/component.repl "0.2.0"]]}

That way, you can lein with-profile +reloaded repl and enjoy Reloaded in any project. Advantages:

  • DRY setup
  • Reloaded setup becomes unobstrusive; collaborators may not want it
  • You can use the profile in any project (e.g. a random git clone), not just projects of yours.

Note that typically you can use the profile in your IDE as well.

Just as a caveat, your IDE snippet must include the require, and particularly resolve business which you'll notice in the final section.

Otherwise you'd need a dev/user.clj with those requires, which is not guaranteed to exist in a given project.

Don't refresh-on-save

Having some setup for issuing a refresh on every save (Command+S) is generally a recipe for disaster. I recommend instead that you create a composite command in your editor that intentfully saves + refreshes.

In my case, I hit:

  • Command+S for just saving
  • Command+Option+Enter for saving and refreshing

That way, I can keep the distinction while still needing few keystrokes.

Snippets

The following are battle-hardened snippets that you can bind to your IDE. I know they look kind of funny but there's a reason for every aspect you can see.

Refresh

(clojure.core/when-let [v (do
                            (clojure.core/require 'clojure.tools.namespace.repl)
                            ((clojure.core/resolve 'clojure.tools.namespace.repl/set-refresh-dirs) "src" "test")
                            ((clojure.core/resolve 'clojure.tools.namespace.repl/refresh)))]
  (clojure.core/when (clojure.core/instance? java.lang.Throwable v)
    (clojure.core/when (clojure.core/instance? java.io.FileNotFoundException v)
      ((clojure.core/resolve 'clojure.tools.namespace.repl/clear)))
    (throw v)))
  • A ["src" "test"] source path structure is assumed, which you can tweak
  • Otherwise the snippet is agnostic to Component vs. Integrant

Reset

(do
  (clojure.core/require 'clojure.tools.namespace.repl)
  (clojure.core/require 'com.stuartsierra.component.repl)
  ((clojure.core/resolve 'clojure.tools.namespace.repl/set-refresh-dirs) "src" "test")
  (try
    ((clojure.core/resolve 'com.stuartsierra.component.repl/reset))
    (catch java.lang.Throwable v
      (clojure.core/when (clojure.core/instance? java.io.FileNotFoundException v)
        ((clojure.core/resolve 'clojure.tools.namespace.repl/clear)))
      (clojure.core/when ((clojure.core/resolve 'com.stuartsierra.component/ex-component?) v)
        (clojure.core/let [stop (clojure.core/resolve 'com.stuartsierra.component/stop)]
          (clojure.core/some-> v clojure.core/ex-data :system stop)))
      (throw v))))
  • A ["src" "test"] source path structure is assumed, which you can tweak
  • Note there's a Component require and stop aspect. If using Integrant, you'll want to replace those.

Alternatively to the "IDE snippets" approach, you can bundle code like that in your own library, similarly to the libraries mentioned in the first section.

Personally I don't because I don't need it - it's more agile to have a snippet that I can continuously refine without releasing .jars, etc.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.