Guessing type over proc

This code fails to compile because the type of t is just Nil and it does not have #[] at the first
type reduction, I think.

def foo(&)
  t = nil
  (1..10).each do |i|
    t = yield(t, i)
  end
  t
end
 
ret = foo do |t, i|
  a = -> do
    { a: i, b: (t ? t[:b] : nil) }
  end
  b = -> do
    { a: (t ? t[:a] : nil), b: i }
  end
  if rand > 0.2
    a.call
  elsif rand > 0.2
    b.call
  else
    t
  end
end
 
pp! ret
Showing last frame. Use --error-trace for full trace.

error in line 11
Error: undefined method '[]' for Nil

https://play.crystal-lang.org/#/r/9hd0

But, the following code which same except without proc compiles fine.

def foo(&)
  t = nil
  (1..10).each do |i|
    t = yield(t, i)
  end
  t
end
 
ret = foo do |t, i|
  if rand > 0.2
    { a: i, b: (t ? t[:b] : nil) }
  elsif rand > 0.2
    { a: (t ? t[:a] : nil), b: i }
  else
    t
  end
end
 
pp! ret
ret # => {a: 10, b: 1}

https://play.crystal-lang.org/#/r/9hd2

What is different here?

The difference is that a Proc captures the variables that exist outside of them. When this happens, the compiler can’t know when the Proc is going to be called, and whether the variable is going to change.

You are looking for this bug which is not going to be fixed anytime soon.

The workaround is to assign t to another variable inside the Proc:

def foo(&)
  t = nil
  (1..10).each do |i|
    t = yield(t, i)
  end
  t
end

ret = foo do |t, i|
  a = ->do
    t2 = t
    {a: i, b: (t2 ? t2[:b] : nil)}
  end
  b = ->do
    t2 = t
    {a: (t2 ? t2[:a] : nil), b: i}
  end
  if rand > 0.2
    a.call
  elsif rand > 0.2
    b.call
  else
    t
  end
end

pp! ret
1 Like

Thanks. I got it.

So, proc captures variables by their names on the scope of defining.
I thought that proc captures the referencing objects of variables used inside.

I’m also going to note this: I found this comment from the given link which describes why type check (t ? t[:b] : nil) does not work as expected.