Refactoring

Summarized using AI

Nil - Nothing is Easy, Is It?

Craig Buchek • May 30, 2024 • Asheville, NC

In the video titled "Nil - Nothing is Easy, Is It?" presented by Craig Buchek at the Blue Ridge Ruby 2024 conference, the speaker delves into the complexities of using nil in Ruby programming. The discussion highlights the historical perspective of nil, referencing Tony Hoare who termed the null reference as his 'billion-dollar mistake'. Though nil represents an absence of value, its ease of use can lead to significant pitfalls in programming.

Key Points Discussed:

  • Understanding nil: nil is an instance of NilClass, which is unique to Ruby. When variables are not initialized, they default to nil, which can create unexpected behavior.
  • The Pitfalls of nil: The frequent debugging required due to nil often results in substantial costs to the industry. Inconsistent usage across different contexts can lead to confusion about what nil signifies.
  • Antipatterns: Using nil as a default value or returning nil from methods without clear context is discouraged. This can lead to NoMethodError when attempting to call methods on nil.
  • Alternatives to nil: Craig suggests various strategies to replace nil or handle its presence effectively:
    • Using Default Values: Instead of using nil, apply meaningful default values that clarify intent.
    • Sentinel Values: Using designated symbols or objects as replacements for nil can make the code more understandable.
    • Guard Clauses: Checking for nil at the beginning of functions improves readability and clarity.
    • Safe Navigation Operator: Ruby's &. operator allows chaining methods safely without raising exceptions if any method in the chain returns nil.
    • Result Monad and Option Types: These patterns help indicate whether a value is present or not, improving semantic clarity.
    • Null Object Pattern: Creating specific objects that encapsulate missing behavior can help avoid nil complications.

Examples and Case Studies:

Craig illustrates the importance of understanding how nil interacts with various Ruby features, including predicate methods like nil?, and how Ruby's dynamic nature contributes to potential runtime errors, as opposed to compile-time issues that are common in statically typed languages.

Conclusions and Key Takeaways:

  • The problems associated with nil indicate a need for a more deliberate design approach in Ruby programming. Replacing nil with meanings or clearly defined substitutes can lead to better coding practices.
  • Avoiding ``nil in favor of object-oriented design principles enhances clarity and maintainability of code.
  • Utilizing Ruby's rich features, such as the safe navigation operator and encapsulation through objects, can mitigate many errors related to nil. Overall, this talk encourages programmers to introspect their usage of nil and actively seek alternatives that enhance code quality and reliability.

Nil - Nothing is Easy, Is It?
Craig Buchek • May 30, 2024 • Asheville, NC

Nil is pretty simple — it represents nothing. But that doesn't mean that it's always easy to use, or that it's always the right choice when it appears to be the obvious choice.

We'll cover several anti-patterns in the use of nil. We'll discuss *why* they're problematic, and explore some better alternatives. The refactorings that we'll look at will help to reduce errors and optimize for understanding. Writing good code might take a little longer in the short run, but it pays off in the long run.
___________________________________________________________________________________________________
Craig has been using Ruby and Rails since 2005. He enjoys writing concise readable code, especially in Ruby. He enjoys a player-coach role, helping teams improve their processes, technical practices, and automation. (He's likely looking for work.)

Giving a conference talk is Craig's way to strike up some conversations. So feel free to go up to him and “talk shop”. If you want to make small talk, ask Craig about traveling, attending concerts, beekeeping, or where he was when the pandemic hit.

Blue Ridge Ruby 2024

00:00:11.519 Craig has been using Ruby and Rails since 2005. He enjoys writing concise, readable code, especially in Ruby. He also takes pleasure in a player-coach role, helping teams improve their processes, technical practices, and automation. Currently, he is likely looking for work.
00:00:25.119 Giving a conference talk is Craig's way to strike up conversations, so feel free to approach him and talk shop during lunch or afterward. If you're looking for small talk, you might ask Craig about his travels, the concerts he's attended, his beekeeping experiences, or where he was when the pandemic hit.
00:00:52.079 Craig is here to give a talk on nothing, so let's give it up for Craig.
00:01:04.360 All right, hi, I'm Craig. I'm going to talk about nil, its pitfalls, and how to avoid them. If you want to connect with me, my Twitter handle is on the screen, along with Ruby Social. I'm rarely active on those platforms, but feel free to reach out.
00:01:10.960 I have a short URL for the slides that you can follow along with. I just committed them, so they should be up to date. If you want to look at them later, I do have presenter notes that include some links, resources, and details that I won't have time to cover today. So, hit P for presenter notes if you want to follow along.
00:01:25.640 By a show of hands, who has spent time debugging problems with nil? Alright, how much time do you think this has cost you in time and money? A lot, right? Yes, in fact, it has cost our industry billions of dollars. Tony Hoare invented the null reference in 1965. He also invented the quicksort algorithm and won the Turing Award in 1980, which is essentially the Nobel Prize in computer science.
00:01:52.240 In a 2009 interview, he referred to the null reference as my billion-dollar mistake. He stated, 'I couldn't resist the temptation to put in a null reference simply because it was so easy to implement.' So, we are stuck with that situation.
00:02:25.879 I’ll begin with the basics, showing some problems with nil. I'll discuss why we run into these issues, look at alternatives, and then wrap it up. I hope you're ready for some code because there will be a lot of it!
00:02:38.640 This is about the end of my interesting pictures in slides. It's pretty simple: use nil when no other value is valid. Like all objects in Ruby, nil has its own class called NilClass. In fact, there is only one instance of NilClass; no matter how we obtain nil, it’s always the same object.
00:02:59.159 Here we see two objects where we call nil two times, and we set it to two variables. They are indeed the same object because they have the same object ID. We could call that a singleton, even though it's not employing the Ruby Singleton module. It's just a special case in the interpreter. The number four is always going to be four. The only other objects that behave like that are true and false.
00:03:25.480 You can't even create a new instance of NilClass. So where does nil come from? Instance variables are nil if they are not defined, but local variables raise a name error if they're not defined. We only see nil unless we're explicitly setting it or obtaining it in some other way.
00:03:46.439 Note that exception messages treat local variables and method calls the same. Ruby doesn’t determine which it is until runtime, which is interesting. I often refactor to extract a variable to a method; it's much more flexible and is beneficial because it abstracts the implementation detail unless you need to dive in to examine it.
00:04:06.200 We'll often encounter nil without expecting it. If there is no value returned in an explicit return statement, it will return nil. Using a bare return like this in a guard clause is a pretty common idiom. An empty method will also return nil, similar to the default value, right? If you have an if statement without an else, it will return nil.
00:04:25.880 Implicit returns are probably the most common way to get nil. A method will always return the value of the last expression unless there is an explicit return statement. The puts method always returns nil, and the error-bang method will also return nil. In general, if a method doesn’t have an explicit return value, you should expect that it might return nil.
00:05:06.000 Now, how does nil behave? NilClass is a direct descendant of Object class; most classes in Ruby inherit from Object. The Kernel and BasicObject classes are beyond the scope of this talk, but I have detailed notes on those for anyone interested. NilClass adds only a few methods of its own. Most of these are used for coercion to other types, while there are a few for logical operations.
00:05:40.919 So, nil can be used as false or in place of false in a lot of cases. I’ll discuss that in a bit. Converting nil to various types results in an empty or zero-like value. If you run inspect, do pretty_print, or see the result in IRB, you’ll see nil printed. What you end up with is an empty string, an empty array, zero, or 0.0, as well.
00:06:01.760 Now, the nil? predicate method returns true if your value is nil and false for everything else. We can refer to this as a predicate method, which means it returns either true or false—or truthy or falsy. There are a few different ways we pronounce that. For instance, some may say 'nil a,' while I prefer the Canadian convention.
00:06:24.760 We often use nil? to check for nil, but we can also use the falsy method. The Ruby convention prefers using 'if' over 'unless,' as it typically places the positive or happy path first.
00:06:39.560 The rubinius implementation illustrates how the nil? predicate works. It's straightforward object-oriented programming: it doesn’t take much to understand. Now, Ruby treats both false and nil as falsy. When using an if statement, if nil is present, it will be treated as false. Everything else evaluates to true. It's worth noting that even zero and empty collections are considered true in Ruby, which differs from several other languages like Python, where empty arrays are treated as false.
00:07:14.440 Nil and false are not equivalent. They are only equivalent in Boolean contexts or tests. Unlike truthy and falsy, there is no nilish concept in Ruby. You might encounter the terms "truthy" and "falsy" within certain RSpec tests—expect nil to be falsy and not to be truthy.
00:08:01.760 There are additional methods such as blank? and present? provided by Rails. The blank? method returns true if the object is nil, empty, or a string containing only whitespace. Conversely, the present? method indicates the opposite by returning false when blank. These methods come from ActiveSupport; to use them, you need to require ActiveSupport.
00:08:51.040 This approach is useful for handling input, as blank input is treated the same as missing input—this is typically what we want. Rails defaults to this behavior. Therefore, if you have spaces in a blank text box, it's treated as empty.
00:09:31.160 In Ruby, instance variables default to nil, and since nil is falsy, we can use the conditional assignment operator to initialize an instance variable. This means we can set it if it hasn’t yet been set. A caveat here is that if it was previously set to false or nil explicitly, this operator would not work; it will inadvertently override that initial value.
00:10:14.920 In most cases, we don’t need to worry about that, and replacing nil with nil again is generally not a big deal. This is a common idiom in Ruby—storing a value so we only need to calculate it one time, allowing subsequent calls to return the stored value.
00:10:50.840 Using the conditional assignment operator, it returns the value based on either side. Now, nil can certainly lead to many headaches.
00:11:27.080 Using nil as a default value for optional parameters is idiomatic. In this case, we're using that operator once again. Although this might not be the best example, I often see it used. We should probably just use name = 'guest' directly in the parameter list. Passing nil is not the same as not providing an argument, though; nil isn’t special here; it’s a valid value for the parameter.
00:12:21.360 In this case, 'd' does not get assigned the default value. We often use this idiom; in this case, however, passing nil results in the same effect as not supplying the argument. This is because 'd' was nil by default, and if we passed nil, it would just use that same value.
00:12:58.920 Here is the main issue: nil only has a few methods defined. I've shown you those that nil defines itself, but I believe Object has like 75 methods or so. That sounds like a lot, but remember that Active Record objects have around 700 or 800 methods at this point.
00:13:42.600 If you try to call any other methods on nil, you will receive a NoMethodError. There is nothing particularly special about nil in this instance. The issue with NoMethodError is often that the exceptions occur nowhere near where you set the value. Then, you're stuck thinking, 'How did this end up as nil?' You need to trace your steps back.
00:14:27.760 Other languages have similar exceptions with comparable names. Note that all these languages are generally dynamically typed; statically typed languages can catch most of these issues at compile time. If you are using something like Sorbet, it can generally catch them before runtime. If you have used Crystal, which is a compiled language with similarities to Ruby, it is quite good at catching nil cases at compile time.
00:15:22.159 One of the reasons for this slide is that Java programmers often assume everyone is also familiar with Java. They may say they've encountered an NPE, and you're left wondering, 'what’s an NPE?' That refers to a null pointer exception. The interesting difference is that C will yield a segmentation fault if you attempt to dereference null since it indicates the CPU cannot reference that address.
00:16:12.280 Before diving into solutions, we should grasp what’s causing these issues. The root problem lies in the fact that nil has many meanings; it's utilized for diverse reasons. Therefore, we need to comprehend what nil signifies in each context in order to effectively replace it or fix it with a more meaningful value.
00:17:03.719 How we fix nil indeed depends on the context. I mentioned unset instance variables earlier—typically, we must deal with that situation when we encounter nil. A lot of times, nil is used to indicate emptiness, and I've got an easy solution for that scenario, which is usually pretty accessible.
00:17:54.520 If nil represents a missing value, we might want to replace it with an empty string or an empty array. If you’re treating it as unsupported functionality, you should consider using a NotImplementedError instead of nil, depending on how you want to handle it. A sentinel value is a special marker that indicates something specific. Going back to the Y2K era, for example, the value '99' typically represented an invalid date—things get a bit weird there.
00:18:46.720 If you use nil as a sentinel value, you ought to carefully select an alternative. Instead of nil, consider using a symbol that conveys the meaning, as it simplifies downstream processing by making it more explicit.
00:19:43.200 If nil is used as a default value, just use the actual default value it should be. Ruby makes heavy use of duck typing—if it walks like a duck and quacks like a duck, it’s likely a duck or very close. We don’t focus on what type an object is; we care about what it does.
00:20:38.640 In this object-oriented programming paradigm, we seldom ask about the type of an object; that’s typically a code smell. Instead of working with types, we should concentrate on the operations we can perform on them. This means that Ruby is highly dynamic and much of the action unfolds at runtime, leading to numerous runtime errors instead of compile-time issues.
00:21:21.000 The crux here is that any value you hold can potentially be nil. Hence, we must do everything in our power to prevent issues arising from nil.
00:22:09.279 For example, I might have an animal variable that could be a dog, a cat, or nil. Consequently, I don't know if animal.speak will return 'woof,' 'meow,’ or raise an exception. This brings up a significant concern, right?
00:22:55.560 So how do we resolve these issues? More importantly, how do we tackle them without introducing others? There are two fundamental strategies: we can either handle nil or replace it. Let's start with handling it. You can’t always eliminate nil so easily. Begin with a nil check before calling the method—if it's nil, we do one thing, otherwise, another.
00:23:50.200 The first example here is more explicit, making it clear that we're checking for nil. The second example, however, is more idiomatic, concise, and generally easier to read for most Ruby developers. The community standards often favor using 'if’ over 'unless,' as unless can be more challenging to read. Using unless-else is often discouraged—it can create confusing double negatives.
00:24:40.000 If you think about 'if user' as indicating that there is likely a user present, it becomes less confusing than saying 'unless there is not a user.' The second example will also raise an exception if the user is false, addressing the problem of methods that return false and nil.
00:25:37.720 Now, these idiomatic expressions have the positive case first when using if-else; they resonate well with readability and understandability. The ternary operator (question mark followed by a colon) is often a suitable choice for nil checks when the positive and negative cases are simple enough to fit on one line. My general rule for the ternary operator is that if it exceeds one line, it’s too difficult to read.
00:26:19.200 A guard clause is often the best approach for simple nil checks. Intriguingly, in this instance, we must cover the negative case first since we intend to exit early without executing any significant code. The community lacks a strong preference between the first two options because neither employs a double negative, making both relatively straightforward.
00:26:56.042 The third option is appropriate when raising an exception is the suitable response, but please note that nil checks can eventually lead to myriad bugs. It's easy to overlook edge cases and introduce extensive error handling code that clouds the main logic of your happy path.
00:27:48.080 However, there is a more effective way to manage nil problems—just don’t employ the rescue string unless there is a specific exception you seek possibility of failing. This is not a viable solution as it hides bugs. Ruby 2.3 introduced the safe navigation operator, denoted by a question mark followed by a period.
00:28:08.720 The safe navigation operator is also called the lonely operator, which metaphorically represents someone standing alone, searching for someone to converse with. My memory trick is to recall 'period' ampersand for distinguishing between the two, while recognizing that JavaScript uses a question mark followed by a period.
00:28:51.000 This lonely operator effectively substitutes a chain of calls in a method without needinglessly calling everything down the list. Ensure you include the safe navigation operator on each link, as it will skip a single call—but not the entire chain.
00:29:38.720 Rails' Active Support had the try method prior to that introduction, which behaves somewhat differently. There's also a Try Bang method that functions similarly. However, try itself doesn’t check for nil. It can handle NoMethodError even when it isn’t due to nil.
00:30:18.960 So, if the receiver is false, the safe navigation operator will return false as its result. Thus, this may yield a false value or nil, depending on the situation. Here, in an example, we provide a default value—if the final evaluation resolves to nil, then on the last line, we can replace it with our default value.
00:31:04.279 Now, let's look at ways to replace nil. In cases where we utilize nil as a default value, we could substitute this with any other value. I've seen the suggestion of a special symbol for this purpose, originating from O'B; this approach might be advantageous.
00:31:43.759 In situations where an error arises if you attempt to call something on a value that's not provided, you’ll know it's missing. This could lead you to search your code for instances of 'not provided.' Alternatively, consider returning an array rather than nil since it often serves our needs.
00:32:26.160 In many cases, this is precisely what we want. I learned of this philosophy through jQuery in 2007, which encourages a different mindset: rather than just seeking to find and alter a specific thing, look for all corresponding items and modify any or none as necessary.
00:33:14.160 Though you might not have control over what a method returns in some cases, in Rails I prefer to use array wrap to ensure we always have an array. This process converts nil seamlessly into an empty array. If you're already dealing with an array, it simply passes that array without altering it. If it's a single value, that gets wrapped in an array, thereby allowing you to treat it as a collection.
00:34:15.720 Ruby does offer an array method at the top level. I choose to use array wrap for clarity, as it resembles an initializer—it sidesteps confusion when transitioning from other languages such as Python. Additionally, Ruby's array method inhabits a separate namespace from the Array class, which can be perplexing.
00:35:01.880 If you encounter an array containing nil values, where you need to process each element, employ the compact method on the array, eliminating any nil instances. Starting with Ruby 3.1, compact is also available for Enumerable, so it functions on virtually any collection type.
00:35:47.940 Furthermore, Ruby 2.3 introduced the dig method, which simplifies interaction with nested arrays and hashes. Two lines of functional code can yield the same result, but with dig, an exception is not raised if nil occurs anywhere along the chain. It simply returns nil when this happens.
00:36:30.480 Sometimes we want to encapsulate a result within some form of object to clarify if the outcome was a success or failure. This leads us to the Result Monad—a gem called resad which allows you to handle such cases effectively. In our example, if we achieve success, we can retrieve the value of the result.
00:37:09.360 However, if there’s a failure, we can access the error message but not the successful value. In exceptional cases, we don’t wish to propagate that exception. Instead, we can indicate that it’s a failure with a meaningful error message, which can help avoid confusing situations.
00:37:58.000 Result types can simplify handling numerous operations that could fail at any point, allowing us to manage those uncertainties elegantly. In doing so, we factor in various methods to return and handle potential errors so that chaining tasks can still be efficient.
00:38:40.480 The option type can also serve to indicate whether it might hold a value or not. This practice results in clearer semantics since it distinctly indicates the presence or absence of a value. This pattern is prominent in many functional programming languages, allowing one to deal effectively with potentially missing values.
00:39:19.759 Now, let’s examine the null object pattern, which is another effective solution. This particular pattern utilizes object-oriented design principles to create an object that responds to the same method calls as other objects belonging to its associated class.
00:40:06.720 Using this approach allows us to minimize the presence of nil while providing default or degenerate behaviors through a dedicated null object. This methodology encapsulates the same API as the original object, yet comprises distinct implementations.
00:41:03.040 It's preferable that functions return a null object rather than allowing the caller to receive nil directly, thereby preventing them from having to handle nil. By eliminating nils and focusing on proper encapsulation, we can greatly enhance usability.
00:41:43.360 We then utilize polymorphism to handle these special cases by creating new classes that respond to common methods from the original class. The null object pattern is essentially a special case pattern applied specifically to the absence of objects—as opposed to treating it simply as nil.
00:42:31.679 However, it’s important to distinguish what constitutes a null object versus having a dummy object. Dummy objects are exceedingly useful while testing since we may not want to interact with the actual object that could perform a database call or external API request.
00:43:25.760 This strategy leverages the same interface, enabling testing without the resource concerns of dependencies. It can be a defining moment for understanding that object-oriented programming prioritizes specialization.
00:44:16.960 One potential pitfall programmers frequently encounter is reaching for basic language primitives when there are better alternatives using language features. For instance, utilizing floating point numbers to represent money is not the optimal approach; rather, you should opt for something finer.
00:44:58.640 Moreover, using strings to represent URLs can lead to parsing issues down the line. In Ruby, harnessing a URL object can help streamline operations since it helps handle all intricacies of the URL, like protocol and path, thus simplifying your workflow.
00:45:37.110 Conclusively, objects enable us to enforce consistent APIs. Adding further methods can establish constraints and validations that contribute towards better-designed code. While everything is an object in Ruby, I'd encourage you to strive for more rich, explicit objects that articulate what they should be doing.
00:46:30.359 One final thought is that there are many more nuances with nil than one might expect. It is ordinarily not the best choice. Generally speaking, replacing nil is better than merely addressing it. Object-oriented programming supports encapsulation and polymorphism.
00:47:09.520 Finally, if we lean into object-oriented design and utilize polymorphism effectively, it provides powerful tools for engaging with Ruby.
00:47:45.639 As I contemplated other billion-dollar mistakes in programming history, one pervasive theme stood out: the implicit coercion of values into Booleans, similar to what I’ve seen in Python. For instance, zero and empty arrays can unexpectedly evaluate as false, leading to numerous headaches. That might rank second in my list of programming language design blunders.
00:48:32.859 We also have SQL injection! If you fail to sanitize inputs, it can cost you tremendously. In the case of low-level string libraries, not checking buffer sizes can create vulnerabilities, like allowing strings of any size and possibly leading to catastrophic buffer overflows.
00:49:19.920 Bad cryptographic practices, such as failing to properly encrypt passwords or relying on fast hashing algorithms, are notorious in our industry. Passwords should be protected via slow hashing techniques to hinder mass attacks.
00:50:02.960 Moreover, issues often arise surrounding time management due to time zone complicacies—setting users up for headaches every four years! If you're interested in gaining deeper insights into the null object pattern, I recommend watching Sy's talk titled 'Nothing to Something'. She explores this concept extensively.
00:50:46.160 O'B has his own talk and book that cover the null object pattern within broader contexts, too. Lastly, I’d point you toward David Copeland, who addresses eliminating branching and attributes in Ruby code—for better design overall.
00:51:31.639 I found several excellent resources regarding design patterns on the Source Making site; it’s a great resource for both refactoring and identifying anti-patterns.
00:52:16.320 Currently, I'm wrapping up a short-term contract and seeking my next great opportunity. You can easily find me online. Thank you all for coming!
Explore all talks recorded at Blue Ridge Ruby 2024
+8