Nil check syntax confusion

I’m wondering why the second version of handling_nil compiles fine but the first doesn’t.

class A
  @a : Array(Int32)?

  def from_the_database : Array(Int32)
    rand(1) == 1 ? [2] : [] of Int32
  end

  # This will not compile
  # $ crystal build tryit.cr
  # Showing last frame. Use --error-trace for full trace.
  # 
  # In tryit.cr:19:17
  # 
  #  19 | if (a = @a).empty?
  #                   ^-----
  # Error: undefined method 'empty?' for Nil (compile-time type is (Array(Int32) | Nil))
  def handling_nil
    @a ||= from_the_database
    if (a = @a).empty?
      @a = [2]
    end
    @a
  end

  # This one is okay
  # def handling_nil
  #   @a ||= from_the_database
  #   if (a = @a) && a.empty?
  #     @a = [2]
  #   end
  #   @a
  # end
end

a = A.new
pp a.handling_nil

The last example in the docs here suggest to me it should be fine. Am I reading something into this that I shouldn’t?

if @a.is_a?(String)
  # here @a is not guaranteed to be a String
end

a = @a
if a.is_a?(String)
  # here a is guaranteed to be a String
end

# A bit shorter:
if (a = @a).is_a?(String)
  # here a is guaranteed to be a String
end

My system:

$ crystal version
Crystal 1.1.0 [af095d72d] (2021-07-14)

LLVM: 10.0.0
Default target: x86_64-apple-macosx

Good question!

I thought it would work but apparently it doesn’t. Only the if (a = @a).is_a?(...) case works, you can’t use any method.

Feel free to open a feature request for this.

Thank you!

1 Like

It can’t work. The conditional expression is (a = @a).empty?. It assigns the value of @a (which is nilable) to a and then calls a method on a. This method is not callable for nil value, so you get a compiler error. There is no expression that restricts a to non-nil value.

(a = @a).is_a? can be called because #is_a? is defined on Nil.

Another method that’s defined on Nil is #try. It’s useful for cases like these. You can call it on any nilable value and the methods block is only executed if the value is non-nil. (a = @a). try &.empty? returns true if the value is not nil and empty, false otherwise.

But again, this does not restrict the type of a to non-nil.

3 Likes

Oh, you are right! My bad… what you said makes total sense.

Thanks for the replies. They make sense but I needed to have a little think about it as I can see I’m not thinking about things in quite the right way. To be clear:

  • The compiler checks are against the type, at runtime they’ll be against the value?

  • if (a = @a) && a.empty? works because && means the Nil type can’t occur on the right hand side?

Sorry if these seem blatantly obvious, I’m trying to shift my lazy thinking to the new paradigm :sweat_smile:

&& works because it is implemented on top of if:

(a = @a) && a.empty?

# is equivalent to:

if a = @a
  a.empty? # `a` is non-nil here
else
  a
end
1 Like