Skip to content

coutego/injectable

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Introduction

Injectable is a(nother) lightweight DI/IoC container for Clojure(script).

It is similar to Integrant in scope. Same as Integrant, it removes some limitations from Component linked to the decision to favour inmutable records. Injectable, like Integrant, doesn’t impose this limitation, giving a more natural injection mechanism and even allowing for circular dependencies.

Unlike Integrant, the implementation does not depend on multimethods, but in plain functions. This is a matter of style and it doesn’t prevent the mechanism for building the objects to be externalised from the configuration of the system.

This library takes inspiration in the Spring container. The configurable elements are named ‘beans’ in the codebase due to this fact.”

Why

This question can mean different things:

  • Why another DI library?
  • Why DI in Clojure?
  • Why DI at all?

These are my personal replies to those questions.

Why another DI library?

There are established DI libraries in Clojure. The main ones are Component and Integrant. Mount is usually considered a DI library, although it arguably is more a kind of service locator rather than DI.

The reason to not use any of these three is that the problem that Injectable is trying to solve is different: make it as easy as possible the creation of containers with high number of managed elements. This is needed when embracing DI to its fullest, where many or even most functions used in the application are managed via the container.

Why DI in Clojure

DI is a technique which is used in OO systems. The technique itself is not linked to OO and it has a place in FP (see, for example https://fsharpforfunandprofit.com/posts/dependency-injection-1/). This technique is less relevant in Clojure since most of the application can be implemented as pure functions operating in immutable data. Nevertheless, it’s quite typical that some parts of the applications depend on state and, unfortunately, the common approach is to use that state directly or indirectly in most of the code of the application. This is usual the case in parts of the code dealing with databases (or files, services, …) and parts dealing with UI state.

Injectable offers an alternative solution based in DI which is lightweight enough as to offer little overhead while allowing all the code to consist in “pure” functions that depend only on their input parameters and nothing else. This makes a huge difference in terms of local reasoning and testability.

Why DI at all?

In short: local reasoning.

DB people understand the importance of normalization, both in practice as in theory (since the concept was first defined mathematically). Normalization is a case of a more fundamental principle: Don’t Repeat Yourself (DRY). We all know the importance to keep things in one place as to avoid inconsistencies.

Same as DRY is a more general case of normalization, local reasoning is an even more general and fundamental principle of programming. It consists in having the absolute certainty of the effects of a change in the code of configuration, independently of whatever else is done in the rest of the codebase.

Pure functions in FP automatically allow for this local reasoning: as long as a function doesn’t perform any impure operations and, therefore, only operates the parameters given to it without changing them, then it’s possible to reason about the function with total independence of the environment this function is running in. The only dependency on the environent is how this environment is calling the function. But that is for the environment to reason about.

Functions that only depend on their arguments, but allow those arguments to be impure (i.e. side effectful) are similar to pure functions in that they allow for local reasoning: if called with the same parameters (with the same state, in case they are stateful!) then they produce the same result. It’s easy to reason about those functions and test them. We can call this functions “quasipure”. Clearly, pure functions are preferrable to quasipure functions, but the latter are preferrable to those impure functions that interact with the environment by means different that using the arguments passed to them. This includes not only functions doing I/O, but also functions accessing global or dynamic variables, directly or indirectly calling functions that are impure themselves, etc.

DI allows us to define all the functions in our codebase to be quasipure. This allows us to left with the simpler problem of using external impure functions, which will be passed to our quasipure functions.

The strategy for implementation is, therefore:

  1. Check whether we can achieve our objective using pure functions. If so, do it.
  2. Check whether we can achieve your objective using quasipure functions. If so, do it and use DI to inject whatever impure functions are needed as parameters.
  3. If you can’t achieve your objective with 1. or 2. then encapsulate your state and external impure functions, exposing the operations to (safely) operate in that state as impure functions to be injected in the normal functions.

This is the most powerful and simple way to allow for local reasoning in practice that I know of.

How

The DI container is extremely minimalist. It’s defined in the container.cljc file. It uses a very simple, but impractical, format to define “systems”. Every element (I call them “beans”, as a reference to the fabulous Spring container, which pioneered the technique in Java) is defined as an entry on a map of the form:

(def conf
  {:element1 {:constructor [(fn [] "Value of element1")]
   :element2 {:constructor [(fn [k]
              (str "Element2 references element1, which has a value: ")
                   :element1]})

Needless to say, this way of defining elements is not practical.

There is a second way of defining elements, which gets internally translated to this simplified form. In this way, vaguely inspired by the Hiccup syntax, the previous configuration would be expressed as this:

(def conf
  {:element1 "Value of element1"]
   :element2 [str "Element2 references element1, which has a value: "
                  :element1])

Another less simplistic example follows:

(def conf
  {:db-user        "john-doe" ; (1)
   :db-pass        "hunter2"
   :db-conn        [create-db-conn :db-user :db-pass] ; (2)
   :all-products   [all-products :db-conn '?query] ; (3)
   :delete-product [delete-product :db-conn '?id]
   :my-products    [:all-products [:= :user-products-query]] ; (4)
   :ui-template    [ui-template :ui-top-bar '?main-content]
   :ui-main-page   [:ui-template [:=bean> [ui-main-page-component]]]}) ; (5)

; (1) Simple value
; (2) Function create-db-conn is called on bean refs :db-user and :db-pass
; (3) A function of 1 parameter is assigned to :all-products
; (4) [= x] Notation for literal value, x is passed raw
;     Note that the bean ref :all-products is on function position
; (5) Inner beans: avoid having to define another bean

Another more complex example taking from a sample web application currently in development

(def conf
  {:main-component   [:ui-page-template default-content]
   ::top-row         [ui-top-row
                      ::app-icon
                      ::app-name
                      ::topbar-center
                      ::topbar-right
                      ::on-logo-click]
   ::app-icon        [:= [:div "*app-icon*"]]
   ::app-name        [:= [:div "*app-name*"]]
   ::topbar-center   [:= [:div.ui.text.container
                          [ui-top-row-entry nil [:i.ui.upload.icon] "Upload"]
                          [ui-top-row-entry nil [:i.ui.clock.icon] "Recent"]
                          [ui-top-row-entry
                           nil
                           [:i.ui.envelope.icon]
                           "Notifications"
                           [:span.ui.label {:style {:font-size :xx-small}} 2]]]]
   ::topbar-right    [ui-login-top-row]
   :ui-page-template [ui-page-template ::top-row '?]})

You get the idea.

About

Data oriented Clojure dependency injection library

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published