jarohen/bounce

Bounce is a protocol-less, function composition-based alternative to Component


License
EPL-1.0

Documentation

Bounce is a lightweight library for structuring dependencies in a functional style, for Clojure and ClojureScript.

Dependency

[jarohen/bounce "0.0.1-alpha2"]

There will likely be many breaking changes until 0.1.0!

Other resources for getting started with Bounce include:

  • lein new bounce-webapp <your-project> - a lein template with a simple CLJS app.
  • Any of the projects in the examples directory.
  • There are also a number of common modules (e.g. JDBC pool, web server, etc) in the modules directory.

This README starts with an extensive discussion of ’why?’ - if you’re looking for the ’how?’, please skip ahead.

Contents

The Big Disclaimerâ„¢

Bounce is opinionated. It does, explicitly and intentionally, make tradeoffs which may not be suitable for all tastes/situations, in order to take advantage of certain pragmatic benefits.

This is, of course, probably true of most libraries!

Spoiler alert: here are those tradeoffs:

  • In particular, Bounce sacrifices explicit referential transparency (without loss of the benefits of referential transparency). Functions do make use of a context that hasn’t been passed in as an explicit parameter. This isn’t to say that we lose the testability benefits of referential transparency, though - we can alter this context easily for testing porpoises.
  • Bounce does rely on a global variable. What? Did he really just say that? In a functional language? How dare he?! Yes :)

    Generally speaking, there’s only one ‘System’ running at a time in an application, so let’s not pretend we ‘might need more than one’ (unless we genuinely do), and reap the benefits of making such an assertion.

    Having said that, Bounce does make it easy to test code against a different System - so, again, we don’t lose the testability benefits.

However, this doesn’t mean that we have to, nor should, abandon our ‘Good Functional Programming Practices’™. Pure, referentially transparent functions are easier to test, so we should still split functions (where possible) into pure functions and side-effecting functions, with the aim of minimising the latter.

With those design decisions in mind, let’s continue!

What is Bounce looking to achieve?

Before writing Bounce, myself and a number of colleagues were discussing what we’d look for in such a library. Most of us had written Component-based systems, and we had a Yoyo-based system that wasn’t as simple as we’d hoped, so it was time to look for/write an alternative. We concluded that it should:

  1. Allow the developer to start a component-based system, specifying the inter-dependencies of the components.
  2. Allow the developer to start/stop/reload the system quickly.
  3. Allow the developer to refer to any of those components easily from within other code (i.e. without having to modify the whole caller stack if a function’s dependencies changed)
  4. Allow the developer to introspect/validate the current state of the system easily.
  5. Allow the developer to swap out any of the components for testing purposes.
  6. Prefer vanilla functions over protocols/records, where possible. Especially, we wanted to avoid the property of the Component library whereby components have a ‘pre-started’ state, where it exists but hasn’t been fully initialised.

On ‘referential transparency’

Normally, in functional code, we look for (amongst other things) the property of referential transparency - that the output of any given function depends simply on its declared inputs (i.e. without referencing or affecting any other variables).

In particular, this helps with composability (because each function can be re-used without needing to replicate the context in which it was called) and testing (because the context in which a function is called can be altered for testing purposes)

If we were to adopt this principle above all others, it generally means one of three patterns pervading through the codebase:

  1. Each function provides the dependencies of any of its callees. This means each function declaring as inputs the union of all of the dependencies of its callees, and passing them down as necessary - a fair bit of boilerplate. When a new dependency is required, the whole call-stack must be updated - not good!
  2. Each function accepts the whole system map, pulls out the dependencies it needs, and passes it down unaltered to the callees that require it. While this is simpler, it means that we lose the explicit information about which function depends on what - only that each function depends on the whole system. It also means another parameter to nearly every function, and altering the whole call-stack if a function that didn’t previously have any dependencies now does.
  3. Monads - this is the principle behind Yoyo, and is explained further in its own documentation. In practice, though, this turned out to be viral - as soon as one function needed to return a monadic value, it meant changing a lot of the call-stack. Also, in our experience, developing with monads definitely benefits from a type-system, so it’s probably not the best approach in Clojure.

Pragmatically speaking, explicitly declaring these dependencies through the call stack meant that we were polluting each function with implementation details of its callees.

So what do we really want?

We quite like a couple of properties of global variables - particularly:

  • We can refer to them from anywhere, without needing to pass a reference through the call stack.
  • It’s easy to trace their usages
  • If you have one, developers know where to go to get a configuration value, or access to a resource.

But their tradeoffs are well-known, particularly:

  • Multiple threads having concurrent access to mutable state causes trouble. (Clojure’s core concurrency primitives mitigate this, to an extent, but we’d still like to avoid it)
  • Not being able to alter their value for individual function calls makes testing harder.

Ideally, we’d like an immutable ‘context’ to be passed implicitly from caller to callee. Each callee could look up its dependencies in the context, without troubling its caller by asking for their dependencies to be explicitly passed as function parameters.

We’d also like to be able to alter the context, albeit for the scope of one function call.

This sounds remarkably like dynamic scope?

Dynamic scope, though, has its own tradeoffs - it doesn’t play particularly well with multiple threads, especially if those threads aren’t in the caller’s control (e.g. in a web server).

The idea behind Bounce, therefore, is that by combining the two, we can take advantage of their relative benefits, and reduce/remove their tradeoffs.

Concepts in Bounce

There are two main concepts in Bounce - Components and Systems.

Components

Components in Bounce are any values that can be ‘closed’. Examples here are resources that can be released, servers that can be shut down, or database pools that can be closed.

Components are simply pairs consisting of a value, and a function that will ‘close’ that value. They’re constructed using bounce.core/->component, a function taking either 1 or 2 args - the value and, optionally, the function to close that value.

(require '[bounce.core :as bc])

(defn open-db-pool! [db-config]
  (let [db-pool (start-db-pool! db-config)]
    (bc/->component db-pool
                    (fn []
                      (stop-db-pool! db-pool)))))

(There is, of course, a predefined Bounce component that does exactly this, in the Bounce JDBC pool module.)

Notably, components in Bounce are always ‘started’ - there’s no uninitialised state.

Systems

Systems, in Bounce, are a composition of components. They’re easier to read in code than they are to describe in English, so here goes:

(defn make-my-system []
  (bc/make-system {:config (fn []
                             (bc/->component (read-config ...)))

                   :db-conn (-> (fn []
                                  (let [opened-pool (open-pool! {:db-config (bc/ask :config :db)})]
                                    (bc/->component opened-pool
                                                    (fn []
                                                      (close-pool! opened-pool)))))

                                (bc/using #{:config}))

                   :web-server (-> (fn []
                                     (let [opened-server (open-web-server! {:handler (make-handler)
                                                                            :port (bc/ask :config :web-server :port)})]
                                       (bc/->component opened-server
                                                       (fn []
                                                         (close-web-server! opened-server)))))

                                   (bc/using #{:config :db-conn}))}))

Points to note:

  • The system is a map from a component key to a 0-arg function returning a Component
  • We can declare dependencies by wrapping that function in a call to bc/using, passing it a set of dependency keys.
  • We can bc/ask for dependencies within the Component function. bc/ask also accepts extra varargs - if the component is a nested map (like :config, here), this behaves similarly to get-in.
  • We can bc/ask for dependencies further down the call-stack, too, without changing any of the call-stack above.
  • If the system errors, for whatever reason, any Components that were started before the error will be stopped.
  • If a Component’s required dependency isn’t declared, but is ask-ed for, an error’s thrown immediately with details of the dependency - it fails fast.
  • bc/make-system returns a System value. You likely won’t use the result directly, though.

Systems, once created, can be stopped using the bc/with-system function:

(bc/with-system (make-my-system)
  (fn []
    ;; after this function exits, whatever the result, the system will
    ;; be closed
    ))

A Bounce application

Most of the time, though, we’ll want to start a system, and leave it running. Through development, we’ll also want to stop a system, reload any code that’s changed, and start it again. We do this by giving Bounce a function that, when called, will create and start a system:

(bc/set-system-fn! 'myapp.main/make-my-system)

;; alternatively, (and probably more likely), define a function that
;; returns the map, then call 'set-system-map-fn!':

(defn my-system-map []
  {:config ...
   :db-conn ...
   :web-server ...})

(bc/set-system-map-fn! 'myapp.main/my-system-map)

We can then start, stop and reload the system using Bounce’s REPL functions:

(bc/start!)

(bc/stop!)

(bc/reload!)

;; 'reload!' optionally takes a map of parameters
(bc/reload! {:refresh? true, :refresh-all? false})

My application -main functions usually look like this:

(ns myapp.main
  (:require [bounce.core :as bc]))

(defn make-system-map []
  {...})

(defn -main [& args]
  (bc/set-system-map-fn! 'myapp.main/make-system-map)

  (bc/start!))

Testing Bounce systems

In the ‘Big Disclaimer’ above, I made the claim that Bounce systems are still just as testable.

First, I never underestimate how useful it is to run ad-hoc forms at the REPL - in fact, this is a large proportion of my coding time (when I’m not writing READMEs, at least!). Bounce makes this easy:

  • (bounce.core/snapshot) gives you the current state of the system (particularly useful when you’re in a CLJS REPL, looking at the state of your webapp)
  • (bounce.core/ask :component-key) gives you the value of a component within the system. You knew that, of course, from earlier - but here’s a reminder that it’s useful at the REPL, too.
  • (your-function args...) - because there’s no ‘context’ parameter, you can run your functions as intended, without worrying about cobbling together a context map. (Make sure you’ve got a running system, though!)

Bounce, though, also provides a number of utilities that make testing easier:

(require '[bounce.core :as bc]
         '[clojure.test :as t])

;; an sample component that we'll use throughout these examples

(defn open-foo-component [opts]
  (let [started-component (start-me! opts)]
    (bc/->component started-component
                    (fn []
                      (stop-me! started-component)))))

;; a sample main application system map - we'll adapt this (for testing) later

(defn make-system-map []
  ;; your main application system map
  {:config ...
   :db-conn ...
   :queue-processor ...
   :web-server ...
   :foo-component (fn []
                    (open-foo-component))})




;; 'with-component' is a good way to test individual components,
;; ensuring they're stopped when you're done.

(bc/with-component (open-foo-component ...)
  (fn [component-value]
    ;; test me!
    ))


;; don't forget about 'with-system'! this is particularly useful for
;; testing:

(bc/with-system (bc/make-system {:db-conn (fn []
                                            (let [mock-conn (open-mock-conn! ...)]
                                              (bc/->component mock-conn
                                                              (fn []
                                                                (close-mock-conn! mock-conn)))))
                                 ...})
  (fn []
    (let [foo-user-id 123]
      (t/is (= :expected-mock-result
               (get-user-from-db foo-user-id))))))

;; you can also pass a map of values, if you don't want any of them to be closed:

(bc/with-system {:config {:a 1, :b 2}
                 :mock-something (reify MyProtocol
                                   ...)}
  (fn []
    (let [foo-user-id 123]
      (t/is (= :expected-mock-result
               (get-user-from-db foo-user-id))))))


;; sometimes, you want to mostly use the current system, with a minor
;; alteration - here's 'with-varied-system':

(bc/with-varied-system #(assoc % :mock-something (reify MyProtocol
                                                   ...))
  (fn []
    ;; test me!
    ))


;; even 'make-system' can be called with an optional ':targets'
;; option, to run a subset of your main system:

(bc/with-system (bc/make-system (make-system-map) {:targets #{:config :db-conn}})
  (fn []
    ;; test code which only needs :config and :db-conn here - no need
    ;; to start the web-server/queue-processor, etc
    ))


;; you can adapt a component function within a system-map, using
;; 'fmap-component-fn' - this allows you to alter/use/wrap the
;; component value before it's put into the system map:

(bc/with-system (bc/make-system (-> (make-system-map)
                                    (update :foo-component fmap-component-fn (fn [started-foo-component]
                                                                               (-> started-foo-component
                                                                                   (wrap-foo ...)))))
                                {:targets #{:foo-component}})
  (fn []
    (bc/ask :foo-component) ;; => now returns the wrapped value
    ))

Thanks!

A big thanks to everyone who’s contributed to the development of Bounce so far. Individual contributions are detailed in the Changelog, but particular thanks go to:

  • The team at Social Superstore - for the numerous design discussions which led to Bounce. Cheers Daniel, Dave, Keigo, Mikey, Nicola and Sean!
  • Kris Jenkins - for many a helpful design discussion (although I suspect this might not be his cup of tea - sorry!)
  • Aphyr (originally known as Kyle, so I’ve heard) - for his 2012 article ’Configuration and Scope’, which I was pointed to while writing Bounce - it expresses a fair few of the ideas here in a very comprehendible way.

Feedback? Want to contribute?

Yes please! Please submit issues/PRs in the usual Github way. I’m also contactable through Twitter, or email.

If you do want to contribute a larger feature, that’s great - but please let’s discuss it before you spend a lot of time implementing it. If nothing else, I’ll likely have thoughts, design ideas, or helpful pointers :)

LICENCE

Copyright © 2015 James Henderson

Bounce, and all modules within this repo, are distributed under the Eclipse Public License - either version 1.0 or (at your option) any later version.