Skip to content

Passing block arguments

Philipp Schulz edited this page May 18, 2022 · 2 revisions

One of the most powerful features of Anyolite is the ability to pass Ruby blocks to Crystal. This allows for extremely flexible scripting and features like event triggers.

There are two ways in which blocks can be passed: Direct block passing and block storage.

Direct block passing

Direct block passing is relevant if you want to keep the Crystal behavior of block-accepting methods. Since Anyolite has no direct way of telling whether a block was passed, you need to mark methods in a specific way:

module Test
  @[Anyolite::AddBlockArg(0, Nil)]
  def self.call_block_twice
    yield
    yield
  end
end

The Anyolite::AddBlockArg annotation accepts two arguments: The number of block variables and the block return type. The return type can also be specified as Nil (always returning nil) or Anyolite::RbRef (returning a reference to the Ruby value).

A more complex example demonstrates how to handle blocks with multiple arguments and return types:

module Test
  @[Anyolite::AddBlockArg(2, String | Int32)]
  def self.give_two_numbers_to_block
    return_value = yield 1, 2
    return_value.to_s
  end
end

Here are two different ways to call this block (from either Ruby or Crystal):

Test.give_two_numbers_to_block {|a, b| "#{a} #{b}"}
Test.give_two_numbers_to_block {|a, b| a + b}

As long as the block has two arguments and returns either a String or an Int32, it is valid to use it here.

Currently, optional block arguments are impossible, so different methods or a thin Ruby layer around these methods are required for the choice of passing blocks or not.

Generally, this way of handling blocks is more restrictive, but also easier to implement.

Block storage

If it is necessary to store a Ruby block, another approach is necessary:

module Test
  @[Anyolite::StoreBlockArg]
  def self.store_and_call_twice
    stored_block = Anyolite.obtain_given_rb_block
    Anyolite.call_rb_block(stored_block, nil)
    Anyolite.call_rb_block(stored_block, nil)
  end
end

This method works similar to the call_block_twice method above, except it does not work in Crystal. However, it has the advantage that the block is now a Anyolite::RbRef variable, which can be stored indefinitely and called at any point from Crystal (as long as the interpreter is still running - it is your task to take care of that).

Multiple arguments work a bit different to direct block passing, but are still easy to do. Similar to the other example above, this is now done by passing an argument Array:

module Test
  @[Anyolite::StoreBlockArg]
  def self.store_and_give_two_numbers
    stored_block = Anyolite.obtain_given_rb_block
    Anyolite.call_rb_block(stored_block, [1, 2], cast_to: String)
  end
end

Since the stored block can now be stored as a RbRef, it is also possible to accept arbitrary return types. Only when casting them to Crystal, their return types need to be known.

Note that the two annotations used here can also be used on the class or module in which the method is defined, e.g. using Anyolite::AddBlockArgInstanceMethod or Anyolite::StoreBlockArgInstanceMethod. Then, the first argument of the annotation must be the name of the respective method, followed by the usual arguments (see the documentation for more details).