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)