A simple condition system for Clojure, without too much machinery.
org.sbrubbles/conditio-clj {:git/url "https://github.com/hanjos/conditio-clj"
:git/tag "0.2.0" :git/sha "d99c9c3"}
Configure your settings.xml
:
<servers>
<server>
<id>github</id>
<username>YOUR_GITHUB_LOGIN</username>
<password>YOUR_AUTH_TOKEN</password>
</server>
</servers>
Add this repo to your deps.edn
:
:mvn/repos {"github" {:url "https://maven.pkg.github.com/hanjos/conditio-clj"}}
And then:
org.sbrubbles/conditio-clj {:mvn/version "0.2.0"}
Exception systems divide responsibilities in two parts: signalling the exception (like throw
), and handling it (like try/catch
), unwinding the call stack until a handler is found. The problem is, by the time the error reaches the right handler, the context that signalled the exception is mostly gone. This limits the recovery options available.
A condition system, like the one in Common Lisp, provides a more general solution by splitting responsibilities in three parts: signalling the condition, handling it, and restarting execution. The call stack is unwound only if that was the handling strategy chosen; it doesn't have to be. This enables novel recovery strategies and protocols, and can be used for things other than error handling.
Beyond Exception Handling: Conditions and Restarts, chapter 19 of Peter Seibel's Practical Common Lisp, informs much of the descriptions (as one can plainly see; I hope he doesn't mind 😁), terminology and tests.
I haven't used Clojure in years, so this seemed as good an excuse as any 😄
It's an opportunity to remove some cobwebs and check out some "new" stuff, such as deps.edn
, transducers, maybe spec
(I said it has been some time...). Let's see how far I go...
Well, with binding
and dynamic variables, most of the machinery is already there, so life is a lot easier 😄
The end result should look something like this:
(ns user
(:require [org.sbrubbles.conditio :as c]))
; This example draws from Practical Common Lisp, but with some shortcuts
; to simplify matters
(defn parse-log-entry [line]
(if (not= line :fail) ; :fail represents a malformed log entry
line
; adds :user/retry-with as an available restart
(c/with [::retry-with parse-log-entry]
; signals :user/malformed-log-entry
(c/signal ::malformed-log-entry :line line))))
(defn parse-log-file []
; creates a function which calls parse-log-entry with :user/skip-entry
; as an available restart. Any entries which return 'skip-entry will
; be skipped
(comp (map (c/with-fn {::skip-entry (fn [] 'skip-entry)}
parse-log-entry))
(filter #(not= % 'skip-entry))))
(defn analyze-logs [& args]
; handles :user/malformed-log-entry conditions, opting to restart
; with :user/skip-entry
(c/handle [::malformed-log-entry (fn [_] (c/restart ::skip-entry))]
(into []
(comp cat
(parse-log-file))
args)))
; every vector is a 'file'
(analyze-logs ["a" "b"]
["c" :fail :fail]
[:fail "d" :fail "e"])
;; => ["a" "b" "c" "d" "e"]
deps.edn
, tools.build
and codox for docs.
- Despite what the name might suggest, I didn't try to maintain parity with conditio-java, although almost everything is there.
I could've used vars instead of keywords.For now, I'm going with keywords. Vars seem to require too much magic...