bounce

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

EPL-1.0 License

Downloads
3.6K
Stars
17
Committers
1

Bounce is a lightweight library for structuring dependencies in a functional style.

** Dependency #+BEGIN_SRC clojure [jarohen/bounce "0.0.1-rc1"] #+END_SRC

There will likely be many breaking changes until 0.1.0!

Other resources for getting started with Bounce include:

  • lein new splat - a lein template with a simple CLJS app.
  • Any of the projects in the =examples= directory.

This README starts with an extensive discussion of '/why?/' - if you're looking for the '/how?/', please [[#concepts-in-bounce][skip ahead]].

  • Contents :TOC_1_gh:
  • [[#the-big-disclaimer][The Big Disclaimer™]]
  • [[#what-is-bounce-looking-to-achieve][What is Bounce looking to achieve?]]
  • [[#concepts-in-bounce][Concepts in Bounce]]
  • [[#a-bounce-application][A Bounce application]]
  • [[#testing-bounce-systems][Testing Bounce systems]]
  • [[#thanks][Thanks!]]
  • [[#feedback-want-to-contribute][Feedback? Want to contribute?]]
  • [[#licence][LICENCE]]
  • The Big Disclaimer™

#+BEGIN_QUOTE 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. #+END_QUOTE

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 [[https://github.com/stuartsierra/component][Component]]-based systems, and we had a [[https://github.com/jarohen/yoyo][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 [[https://github.com/jarohen/yoyo][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 defined using bounce.system/defcomponent:

#+BEGIN_SRC clojure (ns myapp.db (:require [bounce.system :as b] [nomad.config :as n]))

(n/defconfig db-config ...)

(b/defcomponent db-pool (let [db-pool (start-db-pool! db-config)] (-> db-pool (b/with-stop (stop-db-pool! db-pool))))) #+END_SRC

(I'd also naturally recommend taking a look at [[https://github.com/jarohen/nomad][Nomad]], to configure your components. Well, I would, wouldn't I?!)

** Systems

Systems, in Bounce, are a composition of components. We define dependencies between components using {:bounce/deps #{...}} metadata on the defcomponent

#+BEGIN_SRC clojure (ns myapp.web (:require [aleph.http :as http] [bounce.system :as b] [clojure.java.jdbc :as jdbc] [myapp.db :as db] [nomad.config :as n] [ring.util.response :as resp]))

(n/defconfig web-server-config ...)

(defn make-handler [] (fn [req] (let [db-resp (jdbc/query db/db-pool [...] ...)] (resp/response ...))))

(b/defcomponent web-server {:bounce/deps #{db/db-pool}} (let [{:keys [port]} web-server-config opened-server (http/start-server (make-handler) {:port port})] (-> opened-server (b/with-stop (.close opened-server)))))

#+END_SRC

Points to note:

  • We can declare dependencies by adding an opts map to the defcomponent call
  • Once declared, we can refer to other dependencies like any other var - including
    in other functions called from the component
  • If the system errors, for whatever reason, any Components that were
    started before the error will be stopped.
  • b/start-system returns a System value. You likely won't use the
    result directly, though.

You only need to create components for values that need to be stopped - otherwise, I'd recommend using normal dynamic vars for side-effects:

#+BEGIN_SRC clojure (defn ^:dynamic get-user-from-db [user-id] (jdbc/query db/db-pool [...] ...)) #+END_SRC

I can then mock this side-effect out in testing using Clojure's normal binding macro. Particularly, this obviates the need for a (defprotocol UserRepository ...).

Systems, once created, can be stopped using the b/with-system function.

#+BEGIN_SRC clojure ;; at your REPL

(b/with-system (b/start-system #{#'myapp.web/web-server}) ;; after this block exits, whatever the result, the system will ;; be closed

(prn "System started!" {:web web-server
                        :db db/db-pool}))

#+END_SRC

  • 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 set of 'root' dependencies:

#+BEGIN_SRC clojure (b/set-opts! #{'myapp.web/web-server}) #+END_SRC

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

#+BEGIN_SRC clojure (b/start!)

(b/stop!) #+END_SRC

You can integrate Bounce with a clojure.tools.namespace reload pattern using b/stop! and b/start! as your stop/start hooks. If you're using CIDER, you can set them as your cider-ns-refresh-{before,after}-fn hooks.

My application -main functions usually look like this:

#+BEGIN_SRC clojure (ns myapp.main (:require [bounce.system :as b]))

(defn -main [& args] ;; ... start embedded nREPL

(b/set-opts! #{'myapp.web/web-server})

(b/start!))

#+END_SRC

Bounce will require all of the namespaces required for your root dependencies when it starts - which means that, if the namespaces don't compile (during dev, for example) you can still have a started REPL to fix the issues.

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

  • You can access the currently started components by referring directly
    to their vars
  • Those vars are marked :dynamic, so you can mock them out easily at the REPL
    using Clojure's standard :binding macro.
  • (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:

#+BEGIN_SRC clojure (ns myapp.app-test (:require [myapp.app :as app] [bounce.system :as b] [clojure.test :as t]))

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

(b/defcomponent foo-component {:bounce/deps #{db/db-pool}} (let [started-component (reify EmailProvider (send-email! [_ email] ;; the real implementation ))] (-> started-component (b/with-stop (stop-me! started-component)))))

;; remember 'with-system'? this is particularly useful for testing:

(b/with-system (b/start-system #{#'foo-component}) (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 :bounce/overrides

(b/with-system (b/start-system #{#'foo-component} {:bounce/overrides {#'db/db-pool the-mock}})

;; test me!
)

#+END_SRC

  • 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 WeShop - for the numerous design discussions which led to
    Bounce. Cheers [[https://github.com/danielneal][Daniel]], [[https://github.com/actionshrimp][Dave]], [[https://github.com/kgxsz][Keigo]], [[https://github.com/cichli][Mikey]], [[https://github.com/bronsa][Nicola]], and [[https://github.com/olical][Ollie]]!
  • [[https://github.com/krisajenkins][Kris Jenkins]] - for many a helpful design discussion (although I suspect
    this might not be his cup of tea - sorry!)
  • [[https://github.com/aphyr][Aphyr]] (originally known as Kyle, so I've heard) - for his 2012 article
    '[[https://aphyr.com/posts/240-configuration-and-scope][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-2018 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.