Clojure Notes--labrepl 1

Using labrepl to Learn Clojure

The labrepl project is both an add-on Clojure environment and a set of exercises, all designed to give beginners a chance to learn Clojure. You can find out how to install and access labrepl at http://github.com/relevance/labrepl; the Clojure community's official documentation on how to set up various Clojure programming environments is at http://www.assembla.com/wiki/show/clojure/Getting_Started.

The Clojure community owes Stuart Halloway significant thanks in making labrepl freely available, especially since he developed it as part of the commercial beginning Clojure class that he teaches.

I (Gregg Williams) started this page (and successive labrepl pages) as a way of documenting what I learned about Clojure while solving the labrepl exercises. If you're new at Clojure programming, you will probably find some things here that you would not otherwise discover without some significant trial and error. If you decide to go through the labrepl exercises yourself, I invite you to study the code solutions given by labrepl until you understand them completely. You will learn at least as much from this as you will from the programming you do. Good hacking!

gregg at-sign GettingClojure dot-sign com



8/15/10

problems with "… … …" when printing out long data structures, etc.

*print-length* controls how many items of each collection the
printer will print.

Use (set! *print-length* nil) to print all items, no matter how many.

See also *print-level* (prints how many level deep is printed, for a complex object)
and *print-dup* (controls whether objects are printed out in a form that can be read
back in by a program).


8/16/10

Clojure doc definition of if-let:

clojure.core/if-let
([bindings then] [bindings then else & oldform])
Macro
  bindings => binding-form test

  If test is true, evaluates then with binding-form bound to the value of 
  test, if not, yields else

This is the official definition of if-let. It makes no sense to me!

user=> (defn if-let-demo [arg]
             (if-let [x arg]
               "then"
               "else"))

(defn sum-even-numbers [nums]
        (if-let [nums (seq (filter even? nums))]
          (reduce + nums)
          "No even numbers found."))

#'user/sum-even-numbers
user> (sum-even-numbers [1 3 5 7 9])
"No even numbers found."
user> (sum-even-numbers [1 3 5 7 9 10 12])
22

Use of destructuring

Context: Working on exercises in the "looping" page of labrepl. (Assume that the context of entries below will stay the same until I mention otherwise.)

(defn min-2b
 "returns the smallest of all the numeric arguments"
      [x & more]
      (loop [min x
             [x & more] (seq more)]  ;note destructuring
;;;             [x & more] more]  <--alternate that seems to work as well
        (if x
          (recur (if (< x min) x min) (seq more))
          min)))

Note that the destructuring is occurring on the left side of the binding (!) and that "&" can be used to denote an optional symbol (containing everything after the first element, x) within the destructuring—both are totally unexpected to me!

In the code above, the values of "x" on lines 3 and 4 are the same, and the values of "x" on lines 5, 7, and 8 are the same, but the values of these two x's are definitely not the same, as can be shown in the code below:

(defn min-2gw
  "returns the smallest of all the numeric arguments"
      [x & more]
      (loop [min x
             [next-x & next-more] (seq more)]
            (if next-x
          (recur (if (< next-x min) next-x min) (seq next-more))
          min)))

I think this is better because the introduction of next-x and next-more helps you keep track of what's happening during the recursion.

But why oh why would anyone writing in a language that stresses immutable state write code that forces them to remember (and others to realize) that the symbol x has a meaning that changes based on its position within the same definition?!!


8/18/10

(defn zipm-1
  "returns a map associating each key with its corresponding val, by position"
   [keys vals]
   (loop [m {}, ks keys, vs vals]
     (if (or (= ks '()) (= vs '()))
       m
       (recur (assoc m (first ks) (first vs)) (rest ks) (rest vs)))))

(println "zipm-1 returns " (zipm-1 [:a :b :c] [1 2 3]))   --returns-->
zipm-1 returns  {:c 3, :b 2, :a 1}

In the above code, I originally had (if (or (nil? ks) (nil? vs)). That didn't work: nil is not the empty list!


From the offdocs (official (and sometimes "off") docs):

clojure.core/seq
([coll])
  Returns a seq on the collection. If the collection is
    empty, returns nil.  (seq nil) returns nil. seq also works on
    Strings, native Java arrays (of reference types) and any objects
    that implement Iterable.
-------------------------
clojure.core/next
([coll])
  Returns a seq of the items after the first. Calls seq on its
  argument.  If there are no more items, returns nil.

The docs for next are, in fact, "off." The second sentence could be omitted; if not, it should read "Calls seq on (rest [coll])", which is essentially what the first sentence says.

In any case, one of the values of using (seq foo) in a recursive or loop situation is that if foo is the empty list, (seq foo) returns nil, which can then be used in logical expressions.


8/21/10

(defn zipm-1 ;my first try--it works
  "returns a map associating each key with its corresponding val, by position"
   [keys vals]
   (loop [m {}, ks keys, vs vals]
     (if (or (= ks '()) (= vs '()))
       m
       (recur (assoc m (first ks) (first vs)) (rest ks) (rest vs)))))

(defn zipm-1p ; p stands for "prime"; a refinement of zipm-1
  "returns a map associating each key with its corresponding val, by position"
   [keys vals]
   (loop [m {}, ks keys, vs vals]
     (println "ks=" ks "vs=" vs)
     (if (and ks vs) ;recur only if both are non-nil
       (recur (assoc m (first ks) (first vs)) (next ks) (next vs))
; If you replace above line with line below, zip1-p hangs in an infinite
; loop. If ks='(5), then (rest ks)='(), which is *NOT* = to nil in Clojure
; (next ks) is equivalent to (seq (rest ks)). (seq '()) returns nil, which
; enables (and ks vs) to display the behavior we want, true iff both are non-nil.
;       (recur (assoc m (first ks) (first vs)) (rest ks) (rest vs))
       m)))
  (println "zipm-1p returns " (zipm-1p [:a :b :c] [1 2 3]))
  (println "zipm-1p returns " (zipm-1p [:a :b :c] [1 2 3 4 5]))

when run, returns

run:
ks= [:a :b :c] vs= [1 2 3]
ks= (:b :c) vs= (2 3)
ks= (:c) vs= (3)
ks= nil vs= nil
zipm-1p returns  {:c 3, :b 2, :a 1}
ks= [:a :b :c] vs= [1 2 3 4 5]
ks= (:b :c) vs= (2 3 4 5)
ks= (:c) vs= (3 4 5)
ks= nil vs= (4 5)
zipm-1p returns  {:c 3, :b 2, :a 1}
BUILD SUCCESSFUL (total time: 2 seconds)

Note that zipm-1pcorrectly handles the case where one sequence is longer than the other—it stops after it has consumed the shorter sequence.

Only after I got zipm-1p working correctly did I realize that it was the same as the solution code given by labrepl.

NOTE: The bindings that happen at the top of loop occur only once. At the bottom, when recur is called, those values go straight into the loop's symbols. Once I did (loop [m {}, ks (seq keys), vs (seq vals)]…, expecting the last two arguments to have seq performed on them each iteration. Didn't happen! The (seq …) has to be done within the recur!


Well, who knew it did this?

(defn test-restructuring []
  (let [[first & rest] '()]
    (println "first=" first ", rest=" rest)))
; returns first= nil , rest= nil

If you take out the "&", you get an error because first binds to nil…and there's nothing left for rest to bind to. "&" means that the (single) symbol after it is optional. But if nothing binds to it, it takes the value nil. Works for me.


Where's zipm-3?


8/22/10

(defn zipm-4  ;doesn't work
  "returns a map associating each key with its corresponding val, by position"
  [keys vals]
  (hash-map (interleave keys vals)))

doesn't work; returns error: Exception in thread "main" java.lang.IllegalArgumentException: No value supplied for key: clojure.lang.LazySeq@36126507

This is because the program is doing (hash-map '(:a 1 :b 2)) when what I meant to do is (hash-map :a 1 :b 2). This is why you need to add apply, giving the following solution:

(defn zipm-4  ;my solution
  "returns a map associating each key with its corresponding val, by position"
  [keys vals]
  (apply hash-map (interleave keys vals)))

But there's more to be learned here. I get lazy; I see an error and I immediately go back to the source code. If I had looked at the error message carefully ("No value supplied for key"), I might have intuited that the problem was hash-map was seeing only a key ((hash-map '(:a 1 :b 2))), and not key-value pairs ((hash-map :a 1 :b 2)).

If I'm going to get better at this, I need to use all the information I'm given, and I need to pay more attention to details.


(defn zipm-5  ;my solution
  "returns a map associating each key with its corresponding val, by position"
  [keys vals]
  (into {} (map vector keys vals)))

Some magic is going on here. (map vector keys vals) returns values like ([:a 1] [:b 2] [:c 3] [:d 4]), so it's conceivable to expect (zipm-5 keys vals) to return something like {([:a 1] [:b 2], [:c 3] [:d 4])}. I'm just sayin'.

* I'm using Clojure 1.2, starting here *


(defn only-right-args? [args]
  "Only the keywords :read :written and :deleted will be valid."
  (every? #{:read :written :deleted} args))
#'user/only-right-args?
user=> (only-right-args? :deleted)   ;this is (1)
#<CompilerException java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Keyword (NO_SOURCE_FILE:12)>
user=> (only-right-args? [:deleted])  ;this is (2)
true
user=> (only-right-args? '(:deleted))
true
user=> (only-right-args? #{:deleted})
true
user=>

With an parameter name of args, I would have expected (1) to work, but it didn't. A better name would be arg-seq.

But look what else I found:

user=> (seq? '(1 2 3))
true
user=> (seq? #{:deleted})
false
user=> (seq? #{1 2 3 4})
false
user=>

Why aren't sets considered sequences?!! According to the official Clojure docs, they meet the criteria:
user=> (first #{1 2 3 4})
1
user=> (rest #{1 2 3 4})
(2 3 4)
user=> (cons 5 #{1 2 3 4})
(5 1 2 3 4)
user=>

8/29/10

Context: Working on exercises in the "project-euler" page of labrepl.

My first solution for divides-any is:

(defn divides-any
  "returns true iff num is exactly divided by any of the divisors"
  [num & divisors]
  (let [div-list (map #(divides? num %) divisors)]
    (boolean (some true? div-list))))

labrepl gives this definition:

(defn divides-any
  "Return a predicate that tests whether its arg can be
   evenly divided by any of nums."
  [& nums]
  (fn [arg]
    (boolean (some #(divides? arg %) nums))))

On to the recursive solution:

(defn euler1 ;recursive--my solution
  "sum all numbers from 1 to (upper - 1) that are divisible by 3 or 5"
  [upper]
  (let [divisible? #(divides-any % 3 5)]
    (loop [sum 0, n 1]
      (if (>= n upper)
    sum
    (recur (if (divisible? n) (+ n sum) sum)
           (inc n))))))

(defn problem-1-recur ;labrepl solution, equiv to euler1
  "Sum the numbers divisible by 3 or 5, from 0 to upper."
  ([]
     (problem-1-recur 1000))
  ([upper]
     (let [divisible? (divides-any 3 5)]
       (loop [sum 0 n 1]
     (if (>= n upper)
       sum
       (recur
        (if (divisible? n) (+ sum n) sum)
        (inc n)))))))

I didn't read the instructions for divides-any carefully enough; they ask for "a divides-any function that takes a variable list of numbers, and returns a predicate that tests whether its arg can be evenly divided by any of the numbers."

You can see how this definition makes it easy for problem-1-recur to capture this function (line 6) and, later, use it (line 11). In my solution (function euler1, line 4), I essentially recreate labrepl's divides-any inline; I can't use my version because it assumes the first argument is the number to be tested.

(defn euler2 ;reduction--my solution, using my divides-any
  "sum all numbers from 1 to (upper - 1) that are divisible by 3 or 5"
  [upper]
  (apply + (filter #(divides-any % 3 5) (range 1 upper))))

(defn euler2 ;reduction--labrepl's solution, using labrepl's divides-any
  "sum all numbers from 1 to (upper - 1) that are divisible by 3 or 5"
  [upper]
  (apply + (filter (divides-any 3 5) (range 1 upper))))

Lesson learned: there is value in creating a "pure" function that can be reused in multiple situations.



8/30/10

Context: Working on exercises in the "unified-update-model" page of labrepl.

Working with atoms now; made a basic mistake with swap! (used to change the value of an atom). (swap! atom-name new-value) is incorrect; the correct usage is (swap! atom-name update-function optional-argument-list), where update-function is applied to the value inside the atom to update it to its new value. The function must take this value as its first argument. If this function requires N arguments (N >= 1), optional-argument-list is equal to the last N-1 arguments—that is, everything except the value that is being updated.

Here's some code that shows this:

(defn create1
  "returns an atom wrapped around an empty map"
  []
  (atom {}))

(defn put1
  ""
  [cache key value]
  (swap! cache assoc key value))

Here is an example of it in use:

user> (def a (create1))
#'user/a
user> a
#<Atom@1e41830: {}>
user> (put1 a :foo 'bar)
{:foo bar}
user> a
#<Atom@1e41830: {:foo bar}>
user>

In the example above, cache is an atom, @cache is the map inside it.(assoc @cache key value) returns a new map equal to the old map, but with the key/value pair added. To update cache to include this key/value pair, you would execute (swap! cache assoc key value), which is what is done in the last line of function put1.

Allowing the creation of an atom-map to have either a default or a specified value:

(defn create2 ;my version
  "returns atom containing empty map, or start-map if specified"
  [& start-map]
  (if (boolean start-map) ;converts map to true/false
    (atom start-map)
    (atom {})))

(defn create  ;repl's version
  ([] (create {}))
  ([initial-value] (atom initial-value)))

Okay, labrepl's version is more elegant, but both work.


An enhanced version of put1 (labrepl's version is essentially the same as this):

(defn put2
  "adds to atom-map either a key/value or another map"
  ([atom-map key value] (swap! atom-map assoc key value))
  ([atom-map map2] (swap! atom-map merge map2)))

user> @a
{:foo bar}
user> (put2 a :bar 'foo)
{:bar foo, :foo bar}
user> (put2 a {:boof 'bool})
{:boof bool, :bar foo, :foo bar}
user>

Starting to work with refs (labrepl's version is essentially the same as this):

(defn create-with-ref
  "returns a ref containing empty map, or start-map if specified"
  ([start-map] (ref start-map))
  ([] (ref {})))

user> (create-with-ref)
#<Ref@562791: {}>
user> (create-with-ref {:foo 'bar})
#<Ref@1d97efc: {:foo bar}>
user>

100831

I thought I was doing something useful by naming a parameter descriptively, so users would know what type of data to put into that slot. But what happens if you decide to change the data structure? Variable names should be descriptive without revealing implementation details. For example, I stopped using the name atom-map and replaced it with mutable-map. This variable will always be a map, and mutable describes its one of its characteristics.

Continuing functions made with ref:

(defn put-with-ref
  "adds to mutable-map either a key/value or another map"
  ([mutable-map key value] (dosync
                (alter mutable-map assoc key value)))
  ([mutable-map map2] (dosync
               (alter mutable-map merge map2))))

user> a
#<Ref@8b7250: {:foo bar}>
user> (put-with-ref a :barky 'dog)
{:barky dog, :foo bar}
user> (put-with-ref a {:sneaky 'cat})
{:sneaky cat, :barky dog, :foo bar}
user>


9/3/10

Context: Working on exercises in the "zero-sum" page of labrepl.

I had to skip the rest of the "unified-update-model" page because I couldn't get the necessary libraries integrated into my setup—very frustrating!

Now, at the beginning of the "zero-sum" page, I'm beginning to enter cryptoClojureLand, where nothing makes sense: not the descriptions, not the answers, nothing. This, in my opinion, is shows how the existing Clojure community is failing beginners. Here is the first question and its solution:

Listing 1

# Create a function named make-accounts that takes a map with :count and :initial-balance.
# It should create a map whose keys are names (using numbers from 0 to count-1) and whose
# values are refs to the initial balance.

1    (defn make-accounts
2      "Create a map of account-num->:initial-balance for :count numbered
3      accounts."
4      [{:keys [count initial-balance]}]
5      (zipmap (range count) (repeatedly (partial ref initial-balance))))

Once I execute this code, I don't even know how to use it. I try with zero and two arguments—error messages. Finally I get something that works:

Listing 2

user> (make-accounts {:count 3 :initial-balance 100})
{2 #<Ref@fb2ac4: 100>, 1 #<Ref@1b4d679: 100>, 0 #<Ref@16ca3d5: 100>}

Here's some help…or is it?

Listing 3

-------------------------
clojure.core/repeatedly
([f] [n f])
  Takes a function of no args, presumably with side effects, and
  returns an infinite (or length n if supplied) lazy sequence of calls
  to it
nil
user> (doc ref)
-------------------------
clojure.core/ref
([x] [x & options])
  Creates and returns a Ref with an initial value of x and zero or
  more options (in any order):

  :meta metadata-map

  :validator validate-fn

  :min-history (default 0)
  :max-history (default 10)

  If metadata-map is supplied, it will be come the metadata on the
  ref. validate-fn must be nil or a side-effect-free fn of one
  argument, which will be passed the intended new state on any state
  change. If the new state is unacceptable, the validate-fn should
  return false or throw an exception. validate-fn will be called on
  transaction commit, when all refs have their final values.

  Normally refs accumulate history dynamically as needed to deal with
  read demands. If you know in advance you will need history you can
  set :min-history to ensure it will be available when first needed (instead
  of after a read fault). History is limited, and the limit can be set
  with :max-history.
nil
user> (doc partial)
-------------------------
clojure.core/partial
([f arg1] [f arg1 arg2] [f arg1 arg2 arg3] [f arg1 arg2 arg3 & more])
  Takes a function f and fewer than the normal arguments to f, and
  returns a fn that takes a variable number of additional args. When
  called, the returned function calls f with args + additional args.
nil
user> (doc zipmap)
-------------------------
clojure.core/zipmap
([keys vals])
  Returns a map with the keys mapped to the corresponding vals.
nil
user>

I did some experiments to help me understand what was going on:

Listing 4

user> (ref 100)
#<Ref@11641cf: 100>
user> (partial ref 100)
#<core$partial$fn__3678 clojure.core$partial$fn__3678@38c8c5>
user> (take 3 (repeatedly (partial ref 100)))
(#<Ref@1bde392: 100> #<Ref@1a7905e: 100> #<Ref@1f71773: 100>)
user>

Now I can begin to make sense of this function. For your convenience, here it is again:

Listing 5

1    (defn make-accounts
2      "Create a map of account-num->:initial-balance for :count numbered
3      accounts."
4      [{:keys [count initial-balance]}]
5      (zipmap (range count) (repeatedly (partial ref initial-balance))))

I can't find any "official" documentation to explain this function, but here's what I've deduced (feel free to send me feedback on this):

Line 4 says the following: The [{…}] says that this function takes one argument, a map. The {:keys [count initial-balance]} describes a map that has two keys named :count and :initial-balance. Together, somehow, they convey the information that the argument to this function should look like this: {:count <number> :initial-balance <number>}. And that's why (make-accounts {:count 3 :initial-balance 100}) works (see Listing 2, above).

Now the output of this form is {2 #<Ref@fb2ac4: 100>, 1 #<Ref@1b4d679: 100>, 0 #<Ref@16ca3d5: 100>}. This is a map with three keys, 1, 2, and 3, each of which is mapped to a different ref containing the value 100. If you refer to the online documentation for zipmap (see the last definition in block 3, above) and you remember that (range 3) evaluates to a lazy sequence returning the value (0 1 2), you can see that line 5 of Listing 5 takes up, maps it to something, and returns a result of the form {2 <ref2 with value 100>, 1 <ref1 with value 100>, 0 <ref0 with value 100>}.

This something must be a sequence that looks like (<ref0 with value 100> <ref1 with value 100> <ref2 with value 100>). From this, I assume that (repeatedly (partial ref initial-balance)) creates a lazy sequence containing an infinite number of almost-formed (ref 100)s. The use of partial is odd, because (partial ref initial-balance) implies that ref must take more than one argument; it can, but (ref initial-balance) is perfectly legal and will return a ref with value initial-balance. In addition, (partial ref initial-balance) returns a function that will, in fact, return the desired ref once the additional required arguments are supplied to it. (This probably doesn't make sense—study the Clojure partial function, which you can do by visiting the page for partial at http://clojuredocs.org/v/1951.)

Finally, zipmap interleaves the two sequences, (0 1 2) and the lazy infinite sequence (<ref0 with value 100> <ref1 with value 100> <ref2 with value 100> … ) and turns it into a map. (It might help to know that when sequences are being associated by some Clojure function on an element-by-element basis, the process stops when the shortest sequence runs out of elements; this is what's happening with zipmap.) In any case, the final result looks like {2 <ref2 with value 100>, 1 <ref1 with value 100>, 0 <ref0 with value 100>}, ending the explanation of how make-accounts works.



9/6/10

Context: Working on "Is It Theadsafe?"exercises in the "zero-sum" page of labrepl.

 (defn make-accounts ;labrepl's answer
   "Create a map of account-num->:initial-balance for :count numbered accounts."
    [{:keys [count initial-balance]}]
    (zipmap (range count) (repeatedly (partial ref initial-balance))))

(defn total-balance
  "returns total in all accounts within map named accounts"
  [accounts]
  (apply + (map deref (vals accounts))))

(defn transfer
  "In map named accounts, transfers amount between accounts indexed by from and to"
  [{:keys [accounts from to amount]}]
  (dosync
   (alter (accounts from) - amount)
   (alter (accounts to) + amount)))

(defn balance
  "returns the balance of the specified ID in the specified map of accounts"
  [accounts account-id]
  @(accounts account-id))

(defn random-account-ids
  "Return a lazy seq of random account ids from accounts"
  [accounts]
   (let [ids (keys accounts)]
     (repeatedly (fn [] (rand-nth ids)))))

(defn random-transfer
  "Perform a random tranfer between two accounts in accounts.
Both accounts might be same account, we don't care."
  [accounts]
  (let [[from-id to-id] (random-account-ids accounts)
    amount (* 10 (rand-int 15))]
    (transfer {:accounts accounts :from from-id :to to-id :amount amount})))

;;;;; implementation 1, using a map of refs

(defn bunch-o-txes [accounts n iterations] ;my version--does not work
  (take n
       (swank.core/break)
    (future
     (dotimes [_ (quot iterations n)]
       (random-transfer accounts)))))

(defn bunch-o-txes ;labrepl's answer
  "Return n futures doing iterations random transactions
   against accounts, spread equally across n threads."
  [accounts n iterations]
  (take n (repeatedly
           (fn []
             (future
              (dotimes [_ (/ iterations n)]
                (random-transfer accounts)))))))

(def db (make-accounts {:count 3 :initial-balance 100}))

(defn trial ;my version (= labrepl's version)
  "Creates :no-of-accounts accounts with :initial-balance.
   Bang on them from :threads different threads for
   :iterations. All the while, calling thread reads
   total-balance, asserting that it stays correct."
  ([] (trial {:accounts (make-accounts {:count 10 :initial-balance 100})
          :iterations 1000
          :threads 2}))
  ([{:keys [accounts iterations threads]}]
  (let [expected-balance (total-balance accounts)
    futures (bunch-o-txes accounts threads iterations)]
    (loop []
    (if (every?  #(.isDone %) futures)
      {:accounts accounts :futures futures}
      (do
        (assert (= expected-balance
               (total-balance accounts)))
        (recur)))))))

(defn total-balance ;fix that enables trial, above, to work across threads
  "Total balance of all accounts"
  [accounts]
  (dosync (apply + (map deref (vals accounts)))))

9/7/10

More code, this time using a representation of accounts in which it is a reference to a map of account-num->:initial-balance pairs:

;;;;; implementation 2, using a ref of a map

(defn make-accounts ;implementation 2
   "Create a ref to a map of account-num->:initial-balance pairs for :count numbered accounts."
    [{:keys [count initial-balance]}]
    (ref (zipmap (range count) (repeat initial-balance))))

(defn total-balance ;implementation 2
  "returns total in all accounts within map named accounts"
  [accounts]
  (apply + (vals @accounts)))

(defn transfer ;GW implementation 2, 2nd try; works with (trial)
  "In map named accounts, transfers amount between accounts indexed by from and to"
  [{:keys [accounts from to amount]}]
  (let [move-funds (fn [amap from to amount]
             (update-in
              (update-in amap [from] - amount)
              [to]
              + amount))]
    (dosync
     ;alter changes the value @accounts, which is a map
     (alter accounts move-funds from to amount))))

(defn transfer ;labrepl's answer; works with (trial)
  [{:keys [accounts from to amount]}]
  (dosync
   (alter
    accounts
    (fn [accounts]
      (-> accounts
          (update-in [from] #(- % amount))
          (update-in [to] #(+ % amount)))))))

(defn balance ;GW implementation 2
  "returns the balance of the specified ID in the specified map of accounts"
  [accounts account-id]
  (@accounts account-id))

(defn random-account-ids ;GW implementation 2
  "Return a lazy seq of random account ids from accounts"
  [accounts]
   (let [ids (keys @accounts)]
     (repeatedly (fn [] (rand-nth ids)))))

Functions random-transfer, bunch-of-txes, and trial remain unchanged.

I think my version of transfer is more readable than labrepl's (see both, above) because of its one comment and its use of a named function to help clarify matters.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License