Why does is the compile time still an Array(string) | Nil even tho i check i not_nil! it right before

haii, so i am reading from a json config, so it i normal that it is possibly nil. but then i check MULTIPLE TIMES that is isnt nil, and i do not get why i still get this error:

In main.cr:91:20

 91 | config_domains.each do |domain|
                     ^---
Error: undefined method 'each' for Nil (compile-time type is (Array(String) | Nil))

some snippet of my code:

crystal

class Config
   include JSON::Serializable
    property domains : Array(String) | Nil
end
 
if File.exists?(config_path)
       file = File.new(config_path)
       content = file.gets_to_end
       json_content = Config.from_json(content)
       file.close
  
      if json_content.domains
          config_domains = json_content.domains
        end
      end
  
end
  if config_domains.not_nil!
      config_domains.each do |domain|
     end

I would appreciate any help. thanks!

as I work through growing pains in Crystal, I’m still getting bit by similar type union bugs in my own code because I lost the habit. I’m finding that try is a great way to handle Nil unions.

class TryMe
@domains : Array(String) | Nil

def loop_domains
@domains.try &.each do |entry|
puts entry.to_s
end
end
end

t = TryMe.new
t.loop_domains

the above code has no errors and will correctly loop over the @domains instance variable. The ampersand shortcut will work on any method hanging off of your target. In this case it’s an array. you could also do someting like @domains.try &size as an example.

Second observation: you can use a shortcut for | Nil as a type union but just adding a question mark.

@domains : Array(String)? does the same thing as the previous definition in the example.

lastly, using the Play feature in the browser is a great way to work out these little gremlins. cheers

Omg thanks it worked!!!

Also really sorry to ask again, but any ideas on how to solve this? try & does not work TwT

if config_interval

 sleep config_interval.minutes

end

i got


 106 | sleep config_interval.try &.minutes
                             ^--
Error: expected argument #1 to 'sleep' to be Time::Span, not (Time::Span | Nil)

Overloads are:
 - sleep(seconds : Number)
 - sleep(time : Time::Span)
 - sleep()

You also can use a if statement:

class TryMe
  @domains : Array(String) | Nil

  def loop_domains
    if domains = @domains
      domains.each do |entry|
        puts entry.to_s
      end
    end
  end
end

t = TryMe.new
t.loop_domains

Note the assignment in the if statement: the compiler can’t know if the @domain is still not null inside the if statement, therfore you have to assign it to a local variable domain which is ensured to be non-null

You’re issue is that try returns a potential nil type (Type? or Type | Nil). without the try it should work.

Same as the answer before you can leverage the assign operator in the if statement:

if interval = config_interval
  sleep interval.minutes
end

The effects on types for various if instructions are described in if var - Crystal and its sibling pages.

1 Like

right! As I understood it, the ampersand trick is syntactic sugar to avoid having to explicitly define the if statement. I preferred this method because it made it easier for me to identify nil union handling statements among other if/else heavy code. cheers

Ohhh ok like that! i guess i am too used to typescript where you just check like if (a) and then tsc knows its not undefined. I was doing the if wrong. But thanks!!!

Since I’m not seeing anyone saying this explicitly (though it is explained in the linked docs):

When you have a nilable instance variable like this:

class SomeClass
  property some_nilable_property : String?

  def initialize(@some_nilable_property)
  end
end

and then you try to use it like this:

def some_function(something : SomeClass)
  if something.some_nilable_property # nil is falsey, so branches to else if nil
    puts "something has length #{something.some_nilable_property.size}"
  else
    puts "something is nil :("
  end
end

or

def some_function(something : SomeClass)
  something.some_nilable_property.not_nil! # raises if nil
  puts "something has length #{something.some_nilable_property.size}"
end

then the compiler can’t be sure that something.some_nilable_property isn’t nil, even though you checked, because it’s in a class instance and could be modified and accessed from multiple fibers. So, in theory, some other fiber (in another thread, probably) could have set something.some_nilable_property to nil between the time you checked and the time you tried to use it.

@treagod’s answer is what I consider the standard way to handle this difficulty in Crystal, and in this case it would look something like

def some_function(something : SomeClass)
  if (some_string_value = something.some_nilable_property)
    # some_string_value is a local variable, so it's definitely not nil if the
    # program reaches this branch
    puts "something has length #{some_string_value.size}"
  else
    puts "something is nil :("
  end
end
2 Likes


class Foo
  property str
  @str : String?

  def initialize
  end

  def getval : String
    @str.try {@str} || ""  
  end
end

t = Foo.new

puts "first cache: #{t.getval}"
t.str = "update"
puts "second cache: #{t.getval}"

is this kind of thing frowned upon?

I’ve been using the logical-or operator for the falsey bits.

1 Like

I think this solution definitely works. I would say that for anyone in this situation it’s worth looking at whether you actually need a nilable instance variable or if there’s a natural default value (like the empty string in this case) that you should use instead. If the implementation were expanded, Foo might very well need a nilable string, but it also might not. However, I do think you could do it without the #try:

  def getval : String
    @str || ""  
  end

Also, you can put the type restriction on the same line:

  property str : String?
1 Like
struct Foo
  # inline alias easy to see what's public and what isn't.
  getter v1, v2 

  # all variables vertically defined the same way for each class/struct.
  # nil union types == fast inits.
  @v1 : UInt32?
  @v2 : UInt32?
  @v3 : UInt64?
  # class vars for memoization
  @@v4 : StaticArray(UInt8, 4)?
  @@v5 : Bytes?

  def initialize
  end

  def lazyinit
    @v1 ||= 555555_u32
    @v2 ||= 666666_u32
    @v3 ||= 99999999999_u64
  end

  # custom getter for instance var
  def getv3 : UInt64
    lazyinit()
    @v3 || 0_u64
  end

  def memoizev4 : Void
    @@v4 ||= begin
      bytes = [128_u8, 128_u8, 64_u8, 0_u8]
      b = StaticArray(UInt8, 4).new { |i| bytes[i] }
      b
    end
    return Void # defeat implicit return of @@v4
  end

  def getv4 : StaticArray(UInt8, 4)
    @@v4 || StaticArray(UInt8, 4).new(0_u8)
  end

  def v5 : Bytes
    @@v5 ||= expensive_setup_method
  end

  def expensive_setup_method : Bytes
    puts "yup, expensive!"
    Bytes[66, 66, 0, 0]
  end
end

a = Foo.new.v1 || 0      # fast init w/ deterministic default.
f = Foo.new; b = f.getv3 # v3 protected access caches on first access 

# low ceremony, direct use of nillable w/o wrapping it getv3. 
f.v1.try { |val| puts "this is ... #{a}" }

c = a &+ b                 # wrapping addition for UInt64
f.memoizev4                # memoize in other setup
f.getv4.to_slice.hexstring # access memoized value
f.v5                       # assigns once
f.v5.hexstring             # returns cached


this is a bit better assembled example of how I’ve been using nillables. I do a lot of work with ints and staticarrays; much of this is down to personal preference but maybe it’s useful to others. I’d love feedback if there are better ways to go about it! cheers