v2.7.0

Ruby "Safe" Navigation: When you .try! your best, but you don't succeed

November 21, 2019 β€’ β˜•οΈ 6 min read

Updated: 12/12/2019, 12:45:39 AM

Series

Bad DX: On Developer Experience, or Professional Complaints for ${this.lang}

this.lang = Ruby

Part One of One

Being a full-time software engineer basically means I'm a paid professional complainer. Well, sort of. At the very least, I'm highly opinionated on language theory, APIs, frameworks, tooling, and overall developer experience. In this series, I pretend to be an expert on things that I'm not in order to explain my displeasure or distaste of them.

I haven't touched Ruby nor Rails in nearly a year. However, this quarter, I'm working on some authentication and authorization work in our org's application SDK. Coming from JavaScript as my daily driver, my velocity has definitely taken a hit. But, that's a whole separate topic.

Instead, this post is all about how Ruby got their safe navigation operator wrong.

Photo by Chad Peltola on Unsplash

Photo by Chad Peltola on Unsplash

What is safe navigation?

Well, it could be calling an Uber after a night out drinking, but... πŸ’ƒ

Safe navigation allows you to harmlessly access potentially absent properties on an object without crashing your application.

That's why it's safe.

I think this can be best explained by simple code examples. I'll start by diving into what Ruby safe navigation does and what I don't like about it. Next, I'll jump into some useful background knowledge that will provide some extra context. I'll write in my native tongue JavaScript to demonstrate what I believe to be the ideal case and come to use what is called optional chaining. Finally, I'll follow up with some other prior art examples in other languages, like Swift.

Ruby Safe Navigation, or ANDDOT: One Lonely Boi

The Ruby safe navigation operator is the combined characters &. and is called the "and dot" operator, or colloquially "the lonely operator". It's "lonely" because it looks like someone sitting alone on their laptop. Odd, but πŸ€·β€β™‚οΈ.

It was introduced in Ruby v2.3.0 on December 25, 2015. As that post says, it behaves like try! of active_support:

A safe navigation operator (so-called lonely operator) &., which already exists in C#, Groovy, and Swift, is introduced to ease nil handling as obj&.foo...Note that this behaves like try! of Active Support, which specially handles only nil.

Now, I want to point out something very important. One thing that I actually really like about Ruby is that it's convention to label methods that either mutate, have side-effects, can raise (or throw) errors, or all of the above with a "bang": an exclamation point !. So, if &. behaves like try!, which of those options are signaled by the !?

Welp, it throws NoMethodError πŸ˜’:

try!(*a, &b)

Same as try, but will raise a NoMethodError exception if the receiver is not nil and does not implement the tried method.

This is problematic and is the root cause of my dislike of this feature in Ruby. But, before I can explain why, let's go over a little background first.

Some Background

Let's jump into some useful background on typing and type systems, an interpreted language's runtime environment, and missing/undefined values.

Feel free to skip over this section if you're familiar with these concepts! ✌️ Here's the skip link, friend! ❀️

Typing ⌨️

Like JavaScript, Ruby is dynamically typed, which just means that the underlying types of things can, and likely will, change at runtime. If you're unfamiliar with what a type is, it is basically a way to describe what something, a variable for instance, is and how it interacts with other things. A few examples may look like the following code snippet below. Keep in mind that the language being used in this example, Ruby, is fairly unimportant.

# the number 10, as an integer
the_number_ten = 10

# my name, as a string (sequence of characters)
my_name_as_a_string = 'Cody Austin Price'

# my favorite bands, as a list of strings
my_favorite_bands = [
  'Gregory Alan Isakov',
  'W I L D',
  'First Aid Kit',
  'Joseph',
  'Sturgill Simpson'
]
A few examples of different types, written in Ruby

Generally speaking, in a typed system, types don't necessarily mix without explicitly describing how. So, you can't necessarily add an integer to a string, like 5 + 'four', because they're different types, but you can add two integers, like 5 + 10. However, in a dynamically typed language, things can get weird. Alluva sudden you can multiply lists by integers and no one bats an eye!

multiplying a list times two... 😳 wat

Multiplying a list times two... 😳 wat

Runtime πŸƒβ€β™‚οΈ

Similarly, both JavaScript and Ruby are interpreted languages, which means there is no typical "compilation" step prior to running like in C or Java. Instead, the code is, you guessed it, interpreted by a runtime environment. I'm glossing over a lot, but the rest is mostly unimportant for this post's topic. The main takeaway is that things (variables and types) aren't necessarily known until runtime.

What's wrong? Nothing.

In most (all?) programming languages, there exists a special, sentinel value for something that doesn't exist and is usually called nil or null. In Ruby, we have nil, which is oddly an object. JavaScript has a null, as well as undefined.

The story so far: In the beginning both null and undefined were created in JavaScript. This has made a lot of people very angry and been widely regarded as a bad move.

There's really not that much of a difference between the two and often times they're interpretted by developers and business logic as the same: some falsey, non-existent thing or attribute.

In Ruby, while nil does have some methods like #nil? or #to_s, it does not implement #method_missing. Don't worry if that sounds complicated, because it's just a silly way of saying that calling methods that don't exist on nil (e.g. your class' methods) will raise a NoMethodError. In JavaScript, when accessing attributes on objects that don't exist, the runtime will return undefined.

Disclaimer plus a note on JavaScript objects and Ruby hashes

I want to warn you that throughout this post, my JavaScript bias will probably show. Part of this bias is my love for how powerful a standard JavaScript object (a simple key-value pair, dictionary, or hash map) is. Namely, you can easily and dynamically describe the shape of data or an object. You can access properties through bracket access, dot notation, or even destructuring.

// an example object
const exampleObject = {
  foo: true
}

// bracket access, evaluates to true
const bracketAccess = exampleObject["foo"]

// dot notation access, evaluates to true
// this looks like method/property access on a typical OOP object πŸ€”
const dotNotation = exampleObject.foo

// destructuring access, evaluates to true
const { foo } = exampleObject
Demonstrating various ways of property access on JavaScript objects

A big tradeoff for JavaScript objects is that you don't get any type benefits out of the box, so typos can kill you.

Did I say onlick? πŸ‘…πŸ‘…πŸ‘… What I meant was onClick!

The Ruby counterpart to a JavaScript object is called a hash. Hashes are similar to JavaScript objects, but have some key differences. Most importantly, you cannot use dot notation on them to access properties. Destructuring also doesn't exist in Ruby. So you're left to use the old way for accessing properties on a hash, via brackets. Lame! Also, hashes aren't checked against a blueprint either, so they fall prey to the same problem of typos as JavaScript objects do.

Now, dot notation does exist in Ruby, but it is reserved for accessing attributes and methods on classes.

# let's make a weird hash of hashes
a_hash = { foo: { bar: { baz: 'buz' } } }

# access things in the hash using bracket notation
a_hash[:foo][:bar] #=> { baz: 'buz' }

# let's make a simple class
class Foo
  def bar
    'baz'
  end
end

# get an instance of Foo
foo = Foo.new

# access the #bar method on the foo instance via dot notation
foo.bar #=> 'baz'
Demonstrating accessing properties on hashes and objects in Ruby

Why does all this matter, Cody? πŸ€”

The differences between JavaScript objects and Ruby objects and hashes matters because they are reflected in the differences in how the two languages handle safe navigation.

Wild.

Now that we've explained that, let's look at some code.

In Ruby, how safe is safe?

Photo by Cindy Tang on Unsplash

Photo by Cindy Tang on Unsplash

So, in Ruby, how safe is safe navigation?

Spoiler alert: not very.

TODO: writeme

Safe Navigation, in JavaScript

Let's begin our exploration of safe navigation in JavaScript with a contrived example:

const conspiracy = {
  bush: {
    did: "911"
  }
}

// πŸ‘‡ this will throw, see below
const fakeNews = conspiracy.epstein.suicide
// Thrown:
// TypeError: Cannot read property 'suicide' of undefined
When accessing attributes of objects that don't exist (are undefined), it throws an error

In the above example, because Epstein didn't commit suicide, the JavaScript runtime throws an exception. There's simply no attribute called epstein, nor a child attribute called suicide in the conspiracy object. Trying to access anything on undefined throws a TypeError.

Prior to optional chaining, you would need to do something like the following to safely access properties that may or may not exist:

const conspiracy = {
  bush: {
    did: "911"
  }
}

// do a truthiness check for all nested attribute accesses
if (conspiracy.epstein && conspiracy.epstein.suicide) {
  console.log(
    "Epstein committed suicide and here's the proof: ",
    conspiracy.epstein.suicide
  )
} else {
  console.log("We all know the truth, tho πŸ˜‰")
}
Demonstrating truthiness checks with && in JavaScript for nested attribute access.

Performing truthiness checks with && for shallow objects is mostly fine. However, as you can see, even for doubly-nested attributes, it becomes cumbersome and quite verbose!

But, we have been blessed with optional chaining! πŸ˜‡

const conspiracy = {
  bush: {
    did: "911"
  }
}

// unlike before, πŸ‘‡ this DOES NOT throw! πŸ™Œ
// fakeNews will safely be `undefined`
const fakeNews = conspiracy?.epstein?.suicide
Optional chaining in JavaScript, so terse and easy!

Optional chaining allows us to write elegant and terse code that is easier to read and maintain. It allows us to safely navigate, or drill down, through a nested object shape without having to worry about or manually check for the presence of attributes!

What does this look like in other languages?

Safe navigation isn't new. There a plenty of other languages that have implemented it prior to both Ruby and JavaScript. Let's look at a couple!

Swift

In Swift, which is a typed and compiled language, we have the following:

struct Author {
  // an optional attribute, name
  var name: String?
}

struct Book {
  // another optional attribute, author
  var author: Author?
}

// tell Swift that this variable is of the type Book
let book: Book? = nil

// since the `book` variable is explicitly `nil`, this safely
// falls back to the coalesced value: "Sutter Cane"
let name = book?.author?.name ?? "Sutter Cane"

This example is kind of silly, sure, but it illustrates optional chaining in Swift. I'll go ahead and run the above code in a handy dandy Xcode playground:

Demonstrating working optional chaining in a Swift Xcode playground

Demonstrating working optional chaining in a Swift Xcode playground

The pane on the right-hand side shows the result of the expressions on lines 10 and 14. On line 10, a constant of type optional Article is initialized to nil. On line 14, we use optional chaining to try to access the deeply nested attribute name. Since we are operating on a nil value, each link along the optional chain will fail, but safely fallback to nil. At the end of the chain, we use null coalescing operator to set a default value of the string "Sutter Cane". So the final result, as the right pane shows, is that name = "Sutter Cane".

What happens when you try to access properties that don't exist?

Demonstrating what optional chaining can't do in a Swift Xcode playground

Demonstrating what optional chaining can't do in a Swift Xcode playground

In this case, the compiler will throw an error because the Author type doesn't have an attribute called birthday!

The implications of Swift being typed means that the compiler knows ahead-of-(run)time the shape of data structures and other things. This matters because the compiler can and will yell at you if you try to pull some JavaScript-like object schema. In other words, it's a lot harder in Swift to define an object or dictionary that is any? all the way down since the shape or structure of your data is known at compile time.

Oddly enough, even though Ruby is dynamically typed and interpreted, its safe navigation behaves more like Swift's as opposed to JavaScript's! I'll explain why later.

Let's take a look at one more language and its take on safe navigation.

C

Lol, jk.

Just kidding. There's no safe navigation here. In C, nothing is sacred and your safe navigation is in another castle language!


Conclusion

TODO: writeme


Cody A. Price

Written by Cody A. Price who is an Electrical and Computer Engineer turned Web Developer, proponent of Open Source Software and React.js, and an Engineer at Cerner.

Comments