Like most, I like to learn a new programming language by doing. At the moment, I'm enjoying the catharsis of completing unimplemented Rosetta Code tasks in Crystal and vlang.
During some exploration of the Crystal programming language, I took on the Synchronous Concurrency task to pour some of my knowledge of the CSP pattern from Go, into Crystal.
The task asks you to communicate between multiple threads of execution within a process:
One of the concurrent units will read from a file named "input.txt" and send the contents of that file, one line at a time, to the other concurrent unit, which will print the line it receives to standard output. The printing unit must count the number of lines it prints. After the concurrent unit reading the file sends its last line to the printing unit, the reading unit will request the number of lines printed by the printing unit. The reading unit will then print the number of lines printed by the printing unit.
This task requires two-way communication between the concurrent units. All concurrent units must cleanly terminate at the end of the program.
My original solution to the task was as follows:
File.write("input.txt", "a\nb\nc")
lines = Channel(String).new
spawn do
File.each_line("input.txt") do |line|
lines.send(line)
end
lines.close
end
begin
while
line = lines.receive
puts line
end
rescue ex : Channel::ClosedError
end
File.delete("input.txt")
$ crystal run example.cr
a
b
c
The Crystal docs here and here are fairly light on Channels, so there was an element of trial and error involved. I knew that a call to receive
against a closed channel would result in a raised Channel::ClosedError
so I made use of this in a try/catch (begin/rescue) block.
I'm a big fan of Go, so much prefer to handle errors over catching exceptions. I wasn't satisfied with this solution and wanted to see if I could make Crystal's type system work to my advantage.
Crystal's type system makes use of Union types, allowing a variable to be one or more types at compile time. For example, because a
is initialised in both arms of the if statement in the following example, it can be either a String
or an Int32
:
if true
a = "string"
else
a = 1
end
puts typeof(a)
# => (Int32 | String)
What's interesting about Union types in the context of Channels, is Nil
. There's another version of the receive
method, that instead of raising an exception when a channel is closed, it returns nil. It's called receive?
and its definition can be found here.
Initially, I simply swapped out the called to receive
with receive?
and re-ran my code:
File.write("input.txt", "a\nb\nc")
lines = Channel(String).new
spawn do
File.each_line("input.txt") do |line|
lines.send(line)
end
lines.close
end
begin
while
line = lines.receive?
puts line
end
rescue ex : Channel::ClosedError
end
File.delete("input.txt")
I was expecting the call to receive?
to block indefinitely after reading the last line from "input.txt" but was surprised to see the program output exactly what it had done in the original example:
$ crystal run example.cr
a
b
c
Confused, I added a log line into the rescue
arm to see if a Channel::ClosedError
exception had been thrown anyway (despite what the Crystal source told me) and re-ran:
rescue ex : Channel::ClosedError
puts ex
$ crystal run example.cr
a
b
c
No exception!?
Then it dawned on my that the while
loop must be working to truthy values and ignoring the line break!
I rewrote my code to this new expectation, removing the begin/rescue block and the line break, inlining the declaration of line
, and relying on nil from the fourth call to receive?
:
File.write("input.txt", "a\nb\nc")
lines = Channel(String).new
spawn do
File.each_line("input.txt") do |line|
lines.send(line)
end
lines.close
end
while line = lines.receive?
puts line
end
File.delete("input.txt")
$ crystal run example.cr
a
b
c
Success! I've learned something new about Crystal by fumbling around in the dark.
The following demonstrates this succinctly by reading from the array elements until nil:
a = [1, 2, 3, nil, 4, 5]
while b = a.shift
puts b
end
Top comments (0)