In the following snippet (producer-receiver using channel), would it be possible to iterate the channel instead of using "loop" followed by "if" (perhaps with a macro)?

c1 = Channel(Int32).new
c2 = Channel(Nil).new

spawn do
  (1..10).each do |i|
    sleep(100.milliseconds)
    c1.send(i)
  end
  c1.close
end

spawn do
  loop do
    if i = c1.receive?
      puts i
    else
      c2.send(nil)
    end
  end
end

c2.receive
puts "Done"

could

  loop do
    if i = c1.receive?
      ...
    end
  end

be replaced by something like:

c1.each do |item|
   puts item
end

perhaps via a macro

(Obviously , the inspiration is the iteration over a channel with a “for x:= range c1”) in Go.

Crystal’s stdlib Channel isn’t meant to be used that way, but it can be with select (very similar in concept to Go’s select):

loop do
  select
  when value = channel.receive
    yield value
  else
    break
  end
end

Could probably have an Enumerable object for this:

channel = Channel(String).new(10)

5.times { |i| channel.send i.to_s }

doubled = channel.range.map do |string|
  string * 2
end

pp doubled: doubled

class Channel
  def range
    Range.new(self)
  end

  struct Range(T)
    include Enumerable(T)

    def initialize(@channel : Channel(T))
    end

    def each
      loop do
        select
        when value = @channel.receive
          yield value
        else
          break
        end
      end
    end
  end
end

Keep in mind, though, that select on channels at a high-enough volume may not be feasible. Since stdlib channels need to be extremely flexible, they aren’t optimized for single-consumer workloads. Specifically, they can be consumed by any number of fibers safely, but “safe” doesn’t mean “non-blocking” and that safety comes at a performance cost.

If you just need to be able to consume a channel from a single fiber while other fibers write to it, I created a shard a while back called mpsc (multi-producer/single-consumer) optimized for this purpose. Checking if the channel can be read without blocking (via receive?) involves no heap allocations and the benchmark results are pretty wild:

➜  mpsc git:(master) ITERATIONS=1 crystal run --release benchmarks/mpsc_vs_select.cr
Empty channel
MPSC::Channel 430.59M (  2.32ns) (± 2.00%)  0.0B/op        fastest
      Channel   6.71M (149.01ns) (± 0.92%)  288B/op  64.16× slower

The difference with a larger number of iterations per block invocation gets even more extreme:

➜  mpsc git:(master) ✗ ITERATIONS=1000 crystal run --release benchmarks/mspsc_vs_select.cr
Empty channel
MPSC::Channel   1.06M (941.00ns) (± 1.33%)   0.0B/op         fastest
      Channel   6.71k (148.94µs) (± 0.61%)  281kB/op  158.28× slower
4 Likes