Super ant for ClojureCLR

Multithreading in Clojure, brought to a new level of development, since there realized transactions changes memory STM (software transactional memory The system). As a demonstration of rich Hickey (Clojure author of the divine) and David Miller (the man who wrote the Clojure implementation .Net) offer a program of "ants", which simulates an ant hill. Each ant lives there in a separate thread. Ants running around the cells of the common fields, collect food, carry it to the nest and don't conflict with each other.

The result of his exercises with this program and I want to bring to the public. I hope the article will be useful for those who are starting to learn Clojure on the platform .Net.

First, the link:

github.com/kemerovo-man/super-ants-clojure-clr
youtu.be/xZ9AGQ3L-EI
sourceforge.net/projects/clojureclr/files
github.com/clojure/clojure-clr/blob/master/Clojure/Clojure.Source/clojure/samples/ants.clj

What I have done.
1. Graphics. In the original program, the ant is only a dash. I have insects depicted more natural.
2. the New heroes. Except for ants, I have implemented aphids and ladybugs.
3. Interacting characters. The grass grows, the aphid eats grass produces sugar and lays eggs (replicates). Ladybugs eat aphids, and eggs. Ants avoid obstacles on the way. (Even the ants must carry the aphids on the grass and interfere with the ladybirds devour aphids, but it is not yet implemented.)
3. Behavior. The behavior of ants has become more complicated, they become smarter than the original. Added the new behavior of the characters.
4. Graphics. Visible dynamics of processes on graphs. The growth of grass, the aphid population, the amount of sugar in the hive and in the environment.
5. Primitive magnifier you can look at details. Right mouse button.
6. Mouse. When you click on the cell of the field to the console displays information contained in the cell. With the mouse wheel you can change the speed of movement of insects.

Changing the initialization parameters we can obtain different scenarios of the development of this ecosystem. The faster the grass grows, the faster it multiplies aphids and the more there is sugar on the field. Grass does not grow on sugars, so the rapid activity of the aphid effect on the growth of grass. Different ratios of insects when you run (how many aphids, how many ladybugs how many ants) define a new dynamic.

Every insect sees only four cells: that which is, and three cells in front of him, and focused on them.
Ants react to pheromone trail. It is important for them not to lose an anthill, or they can endlessly run with food "teeth", not finding where to put. I added them a few features associated with pheromones, so they can effectively navigate, where is food, where is the anthill.

Closer to the code. But let's start from afar.
First, you need to get acquainted with such concepts in Clojure, as atom, agent and ref. They can call the managers of variables. The code does not directly refers to the value of the variable, but through an intermediary. Atom, agent and ref are the three types of intermediary.

the
(def atom1 (atom 0))
(def agent1 (agent 0))
(def ref1 (ref 0))

Here we have identified atom1, agent1 and ref1. Initial value all set to 0.

the
(swap! atom1 inc)
(prn @atom1)
;->1

(send agent1 inc)
(prn @agent1)
;->1

(dosync
(alter ref1 inc))
(prn @ref1)
;->1

Here we pass in atom1, agent1 and ref1 function inc which increments the value by 1 and see that values are everywhere equal to 1.

the
(reset! atom1 0)
(send agent1 (fn [_] 0))
(dosync 
(ref-set ref1 0))

Here we change the current values atom1, agent1 and ref1 to 0.

While the difference between the brokers is not visible, it is only a syntax.

Define the three functions:
the
(defn atom1-change []
(prn "atom1-change")
(swap! atom1 inc)
(prn @atom1)) 

(defn agent1-change []
(prn "agent1-change")
(send agent1 inc)
(prn @agent1))

(defn ref1-change []
(prn "ref1-change")
(dosync
(alter ref1 inc))
(prn @ref1))


Call them:
the
 

;"atom1-change"
;1

(prn @atom1)
;1

(agent1-change)
;"agent1-change"
;0

(prn @agent1)
;1

(ref1-change)
;"ref1-change"
;1

(prn @ref1)
;1

We see that in the case of the agent (send agent1 inc) is called asynchronously. This agent is different from the rest.

We will need a delay function. We define it using a standard Sleep .Net interop and see in action.
the
(defn sleep [ms] 
(. System.Threading.Thread (Sleep ms))) 


Create a stream that will increase the value of the agent every second.
the
(defn agent1-thread[x] 
(sleep 1000)
(send *agent* agent1-thread)
(prn x)
(inc x))

(send agent1 agent1-thread)

*agent* is the current agent.
There may seem (I thought) that if you swap sleep and send, happen, something terrible. Because send is called recursively and asynchronously, the thread needs to be fruitful and multiply, but that never happens and we can write this:

the
(defn agent1-thread[x] 
(send *agent* agent1-thread)
(sleep 1000)
(prn x)
(inc x))

(send agent1 agent1-thread)

Drawbacks, however, is. Please note that the last line (inc x) returns the value stored in the agent is increased by 1 and this last return value is written to the agent.

Now three ways to put Clojure on the floor with eksepsi stack overflow:

the
(reset! atom1 atom1)

(dosync (ref-set ref1 ref1))

(send agent1 (fn [_] agent1))

Here we record the values of atom Ref agent themselves. This leads to a stack overflow.

Often agents are used to organize timers without changing the value of the agents themselves, for example:

the
(def timer (agent nil) )
(defn on-timer [_] 
(sleep 1000)
(send *agent* on-timer)
(prn "tic-tac"))

(send timer on-timer)

It is worth noting that prn returns nil and that nil is recorded in the value of the agent.
As the send function returns the agent itself, a great way to shoot yourself in the foot:

the
(def timer (agent nil) )
(defn on-timer [_] 
(sleep 1000)
(prn "tic-tac")
(send *agent* on-timer))

(send timer on-timer)

And then, like, even be all right until you want to see what is the timer a is it stack overflow with the fall of Clojure.

The following illustrative example:

the
(def agent1 (agent nil))
(def agent2 (agent nil)) 
(def timer (agent nil))

(def atom1 (atom 0))
(def atom2 (atom 0))

(defn atoms-change-thread1 [_]
(reset! atom1 1)
(sleep (rand-int 90))
(reset! atom2 2)
(sleep (rand-int 90))
(send *agent* atoms-change-thread1)
nil)

(defn atoms-change-thread2 [_]
(reset! atom1 3)
(sleep (rand-int 90))
(reset! atom2 4)
(sleep (rand-int 90))
(send *agent* atoms-change-thread2)
nil) 

(defn on-timer [_]
(prn @atom1 @atom2)
(sleep 1000)
(send *agent* on-timer)
nil) 

(send agent1 atoms-change-thread1)
(send agent2 atoms-change-thread2)
(send timer on-timer)

Here we have two atoms and two threads changing their value.
The first thread writes into atomic values 1 and 2, the second stream — the values 3 and 4.
Also there is a timer that times per second displays the values of the atoms.
The output will be something like this:

3 4
1 2
1 4
3 2
1 2
3 4
1 4
3 2

Now finally about the transaction. Let's rewrite the previous example, but instead of atoms we will have REFs.

the
(def agent1 (agent nil))
(def agent2 (agent nil)) 
(def timer (agent nil))

(def ref1 (ref 0))
(def ref2 (ref 0))

(defn refs-change-thread1 [_]
(dosync 
(ref-set ref1 1)
(sleep (rand-int 90))
(ref-set! ref2 2)
(sleep (rand-int 90)))
(send *agent* refs-change-thread1)
nil)

(defn refs-change-thread2 [_]
(dosync
(ref-set ref1 3)
(sleep (rand-int 90))
(ref-set! ref2 4)
(sleep (rand-int 90)))
(send *agent* refs-change-thread2)
nil)

(defn on-timer [_]
(prn @ref1 @ref2)
(sleep 1000)
(send *agent* on-timer)
nil) 

(send agent1 refs-change-thread1)
(send agent2 refs-change-thread2)
(send timer on-timer)

The output will be something like this
3 4
3 4
1 2
3 4
1 2
3 4
1 2
3 4
3 4
1 2

Now it becomes clear why write dosync. This definition of transaction boundaries. Change of REFs occurs transactional. In this they differ from the rest.

Although, if you replace references to agents, we also see transactioncost. Agents also operate in STM, and the transaction boundary is the entire function of changing the value of the agent.

the
(def agent1 (agent nil))
(def agent2 (agent nil)) 
(def timer (agent nil))

(def agent3 (agent 0))
(def agent4 (agent 0))

(defn agents-change-thread1 [_]
(send agent3 (fn [_] 1))
(sleep (rand-int 90))
(send agent4 (fn [_] 2))
(sleep (rand-int 90))
(send *agent* agents-change-thread1)
nil)

(defn agents-change-thread2 [_]
(send agent3 (fn [_] 3))
(sleep (rand-int 90))
(send agent4 (fn [_] 4))
(sleep (rand-int 90))
(send *agent* agents-change-thread2)
nil)

(defn on-timer [_]
(prn @agent3 @agent4)
(sleep 1000)
(send *agent* on-timer)
nil) 

(send agent1 agents-change-thread1)

(send timer on-timer)

1 2
3 4
1 2
1 2
3 4
1 2
1 2
1 2

Finally, after all this, you can return to the anthill. If strongly to simplify the task, then it will look like this:

the
(def v [1 1 0 0 0])
(def world (vec 
(map 
(fn [x] (ref x))
v)))
(defn place [x] (world x))
(def agent1 (agent 0))
(def agent2 (agent 1))
(def agent-show-world (agent nil))

(defn agent-change [x]
(let [old (place x)
new-coord (rand-int (count world)) 
new (place new-coord)]
(sleep (rand-int 50))
(if (= @old, 1)
(do
(send *agent* agent-change)
(dosync
(if (= @new 0)
(do
(ref-set old 0)
(ref-set new 1)
new-coord)
x)))
(prn "agent" *agent* "is out"))))

(defn show-world [_]
(sleep 1000)
(send *agent* show-world)
(prn (map (fn [x] (deref x)) world)))

(send agent-show-world show-world)
(send agent1 agent-change)
(send agent2 agent-change)

Example output will be like this
(1 0 0 1 0)
(0 0 1 1 0)
(1 0 0 1 0)
(0 1 1 0 0)
(0 0 1 1 0)
(1 0 0 1 0)
(0 0 1 1 0)
(0 0 1 1 0)
(1 0 1 0 0)
(1 0 0 1 0)
(0 0 1 0 1)
(0 1 0 1 0)
(1 0 1 0 0)
(1 0 0 1 0)
(0 0 0 1 1)
(0 1 1 0 0)
(1 1 0 0 0)
(1 0 1 0 0)
(0 0 0 1 1)
(1 0 1 0 0)
(1 0 0 0 1)
(1 0 0 1 0)
(0 1 0 0 1)

Here we identified the vector v to the first element unit and the rest zeros. In the context of anthill, we assume that the unit is ants, and the remaining free cells. Did the world vector is a vector of REFs, whose values are elements of the vector V. We have two agents whose values are the coordinates of the vector world. And there are two streams that move units along the vector world. Due transaction two threads do not write simultaneously the unit in the same cell.

Please note that a transaction wrapped not only recording but also reading check:

the
(dosync
(if (= @new 0)
(do
(ref-set old 0)
(ref-set new 1)
new-coord)
x)))

If a new, randomly generiruemoi, the coordinate is 0, i.e. an empty cell, then write in the old cell 0, and in the new 1. So move the ant from the old coordinates to the new one.

If you wrap in a transaction, only record is not thread-safe.

the
(defn agent-change [x]
(let [old (place x)
new-coord (rand-int (count v)) 
new (place new-coord)]
(sleep (rand-int 50))
(if (= @old, 1)
(do
(send *agent* agent-change)
(if (= @new 0)
(do
(dosync
(ref-set old 0)
(ref-set new 1))
new-coord)
x))
(prn "agent" *agent* "is out"))))


With this function the output will be something like this:
(0 0 0 1 1)
(1 0 1 0 0)
(0 0 1 0 1)
(1 0 0 0 1)
agent # < Agent@549043: 4> "is out"
(1 0 0 0 0)
(0 0 1 0 0)
(0 0 1 0 0)
(0 1 0 0 0)
(0 0 0 1 0)
(0 0 1 0 0)

I.e. the flows recorded at the same time unit in the same place, then one thread jacked this unit in a new location, and the second did not find its units in the place where the left and curled. All. Was an ant and did not. Or rather, two ants came together.

As for multithreading, that's all.

How to implement graphics:

the
(def ant-vert-bitmap
'(0 0 0 0 0 0 0 0 0 2 2 0 0 0 1 1 0 0 0 0
0 0 0 0 0 1 1 0 2 2 2 2 0 1 0 0 0 0 0 0
0 0 0 0 1 0 0 1 2 3 3 2 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 2 2 1 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 0 0 1 5 5 1 0 0 0 0 0 0 0 0
0 0 0 0 1 0 0 0 1 4 4 1 0 0 0 1 0 0 0 0
0 0 0 0 0 1 0 0 0 1 1 0 0 0 1 0 1 0 0 0
0 0 0 0 0 0 1 0 1 5 5 1 0 1 0 0 0 1 0 0
0 0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 0 0 1 4 4 1 0 0 0 0 0 0 0 0
0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0
0 0 0 0 0 0 0 1 5 5 5 5 1 0 0 0 1 0 0 0
0 0 0 0 0 0 0 1 5 5 5 5 1 1 1 0 0 1 0 0
0 0 0 0 0 1 1 1 5 4 4 5 1 0 0 1 0 0 0 0
0 0 0 0 1 0 0 0 1 1 1 1 0 0 0 0 1 0 0 0
0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))

(def ant-diag-bitmap
'(0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 2 2 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 3 2 1 0
0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 2 2 1 0 1
0 0 0 0 0 0 0 0 1 0 0 0 1 5 5 1 1 0 0 0
0 0 0 0 0 0 0 0 1 0 0 0 1 4 5 1 0 0 0 0
0 0 0 0 1 1 1 0 1 0 1 1 1 1 1 0 0 0 0 0
0 0 0 0 0 0 1 0 1 1 1 5 1 0 0 0 0 0 0 0
0 0 1 0 0 0 1 1 1 5 4 1 1 0 0 0 0 0 0 0
0 1 0 1 0 1 5 5 5 1 5 1 0 1 1 0 0 0 0 0
0 0 0 0 1 5 5 5 5 5 1 0 0 0 0 1 0 0 0 0
0 0 0 0 1 5 4 5 5 5 1 1 1 1 0 0 0 0 0 0
0 0 0 0 1 5 4 4 5 5 1 0 0 1 0 0 0 0 0 0
0 0 0 0 1 5 5 5 5 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))

It's the ant. There are options for rotation and flip these matrices. Then everything is being rendered in .Net Bitmap:

the
(defn render-bitmap [bitmap bit color]
(let [bit-pos (positions #{bit} bitmap)
rendered-bitmap (Bitmap. bitmaps-bitmaps dim-dim)]
(doseq [b bit-pos] 
(let [dy (quot bitmaps b-dim)
dx (rem bitmaps b-dim)]
(.SetPixel rendered-bitmap dx dy color)))
rendered-bitmap))


All images are cached. When rendering of the frame everything is taken from the cache.
Well, with regard to Windows Form and Chart here is code:

the
(def current-wins (atom nil))
(def win-app (agent nil))
(def winforms-app-inited? (atom false))

(def chart (atom nil))
(def series (atom nil))

(defn get-series [series-name]
(first (filter 
(fn [x] 
(if (= (. x Name) series-name) true false)) @series)))

(defn add-xy [series-name x y]
(let [series (get-series series-name)]
(when series
(.AddXY (.Points series) x y)
(when (> (.Count (.Points series)) 500) (.RemoveAt (.Points series) 0))))) 

(defn create-series [chart] 
(let 
[series1 (. chart Series)]
(.Add series1 "herb")
(.Add series1 "sugar")
(.Add series1 "anthill-sugar")
(.Add series1 "aphises")

(doseq [s series1]
(doto s 
(.set_ChartType SeriesChartType/Spline)
(.set_IsVisibleInLegend true)
))
(reset! series series1) 
(doto (get-series "herb")
(.set Color/Green)
(.set_LegendText "Herb")
)
(doto (get-series "sugar")
(.set Color/White)
(.set_LegendText "Free sugar")
)
(doto (get-series "anthill-sugar")
(.set (Color/FromArgb 255 61 115 0))
(.set_LegendText "Anthill sugar")
) 
(doto (get-series "aphises")
(.set (ControlPaint/Light Color/Green))
(.set_LegendText "Aphises")
))) 

(defn chart-update[chart]
(add-xy "anthill-sugar" @world-time (anthill-sugar-calc))
(add-xy "herb" @world-time (herb-calc))
(add-xy "sugar" @world-time (free-sugar-calc))
(add-xy "aphises" @world-time (count @aphises))

(let [chart-areas (. chart ChartAreas)
chart-area (first chart-areas)
axis-x (. chart-area AxisX)]
(doto axis-x 
(.set_Minimum (if (> @world-time 500) (- @world-time 500) 0))
(.set_Maximum (if (> @world-time 500) @world-time 500)))))

(defn create-form []
(let [form (Form.)
panel (Panel.)
animation-timer (timer.)
world-timer (timer.)
chart1 (the Chart.)
series1 (. chart1 Series)]
(doto chart1
(.set_Name "chart1")
(.set_Location (new Point size 0))
(.set_Size (Size. size size))
(.set_BackColor (ControlPaint/Light bgcolor)))

(.Add (. chart1 ChartAreas) "MainChartArea")
(.Add (. chart1 Legends), "Legend")

(doto (first (. chart1 ChartAreas))
(.set_BackColor bgcolor))

(doto (first (. chart1 Legends))
(.set_BackColor bgcolor))

(create-series chart1)
(reset! chart chart1)
(chart-update chart1)

(let [chart-areas (. chart1 ChartAreas)
chart-area (first chart-areas)
axis-x (. chart-area AxisX)
axis-y (. chart-area AxisY)]
(doto axis-x (.set_IsStartedFromZero true))
(doto axis-y (.set_IsStartedFromZero true)))

(doto panel
(.set_Location (new Point 0 0))
(.set_Name "panel1")
(.set_Size (Size. size size))
(.add_Click 
(gen-delegate EventHandler [sender, args]
(when (= (.Button args) MouseButtons/Right)
(swap! show lens? (fn [x] (not x))))
(when (= (.Button args) MouseButtons/Left)
(let [mouse-x (@mouse-pos 0)
mouse-y (@mouse-pos 1)
x (/ mouse-x-scale)
y (/ mouse-y-scale)
p (place [x y])]
(prn [x y], @p)
(.Focus panel)))))
(.add_MouseMove 
(gen-delegate MouseEventHandler [sender, args]
(reset! mouse-pos [
(* (quot (.X args) scale) scale)
(* (quot (.Args Y) scale) scale)])))
(.add_MouseWheel 
(gen-delegate MouseEventHandler [sender, args]
(let [f (fn [x] 
(let 
[new-sleep (+ x (* 50 (/ (.Delta args) 120)))]
(if (> new-sleep 0) new-sleep 0)))]
(swap! ant-sleep-ms f)
(swap! ladybug-sleep-ms f)
(swap! aphis-sleep-ms f)
(prn @ant-sleep-ms)))))

(doto animation-timer
(.set_Interval animation-sleep-ms)
(.set_Enabled true)
(.add_Tick (gen-delegate EventHandler [sender, args]
(do 
(when @buf-graph 
(.Render (@buf-graph 0) (@buf-graph 1)))
(reset! rectangles-in-cells [])
(reset! rendered-bitmaps [])
(let [v (vec (for [x (range dim) y (range dim)] 
@(place [x y])))]
(dorun 
(for [x (range dim) y (range dim)]
(render-place (v (+ (* x dim) y)) x y)))
(reset! buf-graph (render panel))
(when @show-lens? 
(reset! buf-graph (render-lens))))))))

(doto world-timer
(.set_Interval 5000)
(.set_Enabled true)
(.add_Tick (gen-delegate EventHandler [sender, args]
(swap! world-time inc)
(chart-update chart1)))) 

(doto (.Form Controls)
(.Add panel)
(.Add chart1))
(doto form
(.set_ClientSize (Size. (* 2 size) size))
(.set_Text "Super Ants"))
form))

(defn init-winforms-app []
(when-not @winforms-app-inited?
(Application/EnableVisualStyles)
(Application/SetCompatibleTextRenderingDefault to false)
(reset! winforms-app-inited? true)))

(defn start-gui [x]
(init-winforms-app)
(reset! current wins (create-form))
(Application/Run @current-wins))


I hope it was not boring.

Thank you for your attention.
Article based on information from habrahabr.ru

Comments

Popular posts from this blog

Powershell and Cyrillic in the console (updated)

Active/Passive PostgreSQL Cluster, using Pacemaker, Corosync

Automatic deployment ElasticBeanstalk using Bitbucket Pipelines