DEV Community ðŸ‘Đ‍ðŸ’ŧðŸ‘Ļ‍ðŸ’ŧ

Cover image for if !obj.nil?
Franciscello
Franciscello

Posted on

if !obj.nil?

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
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And that's it! ... wait ... what? ... the compiler is saying:

$ Error: undefined method '+' for Nil (compile-time type is (Int32 | Nil))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now it's fixed ... wait ...

Error: undefined method '+' for Nil (compile-time type is (Int32 | Nil))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 
} 
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

or simpler using short one-parameter syntax (not to be confused with the previous seen Ruby's Lonely operator!😉):

puts nil.try &.size # => nil
Enter fullscreen mode Exit fullscreen mode
puts "Hello!!".try &.size # => 7
Enter fullscreen mode Exit fullscreen mode

So in our example we can write:

if value = number.try &.inner_value
  puts value + 2
else
  puts "nil branch"
end
Enter fullscreen mode Exit fullscreen mode

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

  1. number is nil
  2. number is not nil and number.inner_value is nil
  3. number is not nil and number.inner_value is not nil
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
Enter fullscreen mode Exit fullscreen mode

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 and if 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)

Need a better mental model for async/await?

Check out this classic DEV post on the subject.

⭐ïļðŸŽ€ JavaScript Visualized: Promises & Async/Await

async await