This is a guide written for users experienced with integrant. I'm not particularly experienced with integrant myself, so feedback is appreciated.
Replace
(ns guide
(:require [integrant.core :as integrant]
[ring.adapter.jetty :as jetty]
[ring.util.response :as resp]))
(defmethod ig/init-key :adapter/jetty [_ {:keys [handler] :as opts}]
(jetty/run-jetty handler (-> opts (dissoc :handler) (assoc :join? false))))
(defmethod ig/halt-key! :adapter/jetty [_ server]
(.stop server))
(defmethod ig/init-key :handler/greet [_ {:keys [name]}]
(fn [_] (resp/response (str "Hello " name))))
(def config
{:adapter/jetty {:port 8080, :handler (ig/ref :handler/greet)}
:handler/greet {:name "Alice"}})
(defn -main []
(ig/init config))
with
(ns guide
(:require [flc-x.simple :refer [start! kw-component lifecycle]]
[ring.adapter.jetty :as jetty]
[ring.util.response :as resp]))
(def jetty
(lifecycle (fn [{:keys [handler] :as opts}]
(jetty/run-jetty handler (-> opts (dissoc :handler) (assoc :join? false))))
(fn [server]
(.stop server))))
(def greet
(lifecycle (fn [{:keys [name]}]
(fn [_] (resp/response (str "Hello " name))))
(fn [_])))
(def system
{:adapter/jetty (kw-component jetty {:port 8080} {:handler :handler/greet})
:handler/greet (kw-component greet {:name "Alice"} {})})
(defn -main []
(ig/init config))
Instead of multimethods you use the flc.program/lifecycle
function (ex-exported by flc-x/simple). You can also use flc.program/process
directly which lifecycle
makes use of, or flc.program/clean
instead of lifecycle
when there is no clean-up needed (i.e. when you write (lifecycle ... (fn [_]))
. However, to make it easier for you, the reader, I will stick to only using lifecycle
.
If you want to keep the keyword argument style of integrant, also use flc-x/kw-args as above, but that is optional. (flc-x.simple
re-exports flc-x.kw-args/component
as kw-component
.) There is no real reason to have keyword arguments in greet
, but for jetty
it is useful since it allows you to pass arbitrary configuration to the server implementation.
In general, translate
(defmethod ig/init-key :foo [_ args]
...)
(defmethod ig/halt-key! :foo [_ state]
...)
to
(def foo
(lifecycle (fn [args]
...)
(fn [state]
...)))
Assuming the definitions above, replace
(def config
{:foo {:config-x :X
:a-dependency (ig/ref :bar)}})
with
(def system
{:foo (kw-component foo {:config-x :X}
{:a-dependency :bar})})
In flc-x/kw-args, the configuration and the dependencies are separated.
system
can be built from a configuration map and an implementation map. Instead of using a special tag I will split the configuration values into two submaps: conf and deps.
(def config
{:adapter/jetty {:conf {:port 8080}
:deps {:handler :handler/greet}}
:handler/greet {:conf {:name "Alice"}}})
(def impls
{:adapter/jetty jetty
:handler/greet greet})
config
and impls
can easily be combined to produce
{:adapter/jetty (kw-component jetty {:port 8080} {:handler :handler/greet})
:handler/greet (kw-component greet {:name "Alice"} {})})
which is the form that flc.core/start!
expects.
With integrant you can do
(derive :adapter/jetty :adapter/ring)
and then initialize and reference :adapter/ring
instead of :adapter/jetty
.
flc does not have derived keywords, but you can get the aliasing effect by defining
(defn alias [k]
(component (lifecycle identity (fn [_]))
[k]))
and using it like
(def system {:adapter/jetty ...
:adapter/ring (alias :adapter/jetty)})
Since system
is a map (or map-like) you can merge
(or concat
) the aliases and the rest of the system, if you want that separation.
Instead of using composite keys like
{[:adapter/jetty :example/web-1] {:port 8080, :handler #ig/ref :handler/greet}
[:adapter/jetty :example/web-2] {:port 8081, :handler #ig/ref :handler/greet}
:handler/greet {:name "Alice"}}
use
{:example/web-1 (kw-component jetty {:port 8080} {:handler :handler/greet})
:example/web-2 (kw-component jetty {:port 8081} {:handler :handler/greet})
:handler/greet (kw-component greet {:name "Alice"})}
Just like in a let
you can call the same function twice and bind the result to different names.
There is no special treatment of vector keys in flc.
The example from integrant's documentation for refsets is
(defmethod ig/init-key :const/name [_ {:keys [name]}]
name)
(defmethod ig/init-key :handler/greet-all [_ {:keys [names]}]
(fn [_] (resp/response (str "Hello " (clojure.string/join ", " names)))))
(derive :const.name/alice :const/name)
(derive :const.name/bob :const/name)
(def config
{:handler/greet-all {:names #ig/refset :const/name}
:const.name/alice {:name "Alice"}
:const.name/bob {:name "Bob"}})
Similarly to aliases above, you can introduce a "refset" in our system through a lifecycle that just creates a vector of the dependencies when started.
(defn function [f & deps]
(component (lifecycle f (fn [_]))
deps))
(defn constant [x]
(function (constantly x)))
(def greet-all
(lifecycle (fn [names]
(fn [_] (resp/response (str "Hello " (clojure.string/join ", " names)))))
(fn [_])))
(def system
{:const.name/alice (constant "Alice")
:const.name/bob (constant "Bob")
:const.name (function vector :const.name/alice :const.name/bob)
:handler/greet-all (component greet-all [:const.name])})
If desired, the :const.name
item in the map can be calculated from the rest of the map and merged in. You could use derive
like integrant does, or use naming conventions.
Since you in flc have control of all the data and implementations all the way, you can prep either the configuration itself, prep when building the system, or even prep once the system has been built. For instance, since jetty
and greet
above are just functions you can always prep after the system has been created. That last sentence might require some explanation.
If assuming that all your components use keyword arguments (i.e. takes a single map as argument), you can do
(require '[flc.component :as component])
(defn prep-system [system preps]
(reduce (fn [system [k prep]]
(update system k component/update-program (fn [program]
(fn [m]
(program (merge prep m))))))
system
preps))
(-> config
system
(prep-system {:adapter/jetty {:port 8080}})
start!)
Here we create new functions (what in flc are called programs) by simply creating an outer lambda over them. You can modify the arguments, the return value, or both. This fact is what makes flc truly extensible.
The previous section served as an introduction to the "advanced" technique of wrapping programs. I will now show how it can be done to add suspend/resume behavior to flc ad hoc. (See dev/guide/integrant_suspend.clj for a full example).
(defn suspendable
([program]
(suspendable program process/stop! (constantly program)))
([program suspend resumer]
(suspendable program suspend resumer (fn [_])))
([program suspend resumer stop!]
(fn program' [& args]
(let [process (apply program args)]
(merge {::suspend (fn []
(let [suspended (suspend process)]
(fn [& new-args]
(let [new-program (if (= args new-args)
(suspendable (resumer suspended) suspend resumer)
(do (stop! suspended)
program'))]
(apply new-program new-args)))))}
process)))))
suspendable
takes a program, like jetty
above, and a function suspend
that returns a value which is passed to resumer
if the arguments to the program are the same. resumer
then returns a new program that will restore the state when called. For this program to again support suspend/resume it is again passed to suspendable
before it is called. If the arguments are different then the process is fully stopped and then restarted.
The one-argument form of suspendable
can be applied to any program to make it stop when suspending, and then simply restart when resuming.
If you want to make it "unsafe", i.e. allow some arguments to change, then you can add yet another argument to select/remove the argument you do/don't care about. However, that might not be necessary if you restructure your system--see below.
Here is the jetty example from integrant's documentation.
(defmethod ig/init-key :adapter/jetty [_ opts]
(let [handler (atom (delay (:handler opts)))
options (-> opts (dissoc :handler) (assoc :join? false))]
{:handler handler
:server (jetty/run-jetty (fn [req] (@@handler req)) options)}))
(defmethod ig/halt-key! :adapter/jetty [_ {:keys [server]}]
(.stop server))
(defmethod ig/suspend-key! :adapter/jetty [_ {:keys [handler]}]
(reset! handler (promise)))
(defmethod ig/resume-key :adapter/jetty [key opts old-opts old-impl]
(if (= (dissoc opts :handler) (dissoc old-opts :handler))
(do (deliver @(:handler old-impl) (:handler opts))
old-impl)
(do (ig/halt-key! key old-impl)
(ig/init-key key opts))))
Here the key feature is that the handler is allowed to change, but nothing else. However, the same effect can be achieved by rearranging the system a bit.
The webserver need not depend on the handler. It can be started without a handler (effectively in "suspended" mode) and then have another component, let's call it web-app, "install" the handler. It is the job of the web-app to uninstall the handler when stopping.
In flc it could look like this:
(def webserver
(lifecycle (fn [opts]
(let [handler (atom (promise))
options (assoc opts :join? false)]
{:handler handler
:server (jetty/run-jetty #(@@handler %) options)}))
#(.stop (:server %))))
(def web-app
(lifecycle (fn [webserver handler]
(deliver @(:handler webserver) handler)
webserver)
#(update % :handler reset! (promise))))
(def system
{:webserver/options (constant {:port 8080})
:webserver (component webserver [:webserver/options])
:handler (constant (fn [_] (resp/response "Hello, world!")))
:web-app (component web-app [:webserver :handler])})
Here I have changed around the dependencies. The webserver does not depend on a handler; instead, the web-app installs the handler and if the handler is updated (meaning it is first stopped, then replaced with a new value, and then started) then when the web-app must also be stopped (which uninstalls the handler from the webserver) and then restarted. Partial stop/start can be done using flc-x/partial.
Notice how this change means that we do not need to encode special logic for realizing if we need to restart the webserver or not.
A downside is that we need to encode this logic explicitly. In integrant we can add this to the webserver component without changing the structure of the system. That is a double edged sword though, if we happen to encode a bug in the suspend/resume logic. By inverting the dependencies no extra logic is added, and thus less room for bugs.
This is a non-issue in flc.