deft/notes/2020-12-23--16-50-17Z--effects_system_in_clojure.org
Yann Esposito (Yogsototh) c1d2459d0c
save
2024-08-14 11:35:42 +02:00

4.3 KiB

Effects system in Clojure

tags :: clojure blog 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:

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

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:

(with-effects
  [logs-eff
   db-eff]
  (my-action))

(with-effects
  [test-logs-eff
   test-db-eff]
  (my-action))

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:

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

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.