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
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
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
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
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:
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