When writing code that needs to handle nilable objects the resulting code can be sometimes verbose and difficult to read/follow.
In this post we will start with an example (presenting the problem), followed by a (not ideal) solution and finally present different ways that different languages (especially Crystal) use to handle nilable objects.
Let's use the following Crystal code to illustrate:
class IntWrapper
getter inner_value : Int32?
def initialize(@inner_value = nil)
end
end
# returns an IntWrapper only if parameter is positive else it returns `nil`
def create_if_positive(n : Int32): IntWrapper?
IntWrapper.new(n) if n > 0
# else it will return `nil`
end
number = create_if_positive(40)
puts number.inner_value + 2
Notes:
- The method
create_if_positive
does not make much sense but for the purpose of the example. - This is not an example of good design (although maybe it's an example of bad design) ð
The compiler will return:
$ Error: undefined method 'inner_value' for Nil (compile-time type is (IntWrapper | Nil))
And the compiler is right: create_if_positive
may return nil
as we specified in the return type IntWrapper?
So we need to check if the returned object is nil
:
...
if number
puts number.inner_value + 2
else
puts "nil branch"
end
And that's it! ... wait ... what? ... the compiler is saying:
$ Error: undefined method '+' for Nil (compile-time type is (Int32 | Nil))
oooh right! Now number.inner_value
can be also nil
(remember getter inner_value : Int32?
)
Let's fix it:
...
if !number.nil? && !number.inner_value.nil?
puts number.inner_value + 2
else
puts "nil branch"
end
Now it's fixed ... wait ...
Error: undefined method '+' for Nil (compile-time type is (Int32 | Nil))
And also, we need to tell the compiler that number.inner_value
cannot be nil
inside the if
branch because we already check on that. For that we use the Object#not_nil! method:
...
if !number.inner_value? && !number.inner_value.nil?
puts number.inner_value.not_nil! + 2
else
puts "nil branch"
end
Well, it's working but I would really want to write the same thing in a more concise and clear way.
For example, I like the following idiom when dealing with nil
and if
condition:
if a = obj # define `a` only if `obj` is not `nil`
puts a.inspect # => the compiler knows that `a` is not `nil`!
end
So let's try to go in that direction. Maybe something like this:
if number != nil && (value = number.not_nil!.inner_value)
puts value + 2
else
puts "nil branch"
end
Again, it's working but I think we can do better (I still don't like telling the compiler that number
is not nil
).
What can we do? ðĪ
Safe Navigation âĩïļ
At this point Ruby's Lonely Operator (aka Safe Navigation Operator) came to my mind:
class IntWrapper
@inner_value = nil
def initialize(inner_value = nil)
@inner_value = inner_value
end
def inner_value
@inner_value
end
end
# 1. `number` is `nil` (using if)
number = nil
if number && number.inner_value # using if
puts number.inner_value + 2
else
puts "nil branch"
end
# 2. `number` is `nil`
number = nil
value = number&.inner_value
puts value + 2 unless value.nil? # nothing is printed
# 3. `number` is not `nil`. `inner_value` is `nil`
number = IntWrapper.new()
value = number&.inner_value
puts value + 2 unless value.nil? # nothing is printed
# 4. `number` is not `nil`. `inner_value` is not `nil`
number = IntWrapper.new(40)
value = number&.inner_value
puts value + 2 unless value.nil? # => "42"
Also JavaScript's Optional chaining:
// 0. Error
let number = null;
let value = number.inner_value; // Error: Cannot read properties of null (reading 'inner_value')
// 1. number is null
let number = null
let value = number?.inner_value;
console.log(value ? value + 2 : "value is null"); // > "value is null"
// 2. `number` is not `null`. `inner_value` is `null`
let number = {
inner_value: null
}
let value = number?.inner_value;
console.log(value ? value + 2 : "value is null"); // > "value is null"
// 3. `number` is not `null`. `inner_value` is not `null`
let number = {
inner_value: 40
}
let value = number?.inner_value;
console.log(value ? value + 2 : "value is null"); // > 42
Do we have some special syntax in Crystal?
The answer is no ð
But don't despair! There is something really cool. It's not syntax but a method: Object#try
So we don't need to learn some new syntax but just know how this method works. It's super simple:
Yields self. Nil overrides this method and doesn't yield.
This means that:
nil.try { |obj|
# this block does not get called!
puts obj.size
}
and a "not-nil" object will yield self
meaning:
"Hello!!".try { |obj|
# the block gets called with the object itself as the parameter.
puts obj.size # => 7
}
or simpler using short one-parameter syntax (not to be confused with the previous seen Ruby's Lonely operator!ð):
puts nil.try &.size # => nil
puts "Hello!!".try &.size # => 7
So in our example we can write:
if value = number.try &.inner_value
puts value + 2
else
puts "nil branch"
end
Great! It's easy to read, right? number
is trying to number.inner_value
and if number
is not nil
then value
will be assigned with the value of inner_value
(furthermore, in the case of inner_value
being nil
then the if-guard fails ðĪð)
The complete example (3 in 1):
-
number
is nil -
number
is notnil
andnumber.inner_value
isnil
-
number
is notnil
andnumber.inner_value
is notnil
class IntWrapper
getter inner_value : Int32?
def initialize(@inner_value = nil)
end
end
def create_if_positive(n : Int32): IntWrapper?
IntWrapper.new(n) if n > 0
# else it will return `nil`
end
# 1. `number` is nil
number = create_if_positive(-1)
if value = number.try &.inner_value # the condition fails
puts value + 2
else
puts "nil branch" # => "nil branch"
end
# 2. `number` is not `nil` and `number.inner_value` is `nil`
number = IntWrapper.new # `inner_value` will be `nil`
if value = number.try &.inner_value # the condition fails
puts value + 2
else
puts "nil branch" # => "nil branch"
end
# 3. `number` is not `nil` and `number.inner_value` is not `nil`
number = create_if_positive(40)
if value = number.try &.inner_value
puts value + 2 # => 42
else
puts "nil branch"
end
You can play with the example in this playground
Farewell and see you later
We have reached the end of this safe navigation journey ðĪŠ. To recap:
- we have dealt with
nil
objects andif
conditions. - we reviewed Ruby's Lonely operator and JavaScript's Optional chaining.
- and finally we have learned Crystal's
Object.try
method!!
Hope you enjoyed it! ð
Top comments (0)