Yo-yo is a lightweight library for starting, reloading (via clojure.tools.namespace) and stopping Clojure systems in a functional style.
[jarohen/yoyo "0.0.4"]
There will likely be many breaking changes until 0.1.0!
Yo-yo came into existence after a few threads of conversation on the Clojure mailing list, Twitter, and in real life (I know!). It seemed that a number of people whose opinions I respect highly weren’t quite sold on Component and its derivatives (openly, including Phoenix) - a few even said that Phoenix reminded them of Spring.
This obviously won’t do!
So we did what Clojurians do: going to look at other languages to see what they do, and pinching the best ideas; mostly Haskell, but a couple of others as well - and Yo-yo is the result of those discussions.
- A lightweight library (currently ~100 LoC!) for starting, reloading and stopping Clojure systems in a functional style.
- That’s it!
- A configuration library
- A dependency injection library
- A Leiningen/Boot plugin
- A source of painful Java/OO/Spring memories ;)
- …
- A module for starting/stopping an Aleph web server
- A module for compiling/building CLJS
- A module for starting/stopping JDBC connection pools
- A module for integrating Component components/systems
- Templates for creating webapps and REST APIs
(There are a couple of Lein templates - ‘yoyo-webapp’ and ‘yoyo-api’, but here follows a fuller explanation!)
I’m presuming you’ve added the Yo-yo dependency. It’s at the top. Go have a look and copy it into your project.clj/build.boot - I’ll wait :)
Before we require
in Yo-yo, let’s think about a toy database
connection pool - a resource that needs to be started and
stopped. Let’s also assume we’re given a function that takes a started
connection pool, and returns when the system needs to be stopped
(we’ll pass this in as f
). What does this DB pool look like?
(defn with-db-pool [db-config f]
(let [db-pool (start-pool! db-config)]
(try
(f db-pool)
(finally
(stop-pool! db-pool)))))
I’m cheating slightly here, assuming something else actually does the starting and stopping, but we only care about the lifecycle here, right?!
These actually compose pretty well - they’re just functions! Let’s start a web server, using a Ring handler that depends on the DB pool:
;; first - a couple of helper functions...
(defn with-web-server [{:keys [handler port]} f]
;; left as an exercise to the reader - it's pretty similar to the
;; one above!
)
(defn make-handler [{:keys [db-pool]}]
(routes
(GET "/" []
;; you've all seen this before too...
;; note that you have access to the db-pool here :)
)))
;; now let's wire it up:
(with-db-pool {...}
(fn [db-pool]
(with-web-server {:handler (make-handler {:db-pool db-pool})
:port ...}
(fn [web-server]
;; TODO: Ah. We've run out of turtles.
;; What goes in here?!!
))))
What goes in the middle there? Until that point, it’s turtles all the way down, but at some point we actually need a function that waits for some signal to ‘stop the system’!
That’s where Yo-yo comes in - there’s a function called run-system!
,
which you can call as follows:
(:require [yoyo])
(yoyo/run-system!
(fn [latch]
(with-db-pool {...}
(fn [db-pool]
(with-web-server {:handler (make-handler {:db-pool db-pool})
:port ...}
(fn [web-server]
(latch))))))) ; Aha!
What’s happening here?
- When the system starts (when your code calls
(latch)
),run-system!
returns a promise. deliver
any value to that promise,(latch)
will return, and your system will stop.
These nested functions can turn into a ‘staircase’ very quickly, disappearing off the right-hand-side of your editor.
Enter ylet
, a macro to simplify the nested callbacks:
(:require [yoyo :refer [ylet]])
(yoyo/run-system!
(fn [latch]
(ylet [db-pool (with-db-pool {...})
web-server (with-web-server {:handler (make-handler {:db-pool db-pool})
:port 3000})]
(latch))))
The expressions on the right-hand-size of a ylet
are all missing
their final parameter - the callback. ylet
passes the callback for
you, binding the callback parameter to the binding on the
left-hand-side. For example, with-db-pool
normally expects two
arguments, but here we’re only passing one - ylet
will pass (fn
[db-pool] ...)
as the second.
De-structuring’s allowed too, in the usual manner.
Similarly to Clojure’s for
, you can also ‘break out’ of the ylet
semantics in the bindings, by passing :let [...]
:
(yoyo/run-system!
(fn [latch]
(ylet [db-pool (with-db-pool {...})
:let [server-opts {:handler (make-handler {:db-pool db-pool})
:port 3000}]
web-server (with-web-server server-opts)]
(latch))))
You’ll probably not want to run this system just once, so there are a number of lifecycle functions included for easy starting, stopping and reloading:
(yoyo/set-system-fn! 'system-fn-sym)
- stores a symbol pointing to a system function (a function accepting a latch, like the one above) for use with the functions below.(yoyo/start!)
- starts a system by calling the storedsystem-fn
.(yoyo/stop!)
- stops the currently started system(yoyo/reload!)
- stops a system (if one’s running), reloads all your code (through clojure.tools.namespace), and starts it up again.
In practice, this looks like:
(ns myapp.main
(:require [yoyo :refer [ylet]]
...))
(defn make-system [latch]
(ylet [db-pool (with-db-pool {...})
web-server (with-web-server {:handler (make-handler {:db-pool db-pool})
:port 3000})]
(latch)))
(defn -main [& args]
(yoyo/set-system-fn! 'myapp.main/make-system)
(yoyo/start!))
In the REPL, later, you can call (yoyo/stop!)
and (yoyo/reload!)
to your heart’s content :)
Why pass a symbol to set-system-fn!
? So that you can make a change
to make-system
(or anywhere else in your codebase, for that matter)
and have that change picked up on reload, without needing to run
-main
again!
That’s all there is to Yo-yo core!
There are a number of modules for common use-cases - web servers, connection pools, CLJS compilers, etc - each with their own documentation. Have a browse through the repo!
If you do create your own, feel free to either start your own repo or
submit a PR to this repo. For the sake of consistency, I’d probably
recommend an artifact named [<your-group>/yoyo.<your-module>]
.
Components/Component systems can be seamlessly be brought into a Yo-yo system using the ‘component’ module in this repo.
For an individual Component:
(:require [yoyo.component :as yc])
(yc/with-component (map->MyComponent {...})
(fn [started-component]
;; next turtle
))
The component will be started before being passed to this function, and stopped afterwards.
For a whole system:
(:require [yoyo.component :as yc]
[com.stuartsierra.component :as c])
(yc/with-component-system (c/system-map
...)
(fn [started-system]
;; next turtle
))
Likewise, the system will be started before being passed to that function, and stopped afterwards.
There are a couple of Leiningen templates that’ll get you up and
running quickly - yoyo-webapp
and yoyo-api
. Run (e.g.) lein new
yoyo-app your-app-name
to get started!
Yes please! Yo-yo’s still in its infancy, so I’d be particularly interested to hear what you think - are we on the right lines here?
I can be contacted via Twitter, Github, e-mail (on my profile), Slack, Gitter, you name it!
Yes please to these too! Please submit through Github in the traditional manner.
A big thanks, in particular, to Kris Jenkins - who’s provided a lot of time, thoughts, advice and inspiration for the ideas behind and around Yo-yo. Cheers Kris!
Thanks also to those involved in discussions about Component which helped to shape Yo-yo, including (but not limited to) Daniel Neal, Martin Trojer, Yodit Stanton and Neale Swinnerton.
Cheers!
James
Copyright © 2015 James Henderson
Yo-yo, and all modules within this repo, are distributed under the Eclipse Public License - either version 1.0 or (at your option) any later version.