GithubHelp home page GithubHelp logo

pangloss / pure-conditioning Goto Github PK

View Code? Open in Web Editor NEW
53.0 2.0 0.0 139 KB

A simple, fast, purely functional condition / restart system for Clojure.

License: Apache License 2.0

Clojure 100.00%
error-handling restarts conditions

pure-conditioning's Introduction

Pure Conditioning

Table of Contents

Clojars Project

Pure Conditioning is a purely functional, fast, and cleanly decomplected condition / restart system in Clojure. It does not use exceptions and needs no global state at all.

While global state is not required, it makes things more convenient so stateful variants of all pure functions that make use of a dynamicly bound var are available.

This project has been stable and in use for me without issue for nearly 18 months now. I still consider it alpha stage until I dedicate some time to revisiting its design in the coming months. At that time I may or may not introduce breaking changes. After that, if I'm still happy with the design I will bump it to 1.0.0 and try to keep it stable.

Background

I've tried a variety of error handling libraries and techniques, including the approach that Chris Houser presented a few years ago, which was probably the most successful of them, but still not totally satisfying and far from the capability of CL conditions and restarts. This library comes closer to any approach I've seen, while making basically no compromises on speed, syntax or compatibility with the Clojure world.

Tutorial and Background info

To help you understand what this library is and what it's good for, the interactive tutorial on NextJournal is probably a good place to start. I also recommend the excellent chapter on conditions and restarts in Peter Seibel’s excellent book, Practical Common Lisp.

Conditions in Clojure

I'll start here by showing some basic behavior and then move on to restarts and examples pulled directly from the CL documentation that I've come across.

Rather than raising an exception and unwinding the stack, it'd be nice if callers which may have the context could easily just inject missing information or resolve questions on intention easily, regardless of how deep down the stack the problem arises.

This library lets you do that.

Let's get started.

(require '[pure-conditioning :refer [manage condition default]])

In this first example we're doing nothing useful but we can see how the core pieces fit together. In the manage block, we define two handlers, one that will increment odd values, and another that just returns the constant :even. We realize the lazy sequence with vec, but later will see how to capture the handler scope to make lazy sequences safe for condition handling.

(manage [:odd inc
         :even :even]
  (vec (for [i (range 10)
             :let [c (if (odd? i) :odd :even)]]
         (condition c i))))
;; => [:even 2 :even 4 :even 6 :even 8 :even 10]

It's worth noting that the above usage is very simple and doesn't have any fancy restarts or other complex concepts. It just lets me ask higher scopes for context.

I may want to do something like provide a default, or throw a specific exception. That can be easily done:

(manage [:odd inc]
  (vec (for [i (range 10)
             :let [c (if (odd? i) :odd :even)]]
         (condition c i (default :unknown)))))
;; => [:unknown 2 :unknown 4 :unknown 6 :unknown 8 :unknown 10]

But what if the higher scope doesn't know? By default condition will use the required handler to throw a standard ex-info exception as follows:

(manage [:odd inc] ;; no :even handler anymore!
  (vec (for [i (range 10)
             :let [c (if (odd? i) :odd :even)]]
         (condition c i))))
1. Unhandled clojure.lang.ExceptionInfo
   No handler specified for condition
   {:condition :even, :value 0}

Provided Handlers

There are a few provided handlers, or you can easily make your own:

required        ;; the default. Raises the ex-info you see above.

(default value)
(default f)     ;; to resume with the default value. Fast and no exception.

optional        ;; equivalent to `(default identity)`.

(trace message)
trace           ;; prints out the condition and value, then returns the value.

(error message)
(error message ex-data) ;; will raise an ex-info exception.

;; Raise an instance of the given exception class:
(exception ExceptionClass message) 
(exception ExceptionClass message cause) ;; with optional cause exception

No unnecessary exceptions

It's worth noting that any of the handlers that raise exceptions only instantiate the exception class in case that the condition is hit (for instance if the condition hits the default case). The handler is just providing the condition system with the response should it need to use it.

More Handlers

While building up this library and experimenting with its implications I discovered some interesting behavior which I've captured in these more obscure handlers. Now we're getting into capabilities that go beyond other systems that I've seen.

Manage blocks may be constructed as a hierarchy. That hierarchy is preserved and may be navigated by handlers. I've provided some that make intuitive sense here but certainly have not exhausted what's possible.

Remap a condition to another condition and restart the handler hierarchy search

(remap :new-handler) ;; is the simplest version of this.
(remap :new-handler f) ;; lets you change the value before restarting the handler search.
(remap handler-f f) ;; lets you change the new handler based on the value
(remap h f new-default) ;; lets you do the above plus change the default handler for the condition.

Remap a condition to a sibling handler

(sibling ...) ;; all of the options of `remap`, but starting from the current handler hierarchy level

Fall through to a handler defined at a higher scope after modifying the handled value

(fall-through f) ;; Change the value before falling through to look for another handler
(fall-through ...) ;; The other options are identical to `remap`

Optionally continue searching parent handler hierarchy.

(handle (fn [v] (if (good? v) v :continue))) ;; lets you choose whether to respond or just keep searching for a better handler.

Here's a silly example of a bunch of these handlers working together. Creating tangled messes like this isn't recommended in practice but it's interesting to see how all of these handlers can work together seamlessly.

;; This one requires way more than normal
(require '[pure-conditioning :refer [manage condition remap sibling 
                                     fall-through error exception required]])
(manage [:even (remap :thing reverse required)
         :odd (sibling :even vector)]
  (manage [:even (fall-through range)]
    (manage [:even (fall-through dec)
             :thing #(map float %)
             :str (error "nein")]
      (mapv (fn [i]
              (manage [:str #(apply str (reverse %))]
                [(condition :str "!ereht ih" (exception Exception "Failed to str"))
                 (condition (if (odd? i) :odd :even) i required)]))
        (range 10)))))

Restarts

Restarts take the idea of conditions and make them work in both directions. First the called method raises a condition, but provides some options to the handler. The handler handles that condition and chooses which of the provided options it prefers.

It's like they enable the called method to ask the caller a question which the caller answers.

;; Require a couple more functions
(require '[pure-conditioning :refer [manage condition restart restarts restart-with]])
(manage [:request-vacation (restart :summer "2 weeks")]
  (condition :request-vacation
             (restarts nil 
                       :summer #(str "I'll go to the cottage for " %)
                       :winter #(str "I'll go skiing for " %))))
;; => "I'll go to the cottage for 2 weeks"

Or if the startup is growing perhaps the manager needs to check the vacation policy:

(manage [:vacation-policy (restart :winter)]
  (manage [:request-vacation
           (restart-with
            (fn [condition arg default-action]
              (condition :vacation-policy
                         (restarts nil
                                   :summer [:summer "2 weeks"]
                                   :winter [:winter "3 weeks"]))))]
    (condition :request-vacation
               (restarts nil
                         :summer #(str "I'll go to the cottage for " %)
                         :winter #(str "I'll go skiing for " %)))))
;; => "I'll go skiing for 3 weeks"

Cool stuff!

CL Examples

I have incorporated two samples that I found in CL documentation. The first is from the C2 Wiki showing how restarts can be defined at multiple levels and managed from up the stack. The second comes from a paper on the CL condition system involving a robot butler. You can find them here.

Unwind the Stack With Retry! and Result!

The final missing piece is the ability to respond to an error by unwinding the stack in the same way as we are used to with try/catch blocks. We can now do that with result!. Even better, we can also respond by retrying from the current stack frame with retry!.

In this example, if do-something raises the condition :x, the result will be "nevermind" regardless of the logic nested within the do-something function, exactly as if that string were returned by a catch block.

(manage [:x (result! "nevermind")]
  (do-something 3))

much like:

(try
  (do-something 3)
  (catch Exception e
    "nevermind"))

On the other hand, retry! requires a specialized manage block called retryable which functions exactly like manage but adds the ability to handle using retry!. This is only necessary because when retrying you need to say which variable is changed, so retryable adds a binding form before the handlers. Below it is [x], but it can be any arity. The value(s) passed to (retry! value) will be bound to the variables listed in that block and then the block will be rerun.

(retryable [x]
    [:x #(retry! (+ 10 %))]
  (do-something x)))))))

These stack-unwinding operations are fully compatible with all of the other handlers and also work with any arbitrary nesting, as the retry and result operations are explicitly tied to the block that they are defined in.

pure-conditioning's People

Contributors

pangloss avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

pure-conditioning's Issues

result! and retry! do not work as qualified imports

I've always been interested to explore CL-style condition systems, thanks for bringing some form of it over to Clojure :)

Here's a repro (adapted from the readme)

(require '[pure-conditioning :as pc])

(defn do-something [x] x)

(manage [:x (pc/result! "nevermind")]
  (do-something 3))
;; => Execution error (ExceptionInfo) at pure-conditioning.core/result! (core.clj:277).
;;    result! must be used within manage, retryable or retryable-fn* blocks.

It appears that the library macros (manage, retryable) are only able to work when given unqualified symbols:

(if (and (list? f) (#{'result! 'retry!} (first f)))

Figuring this out was pretty confusing, as there are actual result! and retry! vars defined in the pure-conditioning.core namespace (I assume for dev tooling affordances?) But it turns out that the library doesn't care whether they exist or were imported in the current namespace, essentially treating the bare symbols as anaphors.

Not that unhygenic DSLs are a bad thing, but perhaps this fact should be made more explicit in the docstrings / Readme? From a syntax perspective I guess there would be less of a surprise if they were given more DSL-y names like !retry or $retry.. Going the pseudo-hygenic route might be possible too by somehow resolving the vars using &env at macroexpansion time.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.