[Blog Post] 5 Use cases for Crystal's select statement

Hi folks :raised_hands:

I finally managed to put my thoughts on Crystal’s select in order. You can find them here:

Not much has been written on the topic, so let me know if any section lacks in clarity or you spot any imprecision. I would love to start the conversation here :point_down:

11 Likes

Well done. I don’t think most people even know that the select statement exists in Crystal, and given that it’s completely undocumented it’s really hard to figure out how to use it. The only real solution is to look at Golang (ick) docs, which of course aren’t a 1:1 match.

You should think about making a PR and updating the docs, seeing as you seem to have a pretty good knowledge of select and how it works.

4 Likes

Hi Ibarasti, thanks for this blog post, very cool indeed.

Would you mind taking a look at this code, I would like to hear your thoughts on whether you would deem it “idiomatic” or how you would change things. E.g. should callers provide the channel instead?

Coming from async await I need to wrap my head around how to do these basic 3 control flow scenarios using Channels:

  • a then b then c
  • a and b then c
  • a or b then c then a or b (remaining)

Code:

def a()
  ch = Channel(String).new
  spawn do
    sleep rand(1..3)
    ch.send "a done"
  end
  ch
end

def b()
  ch = Channel(String).new
  spawn do
    sleep rand(1..3)
    ch.send "b done"
  end
  ch
end

def c()
  ch = Channel(String).new
  spawn do
    sleep rand(1..3)
    ch.send "c done"
  end
  ch
end

def a_b_c()
  puts a().receive
  puts b().receive
  puts c().receive
  puts "a then b then c, done"
end

def a_and_b_c()
  chs = [a(), b()]
  puts chs.map { |ch| ch.receive }
  puts c().receive
  puts "a and b then c, done"
end

def a_or_b_c_remaining()
  # a or b -> c -> a or b
  chs = [a(), b()]
  puts Channel.receive_first(chs)
  puts c().receive
  puts Channel.receive_first(chs)
  puts "a or b then c then a or b, done"
end

a_b_c()
a_and_b_c()
a_or_b_c_remaining()

Hey @peheje, the rationale for creating channels within a function scope, is to give visibility to the fact that the fiber therein defined owns the channel, and is responsible for writing to it and, eventually, closing it.

I often find myself using this pattern, but I can think of use cases where you’d want to operate differently.
For example, you might want to pass the same channel to a number of fibers writing - possibly different types of messages - to it, but only have one collector reading from such channel. In this scenario, you’ll likely initialise a channel upfront, then pass it to each fiber - including the consumer. None of the fibers writing to the channel will be responsible for the channel’s closure.
You can think of this example as a Message Bus / Dispatcher pattern where you have one channel (the Bus), many writer fibers and one fiber routing messages to the right consumers (Dispatcher).

Anyway, digression aside, your code looks good and correct :tada: A couple of idiomatic notes

  1. I think Channel.receive_first(a, b) will also work, so no need to define the auxiliary chs array.
  2. no need for the parenthesis when calling a function with no arguments, so c().receive becomes c.receive
  3. This one is up to personal preference, but I’ll mention it: I tend to use Object.tap in order to not have to initialise the ch variables, e.g.
def b
  Channel(String).new.tap { |ch|
    spawn do
      sleep rand(1..3)
      ch.send "b done"
    end
  }
end

It would be good to see it in the context where it’s called. For example, I’d expect you to want to perform these sort of operations in a loop, or maybe pass a terminate channel, too, as an argument, to ensure you can interrupt the send operation as needed.

1 Like

Wow lbarasti, thanks for the detailed response and code review.

I actually really like this channel approach, seems very flexible without many language constructs needed like in async/await pattern, although that can be nice too.

I agree on all three points, I’ll have to look into .tap, I’m not coming from Ruby but more Python, C# and the like so some of these idioms are new to me. Added: In point 1) I can’t write Channel.receive_first(a, b) twice as that will invoke a and b twice, but that may not have been clear from my part. :slightly_smiling_face:

I think I see your point with the terminate channel, a dedicated channel only for stopping, kind of like the cancellation token some other languages use?

The code is purely for fun atm, not using it for anything particular.

Thanks again and have a nice evening.