Crystal's Channels and the While Loop

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 me 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