fuji
fuji

Reputation: 1203

Updating certain values of maps contained in a list

I have a list of maps which looks something like this:

(def balances ({:name "Steve" :money 1000} {:name "Bill" :money 1000} ...))

I'm trying to write a function that transfers a given amount (let's say 100) of Steves money to Bill and update the data structure:

({:name "Steve" :money 900} {:name "Bill" :money 1100})

I figured that the function should expect the balance as parameter each time and look something like this:

(defn transfer [current-balances amount sender receiver] ... )

How would a function like this look and is this a clever way of managing and updating the account balance? In general my program will take quite a long list of transfers and iteratively apply them to the balance structure. Do I always have to pass the balance structure to the transfer function because of the persistant data structures in Clojure?

Upvotes: 2

Views: 131

Answers (3)

mobyte
mobyte

Reputation: 3752

Here's my two cents. I hope it'll be helpful too.

(def balances {1 (ref {:name "Steve"
                       :money 1000})
               2 (ref {:name "Bill"
                       :money 1000})
               3 (ref {:name "John"
                       :money 1000})})

(defn balance [person-id]
  ((deref (balances person-id)) :money))

(defn credit [balance amount]
  (- balance amount))

(defn debet [balance amount]
  (+ balance amount))

(defn update-account [person-id operation-fn amount]
  (alter (get balances person-id)
         #(assoc % :money
                 (operation-fn (:money %) amount))))

(defn credit-account [person-id amount]
  (update-account person-id credit amount))

(defn debet-account [person-id amount]
  (update-account person-id debet amount))

(defn transfer [sender-id receiver-id amount]
  (if (< (credit (balance sender-id) amount) 0)
    {:result :insufficient-fund}
    (do (credit-account sender-id amount)
        (debet-account receiver-id amount)
        {:result :ok})))

test

(defn transaction []
  (dosync (transfer 1 2 100)
          (transfer 2 3 200)
          (transfer 3 1 200)))

(transaction)
-> {:result :ok}

balances
-> {1 #<Ref@b54dba: {:money 1100, :name "Steve"}>,
    2 #<Ref@1020230: {:money 900, :name "Bill"}>,
    3 #<Ref@1803641: {:money 1000, :name "John"}>}

Upvotes: 0

Arthur Ulfeldt
Arthur Ulfeldt

Reputation: 91534

The fact that your balances are all contained under a single umbrella data structure, and that structure is a proper value, your transfer function can simply take in the structure describing the current state of the accounts, another describing the change, and produce the new state of accounts. This lets you treat the transfer actions as proper values as well which should help deal with very long lists of transfers :) My only change would be to use a map of balances instead of a list.

bar> (def balances {"Steve" {:money 1000} "Bill" {:money 1000}})
#'bar/balances

bar> (def transfers [["Steve" "Bill" 100] ["Bill" "Steve" 100] 
                     ["Steve" "Bill" 10 ] ["Bill" "Steve" 10 ] 
                     ["Bill" "Steve" 10 ]])
#'bar/transfers

Then define a simple transfer function that takes one of these and appliese it to the accounts

(defn transfer [balances [from to ammount]] 
  (-> balances 
      (update-in [from :money] - ammount) 
      (update-in [to   :money] + ammount)))

this function can be used to directly reduce any sequence of transfers to the state of all accounts:

bar> (reduce transfer balances transfers)
{"Bill" {:money 990}, "Steve" {:money 1010}}

A function that accepts a new transfers from a customer can then use this function to alter the state of whatever you choose to store your bank in (DB, atom, agent, etc.)

bar> (def bank (agent {:current balances :ledger []}))
#'bar/bank

bar> (defn accept-transfers [transfers] 
       (send bank assoc :current (reduce transfer (:current @bank) transfers) 
                        :ledger (concat transfers (:ledger @bank))))
#'bar/accept-transfers

bar> (accept-transfers transfers)
#<Agent@2eb9bc1: {:current {"Bill" {:money 1000}, "Steve" {:money 1000}}, :ledger []}>

which puts the transfer on the banks queue (and returns that agent which the REPL quickly prints while the transfer may be running) after a moment when we look at the bank wee see all these transfers have been applied.

bar> bank
#<Agent@2eb9bc1: {:current {"Bill" {:money 990}, "Steve" {:money 1010}}, 
                  :ledger (["Steve" "Bill" 100] ["Bill" "Steve" 100] 
                           ["Steve" "Bill" 10] ["Bill" "Steve" 10] 
                           ["Bill" "Steve" 10])}>

Upvotes: 3

iced
iced

Reputation: 1572

Clojure data is immutable, you can't modify it. You need to use STM.

Here is sample code (simplified to the point of question).

(def account1 {:name "Foo" :money (ref 1000)})
(def account2 {:name "Bar" :money (ref 1000)})

(defn transfer
  [from to amount]
  (dosync
     (alter (:money from) - amount)
     (alter (:money to) + amount)))

(transfer account1 account2 100)

(println @(:money account1))
(println @(:money account2))

Read more at http://clojure.org/refs, http://clojure.org/atoms and, perhaps, http://clojure.org/agents

Upvotes: 1

Related Questions