Fork me on GitHub
#clojure
<
2021-06-28
>
FiVo16:06:30

What is sort of the "standard" way to deal with concurrent data structures with mutable state in clojure? Examples work wonders. So lets take the standard bank account example:

(defprotocol AccountAPI
  (withdraw [this amt] "withdraw the given amount from the account")
  (deposit [this amt] "deposit the given amount into the account"))

(defrecord Account [name balance]
  AccountAPI
  (withdraw [this amt] (conj this {:balance (- (:balance this) amt)}))
  (deposit [this amt] (conj this {:balance (+ (:balance this) amt)})))

(def account (atom (->Account "foo" 100)))

(require '[clojure.core.async :as async])

(dotimes [_ 5]
  (async/thread
    (swap! account withdraw 2)
    (swap! account deposit 1)))

@account ;; => {:name "foo", :balance 95}
This works nicely as withdraw and deposit are side effect free. How would this "normally" be done if withdraw and deposit are not side effect free? Let's say for example they do I/O. Can I still avoid explicit locking somehow?

borkdude16:06:05

In case of side effects, use locking I would say

FiVo16:06:49

I used a combination of deftype,`locking` and set! for my use case. The result just doesn't look and feel very clojure like, but more like some awkward translated java hence the question.

borkdude16:06:46

sometimes a man's gotta do what a man's gotta do

👍 5
emccue17:06:09

you can use refs too, if you have multiple things of state that need to be in sync

emccue17:06:21

like two accounts

emccue17:06:57

but in that case you still need to separate state changes from the io to perform

emccue17:06:12

maybe returning what to perform as a value

walterl17:06:09

Isn't this what dosync is for (too)?

✔️ 3
borkdude17:06:07

@clojurians-slack100 From the docstring of dosync: > The exprs may be run more than > once, but any effects on Refs will be atomic.

3
walterl17:06:53

Good to point out 👍

walterl17:06:41

That's probably why we keep it free from side effects, like in this example, right? https://clojuredocs.org/clojure.core/dosync#example-542692c7c026201cdc32698d (which is really my primary link to that page 😛)

walterl17:06:54

Or is dosync actually just a bad solution to the problem?

borkdude18:06:39

side effects can happen multiple times in the body of dosync, that's the message

borkdude18:06:05

so don't use side effects in there, unless the side effects may happen multiple times

👍 3
lilactown18:06:50

i read a paper once where they implemented a few different ways to coordinate side effects in a transaction within Clojure’s STM

lilactown18:06:04

the one that felt most natural to me was the “defer” method that basically only ran the side effect once it was sure the transaction would commit

lilactown18:06:59

i think you can actually do something basically like that using agents

phronmophobic18:06:42

Agents already do that: > Agents are integrated with the STM - any dispatches made in a transaction are held until it commits, and are discarded if it is retried or aborted. https://clojure.org/reference/agents

jjttjj20:06:30

I have a sequence of maps representing tabular data rows, and i want to assoc in each row the running sum of the value of another "column":

(def rows
  [{:price 10 :qty 6}
   {:price 11 :qty 7}
   {:price 15 :qty 20}
   {:price 14 :qty 7}
   {:price 13 :qty 8}
   {:price 11 :qty 2}])

(defn with-reductions-col [old-key new-key rf rows]
  (let [agg (drop 1 (reductions rf (rf) (map old-key rows)))]
    (map (fn [row v] (assoc row new-key v)) rows agg)))


(with-reductions-col :qty :total + rows)
;;=>

({:price 10, :qty 6, :total 6}
 {:price 11, :qty 7, :total 13}
 {:price 15, :qty 20, :total 33}
 {:price 14, :qty 7, :total 40}
 {:price 13, :qty 8, :total 48}
 {:price 11, :qty 2, :total 50})
Are there any libraries that help do this kind of thing efficiently, so I could append multiple rows in a single pass?

jjttjj20:06:29

Actually I just realized my old favorite https://github.com/cgrand/xforms probably does what i need with transjuxt + reductions

Apple21:06:02

Perhaps incanter can do it. and according to https://slabruyere.net/assets/pdf/2019-04-24-clojure-for-data-science.pdf other statistics lib may be able to do it.

rwstauner21:06:53

i'm really just curious to learn more... is there documentation that would explain why i consistently get sets seq'd in the same order? for example:

user=> (seq #{1 2 3})
(1 3 2)
user=> (seq #{"1" "2" "3"})
("3" "1" "2")
i'm surprised to get that same (non-sequential) order on 2 different machines, even clj and cljs (which i was even more surprised to find)... i'm bewildered that the order appears reliable (even though i would never want to rely on it). if anyone happens to have insight i'd love to know.

p-himik21:06:48

I haven't checked it but I imagine it's due to how the hash function is implemented for those particular types (`long` and str).

rwstauner22:06:21

i suppose it must be, i just never expected "unordered but consistent"

rwstauner22:06:43

user=> #{1 2 3}
#{1 3 2}
user=> #{1 2 3 4}
#{1 4 3 2}
user=> #{1 2 3 4 5}
#{1 4 3 2 5}
user=> #{1 2 3 4 5 6}
#{1 4 6 3 2 5}
user=> #{1 2 3 4 5 6 7}
#{7 1 4 6 3 2 5}
user=> #{1 2 3 4 5 6}
#{1 4 6 3 2 5}
user=> #{1 2 3 4 5}
#{1 4 3 2 5}
user=> #{1 2 3 4}
#{1 4 3 2}
user=> #{1 2 3}
#{1 3 2}

emccue22:06:28

@U01D8NEJ0EP try making sets of this

emccue22:06:04

(defn o [] (let [r (Math/random)] (reify Object (hashCode [_] r)))

Alex Miller (Clojure team)22:06:15

it is based on hash function (which is not guaranteed between versions/jvms/etc), but is stable if those variables are the same

rwstauner22:06:52

thanks, that's what i was hoping to hear

emccue22:06:54

i think java's hashset started purposefully shuffling the items in order to break people who relied on the order

rwstauner22:06:04

yeah, i have a perl background so i'm used to that

Alex Miller (Clojure team)22:06:41

I think there may be some cases where insertion/removal order around the internal tree order may result in different orders, but you'd need a bigger set and a series of "modifications"

Alex Miller (Clojure team)22:06:41

I think there may be some cases where insertion/removal order around the internal tree order may result in different orders, but you'd need a bigger set and a series of "modifications"