Ruby Safe Navigation


Ruby safe navigation, especially in long chains, can be difficult to read and can
hide some subtle edge cases.

Consider a scenario where the following is true:

  1. Users are guaranteed to have an address
  2. Addresses are guaranteed to have a zip code

Given an optional user, we want to either get their zip code or return nil.
Using very explicit code, we might write that as:

if user
  user.address.zip
else
  nil
end

But we are Rubyists and want to write pleasant, terse code. We turn to the safe
navigation operator
and refactor our code to this one-liner:

However, behavior here is subtly different.

When comparing various syntactic sugars for conditional logic, I find it helpful
to convert them to a standardized if/else form. When doing that with the safe
navigation chain we defined, we can see that it is subtly different than the
conditional code we started with.

if user
  if user.address
    user.address.zip
  else
    nil
  end
else
  nil
end

The safe navigation version introduces extra uncertainty. Despite knowing that
we have a user present, our code is uncertain whether or not the user has an
address. This uncertainty leads to the extra nested condition.

By using syntactic sugar we’ve introduced some extra paths through out app that
didn’t exist in our original requirements. We are now faced with a choice. We
can re-write our code to better match the reality we are trying to model.

Alternatively, we may realize our original requirements are incorrect and that
we do need an extra nil check for the address. If we make this choice, we must
make sure to add tests for the new edge case we’ve discovered.

The lonely operator can act in one of two roles in a method chain:

Guarding methods that produce uncertainty. For example
uncertain1&.uncertain2&.uncertain3. When using the lonely operator in this
manner, each call in the chain is equivalent to a nested condition.

Propagating uncertainty down the chain. Once we have a value that can
possibly be nil, every method call downstream of it also needs to check for nil.
For example: uncertain&.certain1&.certain2. This is also equivalent to nested
conditionals but often, what we actually mean to express is a single
condition. This easily spills into defensive coding.

The tricky thing with &. is that, when reading the code, one can’t tell which
of these two behaviors the author intended. To make things more complex, a chain
of &. might have a mix of both behaviors. Looking back at our original problem
user&.address&.zip, one can’t tell whether or not User#address is a nullable
method or not.

When only the first item in a chain is nullable, we can use && instead of &.
to more accurately express our intention.

Beyond just using different syntax, there is also an opportunity to refactor.
The chain of non-nullable methods can safely be extracted out. This likely
results in cleaner code and also satisfies the law of demeter.

class User
  def zip
    address.zip
  end
end

With the given refactor we can now call user&.zip which is equivalent to our
original if/else condition.

So should we avoid &. altogether? No. It has some valid uses cases.

  1. When every method in the chain might produce nil nothing&.is&.certain (make
    sure you have test coverage for all the edge cases!).
  2. As the final method call user&.zip.

Long chains of &. are usually a symptom of broader issues in a codebase such
as defensive code or leaking responsibilities. In moderation, &. is a helpful
tool but make sure to consider some of the other tools in your toolbox too.



Source link

Latest articles

Related articles

Leave a reply

Please enter your comment!
Please enter your name here