Why does this type inference fail?

One AoC puzzle => many questions :grinning:.

Today, I could not understand why in this simplified example:

class C
  def initialize(listing : String)
    @program = listing.split(',').map(&.to_i)


the compiler is not able to infer that @program is an Array(Int32). My reasoning is:

  • listing has a type restriction
  • String#split returns an Array(String)
  • String#to_i either returns Int32 or raises ArgumentError

A one-liner seems to support that argument:

$ crystal eval 'p typeof("1,2".split(",").map(&.to_i))'

Is my reasoning above wrong in some step? Are ivars special in some sense perhaps?

Technically, the compiler could resolve the type of this expression.

But this is not necessarily as easy as that. Complex expressions can in turn rely on other instance variables being fully typed. This could cause loop dependencies and never work. And it’s not trivial to decide whether an expression can be typed without interdependencies or not. Thus the compiler only infers type from very simple expressions and asks you to annotate in all other cases. Even if technically that wasn’t strictly necessary.

For reference: https://crystal-lang.org/reference/syntax_and_semantics/type_inference.html

1 Like


Since typeof does not evaluate its arguments, why is it able to return a type in the one-liner?

The compiler was able to do it in version 0.15:

But fullscale type inference for all instance vars was too slow (especially because types of instance variables can change), so it was decided to change it

Now instance variables are typed in a separate pass before other methods and local variables. So compiler don’t know about String#split and String#to_i return types at the time it is selecting type of @program.
Of course in theory it is possible to solve, but in general this is not too big problem and as a bonus you get readability (ability to easily see types of all instance variables).


Ahhh, that squares. Thanks a lot for the pointer to that post.

For the archives, I have reduced the example to something simpler.

This compiles:

class C
  def initialize(x : Int32)
    @x = x

C.new(1) # fine

but just introduce a method call, and it does not:

class C
  def initialize(x : Int32)
    @x = x.abs

C.new(1) # can't infer the type of instance variable '@x' of C
1 Like