Welcome! Please see the About page for a little more info on how this works.

0 votes
in Refs, agents, atoms by

I know some little Lisp, but Clojure feels a bit different:

(defn mojorate []
  (let [v (vec (range 1 11))
        max (- (count v) 1)
        atomix (atom [])
        chanl (async/chan)]
    (println v)
    (loop [i 0]
      (async/go
        (>! chanl (swap! atomix conj (/ (get v i) 2))))
      (if (= i max)
        (do (<!! (async/go (<! chanl)))
          (async/close! chanl)
          @atomix)
        (recur (inc i))))))

It's just an exercise with channel, atom loop/recur etc.

Can you do it like this?

by
Not sure what you're actually asking - can you elaborate?
by
It works as expected but I’m unsure if it is 1.: idiomatic 2.: best practice. 3.: or can be done better.
by
But what does "expected" mean? What is the function *supposed* to do? When running it, I see different results being produced on different runs so I can't quite deduce what you actually need.
by
I always get:
testomato.core> (mojorate)
[1 2 3 4 5 6 7 8 9 10]                                                                      
[1/2 1 3/2 2 5/2 3 7/2 9/2 5]

The snippet sends 10 numbers to a channel and gets them back - divided by two.

1 Answer

+1 vote
by

You have way too many go blocks, not only in terms of their amount in the code but also in terms of how many are executed. That first go block in the loop doesn't block - it lets the execution continue immediately. There's no guarantee that by the time that same block is executed again the previous block has had enough time to put the value into the channel. It's a race condition.

To confirm, simply run this code: (set (repeatedly 10000 mojorate)). Increase the number if all you get is a set of one element.

The snippet sends 10 numbers to a channel and gets them back - divided by two.

The first half of the sentence is an implementation detail that's beside the point from the perspective of the user of the function. If the only thing you need to do is to get a vector of 10 numbers starting from 1 divided by 2, then it's (mapv #(/ % 2) (range 1 11)) of course.

If you need something else, then please describe in detail so we're not stuck in the XY problem. There are multiple ways in which the original function can be rewritten - none of which could potentially be correct if your real problem demands something specific.

by
I tried to familiarize myself with the concept. And  I realize that mapv is the tighter solution. Simple arithmetic is not uncommon to demonstrate something like this. An example in Common Lisp:

(let ((channel (make-channel)))
  (submit-task channel '+ 3 4)
  (receive-result channel))

But i can confirm what you predicted: With 100.000 repetitions i get:

#{[1/2 1 3/2 2 5/2 3 7/2 4 9/2 5]
  [1 3/2 2 5/2 3 7/2 4 9/2 5 1/2]
  [1/2 1 3/2 2 5/2 3 7/2 4 9/2]
  [1/2 1 2 5/2 3 7/2 4 9/2 5 3/2]}
by
AFAIK channels in Common Lisp are not the same as channels in `clojure.core.async`. They are more like a way to communicate with a particular thread. In Clojure, if all you want to do is to run a series of computations in a separate thread, you're better off with just using a `future` or `pmap` (although it's commonly recommended to avoid `pmap` and instead use something that can be controlled). An async channel in Clojure would be useful if you want to feed the values in one location, maybe add some computation on top of them in the second location, and receive the results in a third location, where those locations might not know about each other at all. E.g. a producer might use `(a/to-chan! (range 1 11))`, a transformer might use `(a/map #(/ % 2) [channel])`, and a consumer might use `(a/go (let [all-values (a/<! (a/into [] transformed-channel))] ...))`.
by
And of course, channels in Clojure are much more than that. They support buffering with different strategies, allow for backpressure, let you statically or dynamically combine and separate channels,...
by
reshown by
Thanks. I begin to see it clearly now. And i really didn‘t see the race condition.
...