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

Viewed 180

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.

5 Answers
(require '[com.rpl.specter :as s])


(let [stocks     {"STOCK1" {:trades [{:trade/id 100 :trade/qty 50}, {:trade/id 140 :trade/qty 50}]}}
      sale-trade {:trade/id 200 :trade/stock "STOCK1" :trade/qty 75}
      trade-path [(s/keypath (:trade/stock sale-trade) :trades) s/ALL]
      qty-path   (conj trade-path :trade/qty)
      [new-qty _] (reduce (fn [[new-amounts leftover] v]
                              (let [due-amount (min v leftover)]
                                  [(conj new-amounts (- v due-amount)) (- leftover due-amount)]))
                          [[] (:trade/qty sale-trade)]
                          (s/select qty-path stocks))]
    (->> stocks
         (s/setval (s/subselect qty-path) new-qty)
         (s/setval [trade-path #(zero? (:trade/qty %))] s/NONE)))

=> {"STOCK1" {:trades [#:trade{:id 140, :qty 25}]}}

i would probably start with finding out which part of essential functionality is absent from the core library. In your case it is the function to map over the collection while keeping some changing state.

It could look this way:

(defn map-state [f state data]
  (when-let [[x & xs] (seq data)]
    (lazy-seq
     (let [[new-state new-x] (f state x)]
       (cons new-x (map-state f new-state xs))))))

small example of how it could work in context like yours:

(def running-subtract (partial map-state
                               #(let [qty (min %1 %2)]
                                  [(- %1 qty) (- %2 qty)])))
#'user/running-subtract

user> (running-subtract 10 (range 7))
;;=> (0 0 0 0 0 5 6)

so, you can use it to subtract the state from your trades:

(defn running-decrease-trades [trades amount]
  (map-state (fn [amount trade]
               (let [sub (min (:qty trade) amount)]
                 [(- amount sub) (update trade :qty - sub)]))
             amount
             trades))

and transforming your data with this function would be as easy as the following:

(defn handle-trade [data {:keys [stock qty]}]
  (update-in data [stock :trades] running-decrease-trades qty))


user> (handle-trade
       {"STOCK1" {:trades [{:id 100, :qty 50} {:id 140, :qty 50}]}}
       {:stock "STOCK1" :qty 75})
{"STOCK1" {:trades ({:id 100, :qty 0} {:id 140, :qty 25})}}

Although i like specter very much, i would say it is an overkill for this one.

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

Unlike imperative programming, where you often modify values in place, in functional programming you instead create new values that contain the modifications. So you will have to create a new version of your map (using update-in) that contains a modified vector with your trades. Something like this:

(def conj-positive-trade ((filter (comp pos? :qty)) conj))

(defn sell [trades sale]
  (update-in trades
             [(:stock sale) :trades]
             #(first
               (reduce (fn [[dst remaining] {:keys [qty id]}]
                         (let [diff (- qty remaining)]
                           [(conj-positive-trade dst {:id id :qty diff})
                            (max 0 (- diff))]))
                       [[] (:qty sale)]
                       %))))

Here, conj-positive-trade is a function that only conjoins positive trades to a vector.

Here is how to use the sell function:

(sell {"STOCK1" {:trades [{:id 100 :qty 50} {:id 140 :qty 50} {:id 150 :qty 70}]}}
      {:id 200 :stock "STOCK1", :qty 75})
;; => {"STOCK1" {:trades [{:id 140, :qty 25} {:id 150, :qty 70}]}}

As an alternative solution that wouldn't use specter (which is great, but requires buy-in). I would keep two atoms, one that is a raw listing of all trades (a vector of maps that you just conj to, so for instance {:trade-id 1 :name "AAPL" :price 100 :qty 20}]), and another that is a map of maps indexed by stock name grouped-result. You'd go from one to the other by group-by or filter so if you added a trade in "AAPL" you can update the quantity as such: (swap! grouped-result update-in ["AAPL"] (-> @listing (filter #(= (:name %) "AAPL")) (map :qty) (reduce +)))

When it comes to the trade-id you keep it's a bit more complicated as when you factor in PnL there can be FIFO or LIFO considerations - but again you can use reductions or reduced to stop where you want.

Related