Crystal: Nil check for instance variables

Created on 27 May 2017  路  4Comments  路  Source: crystal-lang/crystal

Hi there!

I am just trying out crystal language recently. Found the following example does not work:

class X
property :var
def initialize(@var : String | Nil)

end

def some_method
@var = "initialized"
@var.split("") => this throws error during compilation "undefined method 'split' for Nil (compile-time type is (String | Nil))"
end

def some_method
return if @var.nil?
@var.split("") => this throws error during compilation "undefined method 'split' for Nil (compile-time type is (String | Nil))"
end
end

I think the nil check can be improved in both examples above:

  1. In the first example, the variable is already initialized inside of the method
  2. In the second example, the function returns early with a manual nil check

Most helpful comment

There's a section in the Gitbook (albeit a rather hidden one) that covers both of these cases, but somewhat indirectly.

This is because any method call could potentially affect that instance variable, rendering it nil. Another reason is that another thread could change that instance variable after checking the condition.

For the first example:

def some_method
  @var = "initialized"
  @var.split("")
end

raises an error because of the second part of the quote above. Another thread could potentially change the value of @var in between the assignment and the method call, so it's type can't be restricted.

The second example:

def some_method
  return if @var.nil?
  @var.split("")
end

is more tied to the first sentence of the quote, but also affected by the second.

There are a few solutions to this kind of issue, the main one being to assign to a local variable first, where the value _can't_ be affected by anything other than the local scope. For example:

class X
  property :var

  def initialize(@var : String | Nil)
  end

  def some_method
    @var = "initialized"
    # Only call `.split` if `@var` is not nil.
    @var.try( &.split(""))
  end

  # or

  def some_method
    @var = "initialized"
    # Raises an error if `@var` is nil
    @var.not_nil!.split("")
  end

  # or

  def some_method
    @var = "initialized"
    # Local assignment followed by `if` as a suffix
    var = @var
    var.split("") if var
  end

  # or

  def some_method
    if var = @var
      # `var` is a local, so it's type can be (and is) restricted inside of the if.
      var.split("")
    end
  end
end

Note that using if var only restricts the type of the variable _inside_ the condition. It would be nice if return unless var restricted the type, but I don't think that's universally possible, so it hasn't been implemented.

All 4 comments

There's a section in the Gitbook (albeit a rather hidden one) that covers both of these cases, but somewhat indirectly.

This is because any method call could potentially affect that instance variable, rendering it nil. Another reason is that another thread could change that instance variable after checking the condition.

For the first example:

def some_method
  @var = "initialized"
  @var.split("")
end

raises an error because of the second part of the quote above. Another thread could potentially change the value of @var in between the assignment and the method call, so it's type can't be restricted.

The second example:

def some_method
  return if @var.nil?
  @var.split("")
end

is more tied to the first sentence of the quote, but also affected by the second.

There are a few solutions to this kind of issue, the main one being to assign to a local variable first, where the value _can't_ be affected by anything other than the local scope. For example:

class X
  property :var

  def initialize(@var : String | Nil)
  end

  def some_method
    @var = "initialized"
    # Only call `.split` if `@var` is not nil.
    @var.try( &.split(""))
  end

  # or

  def some_method
    @var = "initialized"
    # Raises an error if `@var` is nil
    @var.not_nil!.split("")
  end

  # or

  def some_method
    @var = "initialized"
    # Local assignment followed by `if` as a suffix
    var = @var
    var.split("") if var
  end

  # or

  def some_method
    if var = @var
      # `var` is a local, so it's type can be (and is) restricted inside of the if.
      var.split("")
    end
  end
end

Note that using if var only restricts the type of the variable _inside_ the condition. It would be nice if return unless var restricted the type, but I don't think that's universally possible, so it hasn't been implemented.

Thank you, guys!
This is a great learning for me!

It would be nice if return unless var restricted the type, but I don't think that's universally possible, so it hasn't been implemented.

I'm pretty sure it is implemented and restricts the type properly. Of course it can't restrict @fields, but in the form return unless var = @field it restricts the var as non-nil for the rest of method.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

nabeelomer picture nabeelomer  路  3Comments

asterite picture asterite  路  3Comments

relonger picture relonger  路  3Comments

oprypin picture oprypin  路  3Comments

oprypin picture oprypin  路  3Comments