Block forwarding and signature inference

This code does not compile:

def forward_block(&block)
  [1, 2, 3].sort_by(&block)
forward_block { |a| -a }

Error is

 4 | forward_block { |a| -a }
Error: wrong number of block arguments (given 1, expected 0)

However, it works if I make the block signature explicit:

def forward_block(&block : Int32 -> Int32)
  [1, 2, 3].sort_by(&block)
forward_block { |a| -a } # => [3, 2, 1]

Is that a limitation of the inference logic?

1 Like

There are two different semantics with block arguments:

  • Inline blocks (inlined with yield)
  • Captured blocks (called with or passed as a proc)

In your example, the block is captured because you pass it on to sort_by. But captured blocks requires restrictions for input and output types (see Capturing blocks).
That makes the second example work.

An alternative would be to use an inline block which is forwarded not by passing a captured proc, but forwarding with yield:

def forward_block(&)
  [1, 2, 3].sort_by { |a| yield a }
forward_block { |a| -a }

The error message is probably not very helpful in this case, and we need to improve on that.
I think, with the new unnamed block argument syntax, we could move forward to disallow using named block arguments with yield, which would help separate both variants more clearly. Then a named block argument would always require type restrictions, which would make this example error in def forward_block(&block).


In your unnamed block example, you do not need to specify & in the method signature for the code to work, right? (Did not know this notation!)

1 Like

Exactly. You can omitted. The syntax for unnamed block arguments was only added recently. But Iā€™d consider making it mandatory, even if only to differentiate the signature from the non-yielding overload.