Ruby Ractor: pass an arbitrary block

Let’s say you have a piece of code:

class SomeThing
  def initialize(thing)
    @thing = thing
  end

  def call
    puts @thing.inspect
  end
end

You want to execute this in parallel in a ractor:

Ractor.new { SomeThing.new(1).call }

This works if you have the block written within the Ractor. But it will fail if the block is refferenced from the outside of a ractor.

element = proc { SomeThing.new(1).call }
Ractor.new { element.call }

# <internal:ractor>:267:in `new': can not isolate a Proc because it accesses
#   outer variables (element). (ArgumentError)

To do that you need to instance_eval from a shareable object:

class Worker
  def initialize(&block)
    @block = block
  end

  def run
    block = @block

    Ractor.current.instance_eval { Ractor.new(&block) }
  end
end

worker = Worker.new { SomeThing.new(1).call }
worker.run.take

Important Link to heading

The Ractor.current.instance_eval usage changes the value of self within the block, and you can not reference variables implicitly.

one    = 1
worker = Worker.new { SomeThing.new(one).call }
worker.run.take

# <internal:ractor>:267:in `new': can not isolate a Proc because it accesses
#   outer variables (one). (ArgumentError)

So make sure you pass all dependencies through the Ractor constructor:

class Worker
  def initialize(*args, &block)
    @args  = args
    @block = block
  end

  def run
    args  = @args
    block = @block

    Ractor.current.instance_eval { Ractor.new(*args, &block) }
  end
end

one    = 1
worker = Worker.new(one, 2, 3) { |*args| SomeThing.new(args).call }
worker.run.take