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_positivedoes 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):
-
numberis nil -
numberis notnilandnumber.inner_valueisnil -
numberis notnilandnumber.inner_valueis 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
nilobjects andifconditions. - we reviewed Ruby's Lonely operator and JavaScript's Optional chaining.
- and finally we have learned Crystal's
Object.trymethod!!
Hope you enjoyed it! ð
Top comments (0)