The Crystal Programming Language Forum

Difference between underscore and empty as block return value

I assumed block type restrictions & : -> and & : -> _ to be pretty much equivalent.
And I couldn’t find any difference between them, except that underscore return type restriction doesn’t always work.

For example the following code does not compile:

def foo(& : -> _)
  yield
end

def recursive
  if false
    foo { recursive } # Error: can't infer block return type, try to cast the block body with `as`.
  end
end

recursive

But it compiles if the method signature is foo(& : -> ).

Am I missing any other differences?
If not, should they rather be equivalent in all regards? Because that only difference in behaviour seems rather odd.

_ is “something”. “Returns something”. Is your example NoReturn? It would make perfect sense then.

Without _ type restriciton, the compiler figures the return type to be Nil. With _ type restriction, an explicit .as(Nil) needs to be added.
I’m mostly just wondering if there’s any other difference between both variants.

If there’s any difference it wasn’t on purpose.

3 Likes

Okay, that’s what I expected :+1:

Not related to the language directly: a difference is in the generated API docs:

Code:

def bar(& : ->)
  yield
end

def foo(& : -> _)
  yield
end

Result:

def bar(&)
def foo(& : -> _)
1 Like

That would be a doc generator bug. A type restriction to a proc type with no argumens is different from no type restriciton.

I take _ to mean ‘It returns any sort of value’, and that includes Nil, but the compiler is throwing because of the recursiveness of the function. If you did this with Generics it might be more clear:

class Test(C)
  def foo(& : -> C)
    yield  # Return type is C
  end
end

def recursive
  if false
    Test(Nil).new.foo { recursive }
  end
end  # Returns Nil | Nil, which is Nil.

recursive

In this case, there would be no compiler error because it can figure out the return type. In your code:

def foo(& : -> _)
  yield  # Return type is return type of block
end

def recursive  # Return type is `return type of block | Nil`
  if false
    foo { recursive }  # Return type of block contains itself, so compiler can't infer return type here.
  end
  # Here it returns Nil
end

recursive

It almost like a rogue generic, except here the generic references itself, meaning we can’t infer the return type of the block.

But the thing is the compiler correctly infers the return type if the _ is omitted (which really has the same semantics).

But to my knowledge, they don’t. _ means it has any return value. When it is omitted, it means there is no return value, which is Nil.

That’s the semantic for procs, but not for non-captured blocks.

This is completely valid:

def foo(& : ->)
  yield.upcase
end

foo { "foo" }

this seems inconsistent from the documentation:

in both cases you use some_func { block } so I must be missing something

This documentation is about procs. The block argument &block is captured in both examples.