The Crystal Programming Language Forum

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.

3 Likes