Skip to content
This repository has been archived by the owner on Oct 24, 2019. It is now read-only.
/ yoyo Public archive

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

Notifications You must be signed in to change notification settings

jarohen/yoyo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

I’m in the process of abandoning Yo-yo - while it was a good experiment, it seems in practice that this isn’t an easy way to write readable code, and that refactoring Yo-yo based code is more difficult than ‘normal’ Clojure.

Thanks for checking it out, and for your help and feedback in implementing it!

James

Yo-yo is a lightweight library for composing ‘Components’ in a functional style, in Clojure and ClojureScript, using Clojure’s ’Cats’ monad library.

(Unless specified otherwise, all of the below works in both Clojure and ClojureScript)

Dependency

[jarohen/yoyo "0.0.6-beta11"]

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 means of composing stoppable ‘Components’
  • Optionally, (via yoyo.system) a means of composing Components that depend on other Components.
  • That’s it!

What Yo-yo isn’t:

  • A configuration 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 Aleph/http-kit web servers.
  • A module for compiling/building CLJS
  • A module for starting/stopping JDBC connection pools
  • 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 :)

Yo-yo is based on two main data types: Components and Dependents.

Components

Components are just a pair - a value, and (optionally) a means of ‘stopping’ the value (whatever that may mean for the value in question), created using yoyo.core/->component. For example, for a database pool, you could create a Yo-yo Component as follows:

(:require [yoyo.core :as yc])

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

The interesting side to Components is that they compose very simply - when we compose two Components together, we’d expect a combined Component to have a combination of the two values, and a stop function consisting of the two stop functions, in reverse order.

So, let’s say we wanted to compose a database pool with a scheduler that required a database pool, we’d want to compose them as follows:

(defn start-scheduler! [db-pool]
  (let [scheduled-task {:times ...
                        :schedule-fn (fn []
                                       (run-job! ... {:db-pool db-pool}))}
        stop-scheduler! (schedule! scheduled-task)]

    (yc/->component scheduled-task
                    (fn []
                      (stop-scheduler!)))))

(defn combine-components [component f]
  ;; ...
  )

(defn start-combined-component! [db-config]
  (let [db-pool-component (open-db-pool! db-config)]
    (combine-components db-pool-component
                        (fn [db-pool]
                          (start-scheduler! db-pool)))))

We’d expect start-combined-component! to return a Component with the combined value, and a stop-function that first stops the scheduler, then the database pool, as specified. This combined Component can then be combined with other Components (combined or otherwise), using the same combine-components function, to form a larger system.

What I’ve just described, is the monadic ‘bind’ function, over Components - which, if it were written in Haskell, would have the following type:

bind :: Component a -> (a -> Component b) -> Component b

We can then use all of the support in Cats to build up our systems. I won’t duplicate its documentation here, but one macro in particular is very useful: mlet.

mlet is structured similarly to Clojure’s let bindings, except all of the values on the right-hand-side are monadic values (in this case, Components) which are then extracted and bound to the symbols on the left, like Haskell’s do notation:

(:require [cats.core :as c])

(defn start-combined-component! [db-config]
  (c/mlet [db-pool (open-db-pool! db-config)
           scheduled-task (start-scheduler! db-pool)]
    (yc/->component {:db-pool db-pool
                     :scheduled-task scheduled-task})))

The value returned by the mlet is itself a monadic value, and hence can itself be combined again into higher-level Components.

As users of Yo-yo, we don’t have to worry about combining the stop-functions of the two Components - the bind functionality, implemented by Yo-yo and called by mlet, handles all of that. Likewise, the Yo-yo bind implementation includes error handling so that, if a subsequent Component fails, the earlier Components are stopped - you aren’t left with a half-started system.

(Here, we’re using the 1-arg version of yc/->component, because the combined Component doesn’t require any ‘stop’ behaviour of its own, above the stop-functions of the two individual Components.)

What types can a Component wrap?

Whatever you like! Vanilla maps, records, reify‘d protocols, functions, objects, you name it…

Testing a Component system

Components can be tested on their own, or as part of a combined Component, using Yo-yo’s yc/with-component function:

(deftest test-component
  (yc/with-component (open-db-pool! {...})
    (fn [db-pool]
      ;; test away!
      )))

(deftest test-combination
  (yc/with-component (start-combined-component! {...})
    (fn [{:keys [db-pool scheduled-task]}]
      ;; test away!
      )))

with-component passes the started Component to the given function, and stops it when the function returns.

Starting/Stopping/Reloading a live Component system

Yo-yo has a few REPL utilities in the top-level yoyo namespace: yoyo/start!, yoyo/stop! and yoyo/reload! - these allow you to quickly start, stop and reload your system from the REPL. To set these up, call yoyo/set-system-fn!, passing it a 0-arg function returning a Component, and then REPL away to your heart’s content.

yoyo/reload!, by default, will stop the system, reload any changed namespaces using clojure.tools.namespace, then restart the system.

My -main functions, therefore, usually look something like this:

(ns myapp.main
  (:require [cats.core :as c]
            [yoyo :as y]))

(defn make-system []
  (c/mlet [db-pool (open-db-pool! {...})
           ...]
    ...))

(defn -main []
  (y/set-system-fn! #'make-system)

  (y/start!))

Storing a reference to the started system

It’s often helpful to store a reference to the started system, to introspect for debugging purposes. You can wrap the system in yc/with-system-put-to, as follows:

(:require [cats.core :as c]
          [yoyo.core :as yc])

(defn make-system []
  (-> (c/mlet [db-pool (open-db-pool! {...})
               ...]
        ...)

      (yc/with-system-put-to 'user/foo-system)))

The started system is then available to query at the REPL, at user/foo-system. When the system is stopped, the reference is cleared.

(In CLJ, this can be a symbol or an atom; in CLJS, just an atom (for now?))

Dependents

Yo-yo’s dependency injection is based on a system map - a map of values, identified by a dependency key.

Yo-yo will, given a set of values that declare their dependencies, construct this system map in the correct order and, when required, stop the system in the opposite order.

Yo-yo’s second data type, therefore, is the Dependent - a value that depends on another value. A Dependent has two possible instance types - firstly, another pair, this time consisting of:

  • the dependency key of the value that it depends on, and
  • a function that, given a system with that key, returns another Dependent

There’s also a simple base case, where we’ve resolved all the dependencies that we need - this is a simple wrapper around the resolved value.

Again, if we were writing Haskell, we might write the type out like this:

Dependent a =   Resolved a
              | Dependent (DependencyKey, (System -> Dependent a))

(The fact that there are two types here is, in fact, completely transparent to users of Yo-yo, but is included here for interest!)

The Dependent Monad

These Dependent values are also easily composed: given two values, each with a dependency, we can compose them into a single Dependent that requests the first dependency but then, when it’s function is given the first value, returns a Dependent depending on the second value.

Of course, given that we can compose two Dependents in this way, we can compose arbitrarily many.

It shouldn’t be much of a surprise to readers who’ve made it this far, but the Dependent value is also monadic, and can therefore be bound, fmap‘d, and mlet‘d as before.

Dependents are quite similar to the Reader monad, with the main difference that each value declares its dependency in advance - this is so that Yo-yo’s dependency resolution can construct dependencies in the correct order.

Yo-yo, therefore, provides two main constructors for the Dependent type:

(:require [yoyo.system :as ys])

;; 'return' - yields a 'Resolved' dependent
(ys/->dep <value>)

;; 'ask' - returns a Dependent depending on the given key
(ys/ask :db-pool)

;; 'ask' also takes a path, if the value that you're depending happens
;; to be a map
(ys/ask :config :aws :secret-key)

We can then combine Dependents together easily - let’s say, to fetch a user from a database:

(:require [yoyo.system :as ys]
          [cats.core :as c]
          [clojure.java.jdbc :as jdbc])

;; prefixing with 'm-' because we're returning a value of type
;; 'Dependent User', not a 'User'
(defn m-get-user [user-id]
  (c/mlet [db-pool (ys/ask :db-pool)]
    (ys/->dep
     (jdbc/query db-pool
                 ["SELECT * FROM users WHERE user_id = ?" user-id]))))

Callers of m-get-user then don’t need to know/worry that it depends on the database pool - they can compose with it all the same:

(defn m-send-confirmation-email! [user-id]
  (c/mlet [{:keys [email-address] :as user} (m-get-user user-id)]
    (ys/->dep
     (send-email! email-address (format-confirmation-email user)))))

The return value of that function is still a Dependent on the :db-pool, but there’s no reason for m-send-confirmation-email! to know!

Evaluating a Dependent

Before we get into the dependency injection side of Yo-yo system, it’s still possible to evaluate a Dependent, by providing a pre-constructed system map to ys/run. This, again, is particularly useful for testing:

(:require [yoyo.system :as ys])

(deftest test-the-config
  (-> (c/mlet [{:keys [access-key secret-key]} (ys/ask :config :aws)]
        (ys/->dep
         (is (= secret-key "very-secret"))))

      (ys/run {:config {:aws {:secret-key "maybe-this-isnt-so-secret-really"}}})))

ys/run, here, is taking a Dependent and a system map, and returning the resolved result of the Dependent - in this case, the return value of (is (= secret-key "very-secret"))

Building up a system, with Dependents

This is all very well and good, if you have a system to hand!

To create a system, with Yo-yo, we provide a set of Dependencies - Dependents with names. We create these with ys/named - passing it:

  • a function returning a Dependent (Component a) - a value that depends on other values, that can also be ‘stopped’
  • a dependency key
(defn make-config []
  (-> (fn []
        (ys/->dep
         (yc/->component (read-config (io/file "config-file")))))

      (ys/named :config)))

(defn make-db-pool []
  (-> (fn []
        (c/mlet [db-config (ys/ask :config :db)]
          (ys/->dep
           (let [db-pool (open-db-pool! db-config)]
             (yc/->component db-pool
                             (fn []
                               (close-db-pool! db-pool)))))))

      (ys/named :db-pool)))

We then pass these two Dependencies to ys/make-system which returns a started, combined Component:

(defn make-system []
  (ys/make-system #{(make-config)
                    (make-db-pool)}))

ys/make-system then looks at what each dependency ys/ask-s for, directly or indirectly, and determines the required startup order.

Systems constructed in multiple namespaces can be combined either by referring to individual functions, like make-config, or by clojure.set/union-ing over multiple subsets.

Notice that, at the top level, we don’t have to provide a dependency set for each component - we pass ys/make-system an unordered set. This is done automatically based on what each component has ys/ask-ed for. This means that there’s no need for the top-level to change if, one day, a component requires an extra dependency - just ys/ask for it at the level of abstraction that it’s needed, and Yo-yo’s dependency resolution will automatically take it into account.

We can then use our make-system function, as before, to create a live application:

(ns myapp.main
  (:require [cats.core :as c]
            [yoyo :as y]
            [yoyo.system :as ys]))

;; ...

(defn make-system []
  (ys/make-system #{(make-config)
                    (make-db-pool)}))

(defn -main []
  (y/set-system-fn! #'make-system)

  (y/start!))

We can also test the whole system, in the same way as before:

;; ...

(defn make-system []
  (ys/make-system #{(make-config)
                    (make-db-pool)}))

(deftest test-system
  (yc/with-component (make-system)
    (fn [{:keys [config db-pool]}]
      ;; test away!
      )))

Getting dependencies asynchronously - ‘mgo’

(This part of Yo-yo, in particular, is the part I’m not so convinced of - please let me know if you have ideas around this, whether it be approach, implementation, or even a better explanation!)

Sometimes, we don’t always know the dependencies of a dependent when the system is starting. For example, we have to provide a web handler function to a web server at startup, but we may not know its dependencies until it is called with a request map.

In particular, we have to provide a function of type Request -> Response, but we’d like to write a function of type Request -> Dependent Response:

;; what we'd like to write:

(defn open-web-server! []
  (c/mlet [server-opts (ys/ask :config :web-server)]
    (ys/->dep
     (let [web-server (start-server! {:handler (fn [req]
                                                 (c/mlet [...]
                                                   (ys/->dep
                                                    {:status 200
                                                     :body ...})))

                                      :server-opts server-opts})]
       (yc/->component web-server
                       (fn []
                         (stop-server! web-server)))))))

This won’t work, because the :handler provided returns a Dependent Response, rather than a Response.

We’d rather not:

  • have the web handler depend on the whole started system
  • have the web handler declare and close over its dependencies outside the function - we’d like to keep the ys/ask-s at the same level of abstraction as the values are used

So, Yo-yo provides the means to ask for an ‘environment’ - which can satisfy dependencies from a different thread. In the main thread, we wrap the body in the ys/mgo macro, to capture the environment. Then, in other threads, we evaluate Dependents by calling either <!! (synchronous, throws an exception in CLJS if a Dependency is not satisfied) or <ch (asynchronous, returning a core.async channel). <!! and <ch can only be used within an mgo block, like core.async’s <! and >! operators, because they rely on mgo’s captured environment.

;; what we actually write:

(defn open-web-server! []
  (ys/mgo ;; wrap with `mgo`
   (c/mlet [server-opts (ys/ask :config :web-server)]
     (ys/->dep
      (let [web-server (start-server! {:handler (fn [req]
                                                  ;; Wrap Dependents with `<!!`
                                                  ;; to evaluate them
                                                  (ys/<!! (c/mlet [...]
                                                            (ys/->dep
                                                             {:status 200
                                                              :body ...}))))

                                       :server-opts server-opts})]
        (yc/->component web-server
                        (fn []
                          (stop-server! web-server))))))))

In this case, we wrap the handler function with ys/<!!, which turns a Request -> Dependent Response function into a Request -> Response function. As the !! in its name suggests, <!! will block waiting for a dependency if it is requested on another thread without having yet been started on the main system thread.

In ClojureScript, we don’t have the luxury of blocking, so there is an equivalent <ch function, which turn a Dependent into a core.async channel returning the satisfied value.

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)

  • Michael Griffiths
  • @mccraigmccraig
  • Daniel Neal
  • Yodit Stanton
  • Neale Swinnerton
  • Martin Trojer

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