用不断变化的状态转换序列的功能/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-by
或 filter
从一个转到另一个,因此如果您添加了“AAPL”交易,您可以这样更新数量:
(swap! grouped-result update-in ["AAPL"] (-> @listing (filter #(= (:name %) "AAPL")) (map :qty) (reduce +)))
当涉及到贸易 ID 时,您保留它有点复杂,因为当您考虑 PnL 时,可能需要考虑 FIFO 或 LIFO - 但同样您可以使用 reductions
或 reduced
停在你想停的地方。
我可能会先找出核心库中缺少哪些基本功能。在你的例子中,它是在保持一些变化状态的同时映射集合的函数。
它可能看起来像这样:
(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})
问题背景与股票交易有关。当进行销售时,我正在尝试更新特定股票的持有量。简化摘录
;; @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-by
或 filter
从一个转到另一个,因此如果您添加了“AAPL”交易,您可以这样更新数量:
(swap! grouped-result update-in ["AAPL"] (-> @listing (filter #(= (:name %) "AAPL")) (map :qty) (reduce +)))
当涉及到贸易 ID 时,您保留它有点复杂,因为当您考虑 PnL 时,可能需要考虑 FIFO 或 LIFO - 但同样您可以使用 reductions
或 reduced
停在你想停的地方。
我可能会先找出核心库中缺少哪些基本功能。在你的例子中,它是在保持一些变化状态的同时映射集合的函数。
它可能看起来像这样:
(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})