用不断变化的状态转换序列的功能/clojure 方式是什么?

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

问题背景与股票交易有关。当进行销售时,我正在尝试更新特定股票的持有量。简化摘录

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

现在考虑到 Trade{:id 200 :stock "STOCK1", :qty 75} 的销售交易,我预计持有量会反映

{ "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}]} }

功能性答案让我望而却步。我只能看到一个 doseq 循环,其中包含用于保持状态的原子(例如 sale-qty,可以通过 1 次或 n 次交易来满足)- 但感觉就像 C在 Clojure 中。

是否有更符合 clojure 的解决方案?地图看起来不太合适,因为每个记录处理都需要更新外部状态(待定销售数量 75 -> 25 -> 0)

免责声明:Clojure新手,想学的。

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

与命令式编程不同,在命令式编程中,您经常 就地 修改值,而在函数式编程中,您需要创建包含修改的新值。因此,您必须创建一个新版本的地图(使用 update-in),其中包含您的交易的修改矢量。像这样:

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

此处,conj-positive-trade 是一个仅将正交易连接到向量的函数。

sell函数的使用方法如下:

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

作为不使用幽灵的替代解决方案(很棒,但需要支持)。我会保留两个原子,一个是所有交易的原始列表(一个地图向量,你只是 conj 到,例如 {:trade-id 1 :name "AAPL" :price 100 :qty 20}]),另一个是地图索引的地图股票名称 grouped-result。您将通过 group-byfilter 从一个转到另一个,因此如果您添加了“AAPL”交易,您可以这样更新数量: (swap! grouped-result update-in ["AAPL"] (-> @listing (filter #(= (:name %) "AAPL")) (map :qty) (reduce +)))

当涉及到贸易 ID 时,您保留它有点复杂,因为当您考虑 PnL 时,可能需要考虑 FIFO 或 LIFO - 但同样您可以使用 reductionsreduced停在你想停的地方。

我可能会先找出核心库中缺少哪些基本功能。在你的例子中,它是在保持一些变化状态的同时映射集合的函数。

它可能看起来像这样:

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

它如何在像您这样的上下文中工作的小例子:

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

因此,您可以使用它从您的交易中减去状态:

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

使用此函数转换您的数据会像下面这样简单:

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

虽然我很喜欢幽灵,但我想说这对这个来说有点大材小用了。

每当你想在 Clojure 中遍历 sequence/collection,同时传递一些额外的状态时,想想 reduce Reduce 就像一把瑞士军刀,例如 map和 filter 都可以用 reduce 来实现。但是如何在一个缩减函数中存储多个状态呢?您只需使用地图作为累加器。

让我稍微提炼一下你的问题。让我们创建一个只处理一个问题的函数。

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

现在我们可以使用这个功能来卖股票了。请注意,地图可以接受多个 collections/sequences 作为参数。

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