Search code examples
clojure

What would be the functional / clojure way of transforming a sequence with changing state?


The problem context relates to stock trading. I'm trying to update the holdings for a particular stock, when a sale is made. Simplified excerpt

;; @holdings - an atom
{ "STOCK1" {:trades [Trade#{:id 100 :qty 50}, Trade#{ :id 140 :qty 50}]}
 "STOCK2" ... }

Now given a sale trade of Trade{:id 200 :stock "STOCK1", :qty 75}, I'm expecting the holdings to reflect

{ "STOCK1" {:trades [Trade#{:id 100 :qty 0}, Trade#{ :id 140 :qty 25}]} }
;; or better drop the records with zero qty.
{ "STOCK1" {:trades [Trade#{ :id 140 :qty 25}]} }

The functional answer eludes me.. All I can see is a doseq loop with atoms to hold state (like sale-qty which may be satisfied by 1 or n trades) - but it feels like C in Clojure.

Is there a more clojure-aligned solution to this? Map doesnt look like a fit because every record processing needs to update an external state (pending sale-qty 75 -> 25 -> 0)

Disclaimer: Clojure Newbie, who wants to learn.


Solution

  • Whenever you want to go over a sequence/collection in Clojure, while passing some additional state around think of reduce Reduce is like a Swiss army knife, for example map and filter can both be implemented with reduce. But how can you store multiple states in a reducing function? You simply use a map as the accumulator.

    Let me distill your problem a bit. Let's create a function that only deals with one problem.

    (defn substract-from
      "Given a seq  of numbers `values`, substract the number `value` from each number
       in `values` until whole `value` is substracted. Returns a map with 2 keys, :result contains
       a vector of substracted values and :rem holds a remainder."
      [values value]
      (reduce (fn [{:keys [rem] :as result} n]
                (if (zero? rem)
                  (update result :result conj n)
                  (let [sub  (min rem n)
                        res  (- n sub)
                        rem  (Math/abs (- sub rem))]
                    (-> result
                        (update :result conj res)
                        (assoc :rem rem)))))
              {:rem value :result []}
              values))
    
    ;; when value is smaller than the sum of all values, remainder is 0
    (substract-from [100 200 300 400] 500)
    ;; => {:rem 0, :result [0 0 100 400]}
    
    ;; when value is larger than the sum of all values, remainder is > 0
    (substract-from [100 200 300 400] 1200)
    ;; => {:rem 200, :result [0 0 0 0]}
    
    

    Now we can use this function to sell stocks. Note that map can accept multiple collections/sequences as arguments.

    (def stocks
      (atom { "STOCK1" {:trades [{:id 100 :qty 50} { :id 140 :qty 50}]}}))
    
    
    (defn sell [stocks {:keys [id stock qty]}]
      (let [trades   (get-in stocks [stock :trades])
            qtys     (map :qty trades)
            new-qtys (:result (substract-from qtys qty))]
        (map (fn [trade qty]
               (assoc trade :qty qty))
             trades
             new-qtys)))
    
    
    (sell @stocks {:id 300 :qty 75 :stock "STOCK1"})
    ;; => ({:id 100, :qty 0} {:id 140, :qty 25})