Confused about type inference

Hi!

I’ve been using Crystal for advent of code to explore it and I’ve been finding it slightly confusing with the type inference sometimes, but I’m not sure if I’m missing something or if it’s a bug.

For instance, given the following function:

    def my_func : Int64 | Nil
      dist = Array(Int64 | Float32).new(1, 0_i64)

      raise "oh no" if dist[0].is_a? Float32
      dist[0]
    end

I get the following error from the compiler:

In src/day15.cr:56:19

 56 | def my_func : Int64 | Nil
                    ^
Error: method Day15::CaveNavigator#my_func must return (Int64 | Nil) but it is returning (Float32 | Int64)

Isn’t my if enough guard against it being Float32?

I have a similar, but slightly different issue with Nil checks, if I have something like:

    @foo : Nil | Int64
    def my_func
      raise "oh no" if @foo.nil?

      @foo.format("")
    end

I also get:

In src/day15.cr:60:12

 60 | @foo.format("")
           ^-----
Error: undefined method 'format' for Nil (compile-time type is (Int64 | Nil))

In both caes it seems an if around the condition is not enough to nudge the compiler the right way. I tried using a switch-case for the first example and I still get a similar error. For the second one it seems I can circuvent it by using .not_nil! but it feels to me the raise should take care of that.
Is this an error or is it a missing check somehow?

In this case the compiler can’t prove that the return value is an Int64, at least in the way you’re doing it. The reason being is that the compiler knows the array can contain Int64 | Float32 values. However it does not know the type of each value at a specific index. E.g. it would be possible for you to check the first item, but then something later on could change that first value allowing it to return an incorrect value.

To make it work, you can first assign the value to a local var and use that:

if (v = dist[0]).is_a? Float32
  raise "oh no"
end

v

In this example the compiler knows that v is not an Float32 since it would raise, so v must be an Int64.

See if var - Crystal.

2 Likes

To expand on that: The compiler can’t prove that two consecutive calls dist[0] return the same result. The type of the second call can’t be reduced depending on the program flow after the first call.
This can only be done for non-closured, local variables. Hence the idiomatic solution is to assign the return value to a local variable. That will be properly restricted. And btw. it’s also more efficient because it avoids the second array fetch - which is unnecessary when the value is identical.

1 Like

Welcome to Crystal!

You can also write (Int64 | Nil) as Int64?.

1 Like

Cool, thanks for the answers!

Makes sense, when you say that the compiler can’t prove that two consecutive calls return the same result, that’s for thread safety reasons or something else that I didn’t catch?

Essentially yes. It would be possible for another fiber to mutate the array by shuffling the values which would cause the next access to return a possibly different value. Granted that’s not possible in this small example, but the compiler is still catching that scenario.

1 Like