Checking a nillable type prior to passing it to a function which expects it to not be nil

Hello, new to Crystal here.

I have a larger example that I boiled down to just using Rand and RandBuilder for the Builder pattern, but was surprised when my mental model of the first snippet of code didn’t translate over to when using it with a struct instance variable.

def test(x : Int32) : String?
  if x == 1
	 "wtf"
  else
  	nil
  end
end

x = test(1)

puts "typeof(x) OUTSIDE if x: #{typeof(x)}"
if x
  puts "typeof(x) INSIDE if x: #{typeof(x)}"
end

OUTPUT:
typeof(x) OUTSIDE if x: (String | Nil)
typeof(x) INSIDE if x: String

Ideally, in the code below, Rand’s instance variable @s would be String and not String?, but I have it that way so that the puts inside of RandBuilder’s builder method will be outputted.

struct RandBuilder
  @s : String?
  
  def initialize(@s) end
  
  def s(x : String)
    @s = x
    self
  end
  
  def build()
    puts "typeof(@s) OUTSIDE of if @s: #{typeof(@s)}"
      
    if @s
      puts "typeof(@s) INSIDE of if @s: #{typeof(@s)}"
      Rand.new(@s)
    else
	  raise "MUST provide @s when constructing a Rand struct"
    end
  end
end
 
struct Rand
  @s : String?
  
  def initialize(@s) end
  
  def self.builder()
    RandBuilder.new(nil)
  end
end
 
result = Rand.builder().s("rand_Str").build()
puts "Builder result: #{result}"

OUTPUT:
typeof(@s) OUTSIDE of if @s: (String | Nil)
typeof(@s) INSIDE of if @s: (String | Nil)
Builder result: Rand(@s=“rand_Str”)

Without Rand’s instance variable @s being set to String?, the following error is received:
error in line 26
Error: instance variable ‘@s’ of Rand must be String, not (String | Nil)

Not really sure what posting this will yield, but I felt I should.

EDIT:

Just a few moments later I thought to google ‘crystal lang typecasting’

Changing the build method to this makes it work in the way that I’d want it to:

... all other code unchanged

 def build()
    puts "typeof(@s) OUTSIDE of if @s: #{typeof(@s)}"
      
    if @s
      puts "typeof(@s) INSIDE of if @s: #{typeof(@s)}"
      Rand.new(@s.as(String))
    else
	  raise "MUST provide @s when constructing a Rand struct"
    end
  end

... all other code unchanged

OUTPUT:
typeof(@s) OUTSIDE of if @s: (String | Nil)
typeof(@s) INSIDE of if @s: (String | Nil)
Builder result: Rand(@s=“rand_Str”)

Hi!

Yes, that’s expected behavior. It’s documented here: if var - Crystal

TL;DR: another fiber/thread might change the code between the if check and the actual branch execution, invalidating the assumption that was made in the if check. This can’t happen with local variables because, well, they are local :slight_smile:

A very simple solution, and one that’s idiomatic code, is to first assign the instance variable to a local variable:

s = @s
if s
  # do something with s
end

Or even (also idiomatic):

if s = @s
  # do something with s
end
1 Like

Hey, thank you for the fast reply.

The fiber/thread explanation makes sense.

And the solution you provided, I did actually try within the build method, and it still didn’t fix the issue of s being String as opposed to (String | Nil) after the if s check.

Can you make an example on Carcin of that behavior?

You’re correct asterite and Blacksmoke16, my apologies.

Perhaps I was testing the changes hastily, but after retrying it in the REPL, the solution provided does work.

However, I have ran into additional odd idiosyncratic behavior (if not for my lack of Crystal experience).

In which case perhaps I can make another forum post regarding this (and I’ve also thought of creating a blog series of the issues I’ve run into as well, in an attempt to encourage newcomers like me to take a liking to the language and further adoption).

Here’s the Crystal Playground link for the solution which I’ve previously and erroneously deemed as an insufficient solution to my original post:
https://play.crystal-lang.org/#/r/cc59/edit

^^ Uncommenting line 12 and replacing s = @s with s on line 15 yields the expected results as posted in asterite’s response.

This would be ideal yes, assuming it’s not related to this current thread’s topic.

Just wanted to follow up by saying thank you both once more.

I have to admit and apologize that when I posted this I didn’t do a forum search for similar posts (after revisiting my post today and clicking through a few more older posts I eventually found a similar thread where the OP ran into the same situation which I posted about: Nil checks demistifaction)

My hastiness was born out of frustration in the moment.

Not only am I new to Crystal, but to regularly programming in a Static programming language in general; so there’s always the thought in the back of my mind of not knowing whether the issue I’ve run into at the moment that’s impeding forward progress is due to my lack of knowledge or some obscure edge case that just may not have been discovered in the language yet (more times than not it will be the first, but just due to how new Crystal is, I can’t rule out the second without asking those more experienced than myself).

So thank you once again, the fast response time was really quite surprising for me.

And next time I post, I’ll be sure to have done my due diligence on the forums and at least post the links I’ve found which may have been somewhat close to my query, but fall short in getting me to the answer I’m seeking additional input from someone more experienced.

4 Likes

Running into a similar error here in this code (line 301 is the problem)…

I also tried flipping the else block to be used in the if block after checking mode != nil, but a similar error is raised.

I’m thrown off though, because the nil check was done, and it’s a local variable rather than being an instance variable of the Lex Class. This seems to be an erroneous result.

Rather than checking if mode == nil here, try if mode.nil?. An == check doesn’t split the Nil type from the other types. Instead, you have to use one of the pseudo methods (implemented in the type checker, IIRC):

  • is_a?
  • .nil?

Or even just something like this (my usual preference for its brevity):

if mode = determine_mode(first_char_of_str)
  # mode is guaranteed not nil here at compile time
else
  # mode is nil here
end
1 Like

For posterity, when using the if var = something syntax, that also ensures var is not false or a null pointer. If those are a valid value, and you only want to do something when its explicitly not nil, would need to do:

something.try do |var|
  # var is not nil, but could be false or a null pointer
end

Most of the time those types are not part of the domain of that variable so it’s not usually a problem. Just something to keep in mind.

3 Likes

Wow, always feel like such a newb when I get these responses back. The if mode = determine_mode(first_char_of_str) syntax is interesting.

Also, are there any other languages that have this behavior that Crystal implements regarding these nil checks? If I recall correctly, this is a feature unique to Crystal.

lol When I first started learning Crystal, I had a lot of the same questions. There are a few weird humps to get over, especially if you’ve been working with languages like Ruby, JS, or Python most of your career.

I would not be the least bit surprised to learn that this is unique to Crystal, at least when it comes to statically typed languages. Languages like Rust focus on technical correctness above all else and it makes it very verbose. The Crystal core team cares about friendliness of the language just as much as technical correctness — in a lot of cases you can still write code that emphasizes technical correctness (though not quite to a pathological extent), but the code shouldn’t be frustrating to work with or take quite as long to grok.