In this chapter, you will write an étude that uses core.async to do asynchronous processing. Even though the JavaScript environment is single-threaded, core.async allows you to work with anything that needs to be handled asynchronously; this is a very nice feature indeed.
Here are two examples of using core.async. In the first example, Annie and Brian are going to send each other the numbers 5 down to zero, stopping at zero, in a project named async1
. You will need to add some :require
and :require-macro
specifications to your namespace:
(
ns
^
:figwheel-always
async1.core
(
:require-macros
[
cljs.core.async.macros
:refer
[
go
go-loop
]])
(
:require
[
cljs.core.async
:refer
[
<!
>!
timeout
alts!
chan
close!
]]))
Then, define a channel for both Annie and Brian:
(
def
annie
(
chan
))
(
def
brian
(
chan
))
Annie gets two processes: one for sending messages to Brian and another for receiving messages from him.
(
defn
annie-send
[]
(
go
(
loop
[
n
5
]
(
println
"Annie sends"
n
"t` Brian"
)
(
>!
brian
n
)
(
when
(
pos?
n
)
(
recur
(
dec
n
))))))
(
defn
annie-receive
[]
(
go-loop
[]
(
let
[
reply
(
<!
brian
)]
(
println
"Annie receives"
reply
"from Brian"
)
(
if
(
pos?
reply
)
(
recur
)
(
close!
annie
)))))
In the annie-send
function, you see the go
function, which asynchronously executes its body and immediately returns to the calling function. The >!
function sends data to a channel. The loop continues until n
equals zero, at which point the function returns nil
.
Because go
and loop
occur together so often, ClojureScript has the go-loop
construct, which you see in the annie-receive
function. That function loops (but does not need the loop variable) until it has received the zero, at which point it performs a close!
on the channel.
A similar pair of functions brian-send
and brian-receive
do Brian’s sending and receiving tasks (they are not shown here). You may have noticed there’s a lot of duplication here; we’ll get rid of it in the next example.
All that remains to be done is write a function that invokes these processes:
(
defn
async-test
[]
(
do
(
println
"Starting..."
)
(
annie-send
)
(
annie-receive
)
(
brian-send
)
(
brian-receive
)))
Here is the console log output from invoking async-test
. You can see that this is indeed asynchronous; the sends and receives are in no particular order.
Starting... Annie: 5 -> Brian Annie: 5 <- Brian Brian: 5 -> Annie Brian: 5 <- Annie Annie: 4 -> Brian Annie: 3 -> Brian Brian: 4 -> Annie Brian: 3 -> Annie Annie: 4 <- Brian Annie: 3 <- Brian Brian: 4 <- Annie Brian: 3 <- Annie Annie: 2 -> Brian Annie: 1 -> Brian Brian: 2 -> Annie Brian: 1 -> Annie Annie: 2 <- Brian Annie: 1 <- Brian Brian: 2 <- Annie Brian: 1 <- Annie Annie: 0 -> Brian Brian: 0 -> Annie Annie: 0 <- Brian Brian: 0 <- Annie
You can see the entire program here: Sample core.async Program 1.
The next example using core.async, in a project named async2
, has processes that communicate with one another in a semi-synchronized manner. In this case, Annie will start off by sending Brian the number 8; he will send her a 7; she sends back 6, and so on, down to zero.
In this case, both people do the same thing: send the next lower number to their partner, then await the partner’s reply. Here is the function to activate the process for the two partners. The from-str
and to-str
parameters are used for the debug output.
(
defn
decrement!
[[
from-str
from-chan
]
[
to-str
to-chan
]
&
[
start-value
]]
(
go-loop
[
n
(
or
start-value
(
dec
(
<!
from-chan
)))]
(
println
from-str
":"
n
"->"
to-str
)
(
>!
to-chan
n
)
(
when-let
[
reply
(
<!
from-chan
)]
(
println
from-str
":"
reply
"<-"
to-str
)
(
if
(
pos?
reply
)
(
recur
(
dec
reply
))
(
do
(
close!
from-chan
)
(
close!
to-chan
)
(
println
"Finished"
))))))
There are several clever tricks going on in this function. The & [start-value]
allows an optional starting value. There’s an asymmetry in the processes; Annie starts the sending, and Brian starts by receiving her data. Thus, Annie will start with 8 as her start-value
; Brian will omit that argument. The completion of this bit of kabuki is in (or start-value (dec (<! from-chan)))
; if start-value
is nil
(which evaluates to false), you take one less than the received value as your starting value.
Similarly, the when-let
clause is executed only when the reply from from-chan
is true (i.e., not nil
).
(
defn
async-test
[]
(
let
[
annie
(
chan
)
brian
(
chan
)]
(
activate
[
"Annie"
annie
]
[
"Brian"
brian
]
8
)
(
activate
[
"Brian"
brian
]
[
"Annie"
annie
])))
Here is the output from invoking async-test
.
Annie : 8 -> Brian Brian : 7 -> Annie Annie : 7 <- Brian Annie : 6 -> Brian Brian : 6 <- Annie Brian : 5 -> Annie Annie : 5 <- Brian Annie : 4 -> Brian Brian : 4 <- Annie Brian : 3 -> Annie Annie : 3 <- Brian Annie : 2 -> Brian Brian : 2 <- Annie Brian : 1 -> Annie Annie : 1 <- Brian Annie : 0 -> Brian Brian : 0 <- Annie Finished
You can see the entire program here: Sample core.async Program 2.
In this étude, you’re going to write a program that lets the computer play the card game of "War" against itself.
(Apologies to Sun Tzu.) These are the rules of the game as condensed from Wikipedia, adapted to two players, and simplified further.
Two players each take 26 cards from a shuffled deck. Each person puts their top card face up on the table. Whoever has the higher value card wins that battle, takes both cards, and puts them at the bottom of her stack. What happens the if the cards have the same value? Then the players go to "war." Each person puts the next two cards from their stack face down in the pile and a third card face up. High card wins, and the winner takes all the cards for the bottom of their stack. If the cards match again, the war continues with another set of three cards from each person. If a person has fewer than three cards when a war happens, they put in all their cards.
Repeat this entire procedure until one person has all the cards. That player wins the game. In this game, aces are considered to have the highest value, and King > Queen > Jack.
A game can go on for a very long time, so I have added a new rule: if the game goes more than a pre-determined maximum number of rounds (50 in my program), stop playing. The person who has fewer cards win. If the number of cards is equal, it’s a tie.
Absolutely nothing. Well, almost nothing. War is possibly the most incredibly inane card game ever invented. It is a great way for children to spend time, and it’s perfect as an étude because
When you purchase an item, if you pay cash on the spot, you often end up paying less than if you used credit. If you are cooking a meal, getting all of the ingredients collected before you start (pay now) is often less stressful than having to stop and go to the grocery store for items you found out you didn’t have (pay later). In most cases, “pay now” ends up being less expensive than “pay later,” and that certainly applies to most programming tasks.
So, before you rush off to start writing code, let me give you a word of advice: Don’t. Spend some time with paper and pencil, away from the computer, and design this program first. This is a non-trivial program, and the “extra” time you spend planning it (pay now) will save you a lot of time in debugging and rewriting (pay later). As someone once told me, “Hours of programming will save you minutes of planning.”
Trust me, programs written at the keyboard look like it, and that is not meant as a compliment.
Note: This does not mean that you should never use the REPL or write anything at the keyboard. If you are wondering about how a specific part of ClojureScript works and need to write a small test program to find out, go ahead and do that right away.
Hint: Do your design on paper. Don’t try to keep the whole thing in your head. Draw diagrams. Sometimes a picture or a storyboard of how the messages should flow will clarify your thinking. (If your parents ever asked you, “Do I have to draw you a diagram?”, you may now confidently answer “Yes. Please do that. It really helps.”)
When I first started planning this, I was going to have just two processes communicating with one another, as it is in a real game. But let’s think about that. There is a slight asymmetry between the players. One person usually brings the cards and suggests playing the game. He shuffles the deck and deals out the cards at the beginning. Once that’s done, things even out. The game play itself proceeds almost automatically. Neither player is in control of the play, yet both of them are. It seems as if there is an implicit, almost telepathic communication between the players. Of course, there are no profound metaphysical issues here. Both players are simultaneously following the same set of rules. And that’s the point that bothered me: who makes the “decisions” in the program? I decided to sidestep the issue by introducing a third agent, the dealer, who is responsible for giving the cards to each player at the start of the game. The dealer then can tell each player to turn over cards, make a decision as to who won, and then tell a particular player to take cards. This simplifies the message flow considerably.
In my code, the dealer had to keep track of:
The dealer initializes the players, and then is in one of the following states. I’m going to anthropomorphize and use “me” to represent the dealer.
If I really have cards from both players, compare them. If one player has the high card, give that player the pile plus the cards currently in play, and go into post-battle state. Otherwise, the cards match. Add the cards currently in play to the pile, and go back to “Pre-battle” state.
Note that this is my implementation; you may find an entirely different and better way to write the program.
Remember that the order in which a process receives messages may not be the same order in which they were sent. For example, if players Annie and Brian have a battle, and Annie wins, you may be tempted to send these messages:
This works nicely unless Annie had just thrown her last card down for that battle and message two arrives before message one. Annie will report that she is out of cards, thus losing the game, even though she’s really still in the game with the two cards that she hasn’t picked up yet.
I decided to represent the deck as a vector of the numbers 0 through 51 (inclusive); 0 through 12 are the Ace through King of clubs, 13 through 25 are diamonds, then hearts, then spades. (That is, the suits are in English alphabetical order.) You will find ClojureScript’s shuffle
function to be quite useful. I wrote a small module in a file named utils.cljs for functions such as converting a card number to its suit and name and finding a card’s value.
If you want to make a web-based version of the game, you will find a set of SVG images of playing cards in the datafiles/chapter08/images directory, with names 0.svg through 51.svg. These file names correspond to the numbering described in the preceding paragraph. The file blue_grid_back.svg contains the image of the back of a playing card.
Note: You may want to generate a small deck with, say, only four cards in two suits. If you try to play with a full deck, the game could go on for a very long time.
Here is output from a game:
Starting Player 1 with [2 0 16 13 14 18] Starting Player 2 with [1 4 3 15 17 5] ** Starting round 1 Player 1 has [2 0 16 13 14 18] sending dealer (2) Player 2 has [1 4 3 15 17 5] sending dealer (1) 3 of clubs vs. 2 of clubs Player 2 receives [2 1] add to [4 3 15 17 5] ** Starting round 2 Player 1 has [0 16 13 14 18] sending dealer (0) Player 2 has [4 3 15 17 5 2 1] sending dealer (4) Ace of clubs vs. 5 of clubs Player 2 receives [0 4] add to [3 15 17 5 2 1] ** Starting round 3 Player 1 has [16 13 14 18] sending dealer (16) Player 2 has [3 15 17 5 2 1 0 4] sending dealer (3) 4 of diamonds vs. 4 of clubs ** Starting round 4 Player 2 has [15 17 5 2 1 0 4] sending dealer (15 17 5) Player 1 has [13 14 18] sending dealer (13 14 18) 6 of diamonds vs. 6 of clubs ** Starting round 5 Player 1 has [] sending dealer () Player 2 has [2 1 0 4] sending dealer (2 1 0) nil vs. Ace of clubs Winner: Player 1
See a suggested solution: Solution 8-1.