Search code examples
clojurefunctional-programmingcompojure

Shared resource in functional programming


Context

I am writing an app using Clojure and following the functional programming paradigm. In this app I have two HTTP endpoints: /rank and /invite. In /rank the app ranks a list of costumers based on their scores. In /invite the app receives an invite from one costumer to another and this should yield changes on the scores of some costumers.

Problem

Data from costumers are kept in a single vector of maps called record.

Putting aside referential transparency for a moment, record should be a shared resource between the endpoints, one reads it and uses it in a ranking function to respond an HTTP request and the other reads it and updates scores in it.

Now, with functional programming in mind, record cannot be updated, so the /invite endpoint should read it and return a new record', problem is, /rank endpoint is setup to use record, but when a new record' is generated it should use it instead of the original one.

My ideas for solving this

I understand that in this context the whole app cannot be fully, functionally speaking, pure. It reads initial input from file and receive requests from the external environment, all of which renders functions that deal with these parts not referentially transparent. And almost every program will have these small portions of non-functional code, but in an attempt of trying not to add more of these non-functional functions to the app and this app being just to exercise some functional programming, I am not persisting record to a database or something, because if this were the case, problem would be solved as I could just call a function to update record on the database.

My best idea so far is: The endpoints are created with a routes function from Compojure, so in the /invite endpoint I should process the new record' vector, and recreate the /rank endpoint supplying it with record'. This part of recreating /rank is what I am struggling at, I am trying to call routes again and define again all the endpoints in the hope that it will override the original call of routes, but as expected, as I believe Compojure follows functional programming, once created, the routes are immutable and a new call of routes won't override anything, it will just create new routes that will be left in the void, not attached to the HTTP requests.

So, is it possible to do what I want with pure functional code? Or it is unavoidable to break referential transparency and I should persist record to a file or database to update it?

PS.: I don't know if this is relevant, but I am new to Clojure and to any sort of web interaction.


Solution

  • Clojure (in contrast to Haskell) is not pure and has its own constructs to manage changes to shared state. It doesn't isolate impureness using a type system (like IO monad in Haskell) but promotes using pure functions and managing the state with different types of references (atom, agent, ref) defining a clear semantics how and when the state is changed.

    For your scenario Clojure's atom would be the simplest solution. It provides a clear contract on how its state is managed.

    I would create a var holding your record within an atom:

    (def record (atom [])) ;; initial record is empty
    

    Then in your rank endpoint you could use its value using deref or with @ as a syntactic sugar:

    (GET "/rank" []
      (calculate-rank @record))
    

    Whereas your invite endpoint could update your record value atomically using swap!:

    (POST "/invite/:id" [id]
      (invite id)
      (swap! record calculate-new-rank id)
      (create-response))
    

    Your calculate-new-rank function would look like that:

    (defn calculate-new-rank [current-record id]
      ;; do some calculations
      ;; create a new record value and return it
      (let [new-record ...]
        new-record))
    

    Your function will be called with the current version of the data stored in the atom and other optional parameters and the result of your function will be installed as the new value of your atom.