115 lines
4.2 KiB
Org Mode
115 lines
4.2 KiB
Org Mode
:PROPERTIES:
|
|
:ID: 9df0dad0-11e9-4c1a-ab27-ff2af21a8f78
|
|
:END:
|
|
#+TITLE: Effects system in Clojure
|
|
#+Author: Yann Esposito
|
|
#+Date: [2020-12-23]
|
|
|
|
tags :: [[id:debfbdb6-03a6-478e-8316-bce0119c0dd7][clojure]]
|
|
source ::
|
|
|
|
|
|
Warning foreword. The Effect system (see type à la carte) from the Freer
|
|
Monad article takes in root in a precise definition.
|
|
Here the notion of Effect system will probably feel totally skewed from the
|
|
perspective of this article.
|
|
But I will talk about how to use the same ideas in a dynamicly typed
|
|
programming language. And in particular Clojure.
|
|
|
|
On a high level perspective the end goal of an Effect system is to be able
|
|
to write program such that the semantic of the program will depend on the
|
|
context the program is executed in.
|
|
|
|
So for example, let's say you write a program like this:
|
|
|
|
#+begin_src clojure
|
|
(defn user-logged-in [user-id]
|
|
(if-let [user (get-user user-id)]
|
|
(do
|
|
(log "user found!")
|
|
(update-last-time-logged user))
|
|
(throw {:error :user-not-found
|
|
:user-id user-id})))
|
|
#+end_src
|
|
|
|
Generally you expect every =*-action= to be a well defined function.
|
|
You can take a look at that function and see what code will be execute.
|
|
|
|
Within an effect system, you would be able to write something like this:
|
|
|
|
#+begin_src clojure
|
|
(with-effects
|
|
[logs-eff
|
|
db-eff]
|
|
(my-action))
|
|
|
|
(with-effects
|
|
[test-logs-eff
|
|
test-db-eff]
|
|
(my-action))
|
|
#+end_src
|
|
|
|
You can imagine that =logs-eff= and =db-eff= to be /Effects/ the first
|
|
could be used to write logs and the second one will be able to access a DB.
|
|
And in the second bloc =test-logs-eff= would be a completely pure
|
|
implementation of the =logs= and a fake =db= implementation.
|
|
|
|
So that way of writting your code is extremely appealing because it will
|
|
help a lot to test it.
|
|
Mainly you only need to change the list of effects used in your main
|
|
function and you will have the same code you will use in production to only
|
|
use pure effect. And thus will be completely reproductible and easy to test.
|
|
|
|
Effects systems while used in some languages (like Purescript, and some
|
|
libs exists in Haskell) does not really appear to be a very hot subject in
|
|
the Clojure community.
|
|
One of the reason might be because in Clojure the concepts and design space
|
|
is occupied with different existing solutions to a similar solution.
|
|
Mainly, compononent and trapperkeeper libraries for example.
|
|
Unlike effects, component and trapperkeeper have a few advantages.
|
|
I will not talk too much about component as I never used it.
|
|
So I will use trapperkeeper to explain myself.
|
|
One major advantage is the ability to describe dependecies between
|
|
different Effects in trapperkeeper.
|
|
Typically, one can simply imagine you would like to create an Effect to
|
|
handle the persistence of your Users.
|
|
It would seems natural that to provide an API to manipulate Users it will
|
|
need a DB related effect.
|
|
And thus you will also probably need to describe the fact that you need the
|
|
DB effect to be "ready" before being able to provide the User Effect.
|
|
|
|
So instead of talking about "Effects" we are talking about "Services".
|
|
They start, they stop and you can use functions they provide and the
|
|
services will take care about their internal state.
|
|
This has similarities with object oriented programming, but it sounds a lot
|
|
better to use this system in a functional programming context.
|
|
|
|
The generic pattern will be something like:
|
|
|
|
#+begin_src clojure
|
|
(defprotocol Protocol
|
|
(fn-of-protocol [this params]))
|
|
|
|
(defservice ServiceName
|
|
Protocol
|
|
[OtherService1
|
|
OtherService2]
|
|
(init [this context])
|
|
(start [this context])
|
|
(stop [this context])
|
|
(fn-of-protocol [this params]))
|
|
#+end_src
|
|
|
|
To start an application you just need to provide a list of instances of
|
|
services to start.
|
|
So in order to test with subbed services you can write the code of pure
|
|
fake services that expose an identical Protocol than the service you wish
|
|
to stub.
|
|
|
|
That's nice. But this is not 100% a positive outcome.
|
|
One issue is that as this is a completely dynamic language you will lose
|
|
all the static analysis tools you are used to.
|
|
Typically you pass the method of the services to your methods.
|
|
And thus you end up only using higher level functions in your code.
|
|
You are no more able to discover which function will be really used.
|
|
This is a feature of that effect system.
|