/conditio-clj

(Yet another) simple condition system for Clojure.

Primary LanguageClojureMIT LicenseMIT

A simple condition system for Clojure, without too much machinery.

CI Docs Git package Maven package

Latest

Git

org.sbrubbles/conditio-clj {:git/url "https://github.com/hanjos/conditio-clj" 
                            :git/tag "0.2.0"  :git/sha "d99c9c3"}

Maven

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"}

What

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.

Why?

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

How?

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"]

Using

deps.edn, tools.build and codox for docs.

Caveats and stuff to mull over

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