Skip to content
forked from jarohen/yoyo

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

Notifications You must be signed in to change notification settings

krisajenkins/yoyo

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

57 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Yo-yo

https://badges.gitter.im/Join%20Chat.svg

Yo-yo is a lightweight library for starting, reloading (via clojure.tools.namespace) and stopping Clojure systems in a functional style.

Dependency:

[jarohen/yoyo "0.0.4"]

There will likely be many breaking changes until 0.1.0!

Rationale

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.

What Yo-yo is:

  • A lightweight library (currently ~100 LoC!) for starting, reloading and stopping Clojure systems in a functional style.
  • That’s it!

What Yo-yo isn’t:

  • A configuration library
  • A dependency injection library
  • A Leiningen/Boot plugin
  • A source of painful Java/OO/Spring memories ;)

What Yo-yo (core) isn’t, but is also provided in this repo

  • 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

Getting Started - your first Yo-yo system

(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’!

The full story:

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.

Introducing ‘ylet’

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))))

The version you’ll probably use, 99% of the time:

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 stored system-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 folks!

That’s all there is to Yo-yo core!

Provided modules

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>].

Integrating with Component

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.

Templates

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!

Feedback/thoughts

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!

Bug reports/PRs

Yes please to these too! Please submit through Github in the traditional manner.

Thanks!

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

LICENCE

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.

About

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

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Clojure 100.0%