What's making my block a captured block?

I’m struggling to understand why a block is a captured block i.e. why i get Error: can’t use yield inside a proc literal or captured block.

My code is similar to this, which also gives the same error:
https://play.crystal-lang.org/#/r/c3ky

TreeNode#each captures the block, while TreeNodeUser#each yields. You can’t yield from inside a captured block.

See Capturing blocks - Crystal for more details on capturing blocks.

I realize that the differentiation between captured and yielded blocks can be difficult. See Continue with anonymous block arguments · Issue #8764 · crystal-lang/crystal · GitHub for a proposal to introduce more explicit syntactic differences in method signatures.

Hm, i thought that forwarding it wouldn’t capture it. But ok, I see now.

And I guess I can’t fix this recursive case easily:
https://play.crystal-lang.org/#/r/c3lo

Maybe I can use some kind of visitor pattern to get around this.

I think I’ve read "To forward non-captured blocks, you must use yield" multiple times in the docs, but now i’ve actually read it.

I think I’m starting to get this now :)

To capture a block you must specify it as a method’s block parameter, give it a name and specify the input and output types.

I think this tricked me. I thought i had to explicit specify input and output type for the block. Or maybe I’m missing something? Wouldn’t it be enough to say that one must specify it as a method’s block parameter? hm

Yeah, that’s a bit misleading. If you omit the type restriction, it defaults to Proc(Nil).

I don’t think it’s misleading, it’s just wrong. The docs should say that a captured block is one where you mention the name of the block inside the method’s body. The signature doesn’t matter at all for this to happen.

1 Like

Today another question was raised in my head.

Is this block captured or not?

def f(&block : String -> _)
  yield "hello"
end

According to the docs it should be, but it feels like it’s not since I’m just yielding and never accesses the block parameter.

If I change the block parameter definition to

def f(&block : Int32 -> _)
  yield "hello"
end

i get a compiler error, so the block parameter isn’t ignored which indicates that it’s captured?

Will the compiler change yield to block.call?

A captured block is one where the block argument is mentioned inside the method definition, not just in the arguments list. Like this:

def f(&block)
  block # here I'm using it
end

In any other case, it’s not a captured block, even if you give it a name in the arguments list.

The current docs are wrong and should be updated accordingly.

3 Likes

Thanks, that confirms my findings after comparing the llvm-ir output!