Bounce is a protocol-less, function composition-based alternative to Component
EPL-1.0 License
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:
This README starts with an extensive discussion of '/why?/' - if you're looking for the '/how?/', please [[#concepts-in-bounce][skip ahead]].
#+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!
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:
** 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:
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:
But their tradeoffs are well-known, particularly:
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.
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:
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
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.
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, 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
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:
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 :)
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.