DEV Community 👩‍💻👨‍💻

Rob Reid
Rob Reid

Posted on • Originally published at robreid.io

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")
Enter fullscreen mode Exit fullscreen mode
$ crystal run example.cr
a
b
c
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
$ crystal run example.cr
a
b
c
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode
$ crystal run example.cr
a
b
c
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)

Update Your DEV Experience Level:

Settings

Go to your customization settings to nudge your home feed to show content more relevant to your developer experience level. 🛠