Type bug?

Take this:

class X
  property y : String
  
  def initialize(@y) end
end

class Test
  @x : X?
  @x_y: String
  
  def initialize(@x : X)
    @x_y = @x.y
  end
end

Test.new(X.new("test"))

And we get

Error: undefined method 'y' for Nil (compile-time type is (X | Nil))

Obviously, the compiler is looking at the type of the instance var, but ought this not work?

Of course it can be fixed with

  def initialize(x : X)
    @x = x
    @x_y = x.y
  end

but that feels a bit stupid.

I think this is working as expected. You declare the ivar as @x : X? so to the compiler @x can be nil. However, def initialize(@x : X) is short for doing @x = x in the method body. So I think you really could just do

  def initialize(@x : X)
    @x_y = x.y
  end

and it should just work. since it’ll be using the implicit x argument to initialize that is not nilable versus the @x instance var which is nilable.

Or just make @x not nilable like it is in the constructor via @x : X.

The compiler does this because, generally speaking, it has to account for times where the instance variable can change between two lines of code because that change can occur in another fiber/thread.

On one hand, it makes a lot of sense for the compiler to be extra safe. On the other hand, I don’t know that it makes sense to apply this particular safeguard to initialize specifically. Other fibers/threads aren’t going to change my object while it’s being initialized because they can’t possibly hold a reference to it because even my current call stack won’t have a reference to it until after the initialize method returns.

Unfortunately (for this case, at least — it’s arguably a good thing in general), there is no guarantee that initialize will only be called during object initialization. There is nothing stopping you from calling it later. In fact, that’s exactly how my Redis client implements self-healing — if you get disconnected from the server, it will reconnect and retry the command.

So it seems unintuitive, but the compiler is doing the best thing (or least bad, depending on your perspective) considering all other aspects of the current compiler implementation. Maybe the compiler could detect that initialize is only ever called downstream from new and relax that constraint a bit, but the implicit local variable is a pretty solid workaround.

1 Like

Oho, didn’t realize that @x still defines a local variable with the type of the parameter. Works for me!

1 Like

You can absolutely do this by redefining .new to use the newly allocated blob of memory before calling #initialize on it. Your .new might not even call #initialize at all. I don’t think #initialize should be specially handled in this regard

One of the hard parts about static languages is providing guarantees for all the weird stuff that shouldn’t happen but can because there’s nothing preventing someone from doing them.