layout: true
Blue Ridge Ruby 2024
@CraigBuchek
--- class: title, middle, center # Nothing Is Easy, Is It? ## A Talk About Nothing (Nil) ??? * Hi, I'm Craig * I'm going to talk about `nil`, its pitfalls, and how to avoid them. * If you want to "at" me, ... * I'm on Twitter and ruby.social Mastodon * I'm rarely on them * I have a short URL for the slides in the lower right. * Follow along * Look at them later * Hit `P` to toggle presenter notes * Links to resources * Details I don't have time to cover * AUDIENCE QUESTION: Show of hands - who has spent time debugging problems with `nil`? * How much do you think that has cost you in time and money? --- class: middle, billions > I call it my **billion-dollar mistake**. > It was the invention of the **null** reference in 1965. [....] > This has led to innumerable errors, vulnerabilities, and system crashes, > which have probably caused a billion dollars of pain and damage > in the last forty years. -- Tony Hoare, inventor of null references, 2009 ??? * ANSWER: It has costed our industry **billions** of dollars! * Tony Hoare invented the null reference in 1965 * He also invented the Quicksort algorithm * He won the Turing Award in 1980 * The "Nobel Prize" of computer science * This is a quote from him from 2009 * He also said: > I couldn't resist the temptation to put in a null reference > simply because it was so easy to implement ------ * Emphasis mine. * Source: https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/ * Elided text: > At that time, I was designing the first comprehensive type system for references > in an object oriented language (ALGOL W). > My goal was to ensure that all use of references should be absolutely safe, > with checking performed automatically by the compiler. > But I couldn't resist the temptation to put in a null reference, > simply because it was so easy to implement. --- class: agenda # Agenda * Basics * Problems * Root Causes * Solutions * Conclusions ??? * I'm going to start with the basics * Show some problems with `nil` * I'll talk about **why** we run into so many problems with `nil` * Then we'll look at alternatives --- class: transition, basics # Basics ??? * On to some Ruby code! --- # Basics ~~~ ruby a = nil ~~~ ??? * Nil is pretty simple, right? * You use it when no other value is valid. --- # Basics ~~~ ruby a = nil a.class # => NilClass ~~~ ??? * Like all objects in Ruby, `nil` has a class * `NilClass` --- # Basics ~~~ ruby a = nil a.object_id # => 4 c = nil c.object_id # => 4 ~~~ > "no two objects will share an id" -- `object_id` documentation ??? * In fact, there's only 1 instance of `NilClass`. * No matter how we get `nil`, it's always the same object. ------ * So `nil` is a singleton. * But `NilClass` does **not** use the Ruby `Singleton` module. * It's a special case in the Ruby interpreter * Ruby has a few objects that always have the same `object_id`: * false: 0 * true: 2 * nil: 4 * It's technically more appropriate to say "the nil", instead of "a nil"! --- # Basics ~~~ ruby NilClass.new # => NoMethodError: undefined method `new' for NilClass:Class ~~~ ??? * You can't even create a new instance of NilClass. --- class: transition, origins # Origins ??? * Where does `nil` come from? * How do we end up with `nil`? --- # Unset Variables ~~~ ruby @undefined # => nil undefined_local # !> NameError: undefined local variable or method `undefined_local' ~~~ ??? * Instance variables are `nil` if they're not defined. * Local variables raise a `NameError` if they're not defined. * Note the exception message * Ruby treats local variables and method calls the same * It doesn't know until runtime which it is ------ * I often refactor local variables, extracting them to private methods * The caller doesn't need to know which it is * Abstracts the implementation behind a name * I don't have to think or worry about the implementation * Unless/until I need to * At least, if the method doesn't require passing an argument * I usually take that to mean that I'm missing an abstraction --- # Bare Return ~~~ ruby def bare_return return if true "never gonna happen" end bare_return # => nil ~~~ ??? * We'll often get a `nil` without expecting it * If no value is specified in a `return`, it will return `nil` * Using a bare `return` like this in a guard clause is a common idiom --- # Empty Method ~~~ ruby def empty_method end empty_method # => nil ~~~ ??? * An empty method returns `nil` --- # Implicit Else ~~~ ruby def implicit_else if true "true" end end implicit_else # => nil ~~~ ??? * If you have an `if` without an `else`, ... * It results in `nil` if the condition is false --- # Implicit Return ~~~ ruby def error! file = File.open("log", "w") file.puts "error!" end error! # => nil ~~~ ??? * Implicit returns are probably the most common way to get `nil` * A method returns the value of the last expression * Because `puts` method always returns `nil`, ... * The `error!` method here also returns `nil` * In general, if a method doesn't have an explicit return value, ... * Expect that it might return `nil`. ------ * I used `file.puts` instead of `puts` to avoid confusion between output and return value --- class: transition, behavior # Behaviors ??? * How does `nil` behave? --- # NilClass Ancestors ~~~ ruby NilClass.ancestors # => [NilClass, Object, Kernel, BasicObject] ~~~ ??? * `NilClass` is a direct descendent of the `Object` class. * Most classes in Ruby inherit from `Object`. ------ * BasicObject is the top of the Ruby object hierarchy * It has only 12 methods * Mostly just equality and message passing * BasicObject is primarily used for creating proxy objects * Like the `delegate` method in Rails * Messages passed to a proxy object get passed to the target object that it's proxying * We want as many methods as possible to be forwarded to the target object of the proxy * Kernel is a module that adds ~50 more methods * Mostly for IO, system calls, and process management * Methods that seemingly take no receiver * Things like: * puts, open, readlines * sleep * require * raise, throw, catch * These are just (special) methods in Ruby! * system, fork * binding, caller, class * Info about the current context --- # Methods ~~~ ruby NilClass.instance_methods - Object.instance_methods # => [:rationalize, :to_a, :to_c, :to_f, :to_h, :to_i, :to_r, # => :&, :|, :^, :=~] ~~~ ??? * NilClass adds only a few methods of its own. * Most are for _coercion_ to other types * A few are for logical operations, so `nil` can be used as `false` ------ * That last one (`=~`) is the pattern matching operator * The docs say the pattern matching operator is there so you can do this easily: ~~~ ruby while gets =~ /re/ # ... end ~~~ * Why does Ruby have both `to_r` and `rationalize`? --- # Coercion ~~~ ruby nil.inspect # => "nil" nil.to_s # => "" nil.to_i # => 0 nil.to_a # => [] nil.to_h # => {} ~~~ ??? * Converting `nil` to various types results in an empty or 0 value. --- # Nil? ~~~ ruby nil.nil? # => true false.nil? # => false Object.new.nil? # => false 0.nil? # => false ~~~ ??? * The `nil?` predicate method returns `true` for `nil` * And `false` for everything else * I may pronounce this as: * "nil predicate method" * "nil question mark" * "nil query" * "nil, eh?" * "nil, huh?" ------ * A _predicate method_ is a method that returns true or false. * Ruby convention is to end the method name with a question mark (`?`). * Pronouncing the question mark as "eh?" is the "Canadian convention". * Other languages usually call this something like `isNull`. --- # Nil? ~~~ ruby v = something_that_might_return_nil if v.nil? # handle the nil case else # handle the non-nil case end if v # handle the non-nil case else # handle the nil case end ~~~ ??? * We often use `nil?` to check for `nil` * But we can also use the falsiness of `nil` * Ruby convention is to use the latter form * Because it puts the "positive" "happy path" case first * The only caveat is if `v` could be `false` * And you want to treat `false` differently than `nil` * But it's rare to have a method that could return `false`, `nil`, or something else * And a huge code smell --- # Nil? ~~~ ruby def xyz v = something_that_might_return_nil return if v.nil? # Normal processing end def xyz v = something_that_might_return_nil return unless v # Normal processing end ~~~ ??? * But in a guard clause, ... * Both of these are equally common styles * I normally prefer to be explicit and _intention-revealing_ * But I really don't have a preference here --- # Nil? Definition ~~~ ruby module Kernel def nil? false end end class NilClass def nil? true end end ~~~ ??? * This is how Rubinius implements the `nil?` predicate method. * Pretty straight-forward OOP ------ * Interestingly, Rubinius also has `NIL = nil`. * Other rubies do not. --- # Falsiness ~~~ ruby if nil "nil is truthy" else "nil is falsey" end # => "nil is falsey" if false "false is truthy" else "false is falsey" end # => "false is falsey" "this is true" unless nil # => "this is true" "this is true" unless false # => "this is true" ~~~ ??? * Ruby treats `false` and `nil` as false. * We say that `nil` is "falsey". --- # Falsiness ~~~ ruby "0 is truthy" if 0 # => "0 is truthy" "an empty array is truthy" if [] # => "an empty array is truthy" "an empty hash is truthy" if {} # => "an empty hash is truthy" ~~~ ??? * Everything else (other than `nil` and `false`) is treated as true. * Even 0s and empty collections * We call these "truthy" --- # Falsiness ~~~ ruby "nil is not equal to false" if nil != false # => "nil is not equal to false" !!nil == false # => true ~~~ ??? * But `nil` and `false` are not equivalent. * They are only equivalent when used in a Boolean test. * Unlike "truthy" and "falsey", there is no "nilish" concept in Ruby. --- # Falsiness ~~~ ruby expect(nil).to be_falsey expect(nil).not_to be_truthy ~~~ ??? * You might come across those terms ("truthy" and "falsey") in RSpec tests. --- # Blank/Present ~~~ ruby require 'active_support/all' v = nil "variable is blank" if v.blank? # => "variable is blank" "variable is not present" unless v.present? # => "variable is not present" ~~~ ??? * Rails adds `blank?` and `present?` methods. * `blank?` is true if the object is `nil`, empty, or a string with only whitespace * `present?` is just `!blank?` * These are **not** in standard Ruby * They're in the ActiveSupport gem * These are great for handling user input * They treat blank input the same as missing input --- # Conditional Assignment ~~~ ruby @x ||= 1 ~~~ ??? * Because Ruby instance variables default to `nil`, and `nil` is falsey, ... * We can use the "conditional assignment operator" (`||=`) to "initialize" an instance variable --- # Conditional Assignment ~~~ ruby def intermediate_result @intermediate_result ||= expensive_calculation end ~~~ ??? * This is a common idiom in Ruby to _memoize_ a value * So we only have to calculate the result once --- class: transition, problems # Problems ??? * Nil can cause us a lot of headaches. --- # Nil Parameters * Optional parameters are usually given the value `nil` ~~~ ruby def greet(name = nil) name ||= "Guest" "Hello, #{name}!" end ~~~ ??? * It's idiomatic to use `nil` as a default value for optional parameters * Here we use the "conditional assignment operator" (`||=`) * Note that we could have just used `name = "Guest"` in the parameter list --- # Nil Parameters * WARNING: Passing a `nil` is not the same as not passing an argument ~~~ ruby def example_default_params(d = 1000) d.inspect end example_default_params() # => "1000" example_default_params(nil) # => "nil" ~~~ ??? * Passing `nil` is not the same as not passing an argument * `nil` isn't actually special here * It's a valid value for the parameter * So `d` does not get assigned the default value here --- # Nil Parameters * WARNING: Except ... ~~~ ruby def example_default_params(d = nil) d ||= "1000" d.inspect end example_default_params() # => "1000" example_default_params(nil) # => "1000" ~~~ ??? * However.... * We tend to use _this_ idiom a lot * In this case, * Passing `nil` **does** have the same result as not supplying the argument --- # NoMethodError * Raised when a method is called on an object that does not support it ~~~ ruby nil.to_s # => "" 1.6.ceil # => 2 nil.ceil # !> NoMethodError("undefined method `ceil' for nil") ~~~ ??? * Here's the main problem * `nil` has only a few methods defined * If you try to call any _other_ methods, ... * You get a `NoMethodError` --- # NoMethodError * Not particular to `nil` ~~~ ruby 1.6.ceil # => 2 "1.6".ceil # !> NoMethodError("undefined method `ceil' for an instance of String") ~~~ ??? * There's nothing particularly special about `nil` and NoMethodError * It happens **any** time you tell **any** object ... * to do something it doesn't know how to do * The biggest problem with NoMethodError: * Exception is usually **raised** nowhere near where the value was **set** --- # ASIDE: Other Languages * Java: NullPointerError (NPE) * Python: AttributeError * JavaScript: TypeError * C: segmentation fault ??? * Other languages have similar exceptions * With similar names * Note that all of these languages are dynamically typed * Statically-typed languages catch most of these errors at compile time ------ * QUESTION: Isn't Java statically typed? * ANSWER: Yes, but it has a special case for `null` that allows it to be assigned to any reference type * per Copilot * It also borrows a lot from C and C++ --- class: transition, root-causes # Root Causes ??? * Before we get into solutions, ... * We should understand what's causing these problems --- # Root Causes * There's no single meaning of `nil` ??? * The problem at the root of it all is that `nil` has **many** meanings --- # Multiple Meanings * Unset variable * Empty * Not found / missing * Not applicable * Not supported * Sentinel * Default value ??? * Nil is used for a lot of different reasons * We need to understanding what `nil` means in each context * Then replace it with a more meaningful value * How we fix a `nil` depends on the context --- # Multiple Meanings * Unset variable * Empty * Not found / missing * Not applicable * Not supported * Sentinel * Default value ??? * I talked about unset instance variables * We pretty much just have to deal with that one * A lot of times we use `nil` to mean "empty" * I'll show an easy fix for that * If `nil` represents a missing value * We might replace it with an empty string or empty array --- # Multiple Meanings * Unset variable * Empty * Not found / missing * Not applicable * Not supported * Sentinel * Default value ??? * We should really use `NotImplementedError` for "not supported" * A sentinel value is a special value that indicates something special * Back in the day, we'd often use "9999" to represent an invalid date * If `nil` is used as a sentinel value * We might replace it with a symbol * If `nil` a default value, we should replace it with the actual default value --- # Duck Typing ~~~ ruby class Dog def speak "Woof!" end end class Cat def speak "Meow!" end end # We have an animal, but we don't know what kind. animal = [Dog.new, Cat.new].sample animal.speak # => "Woof!" or "Meow!" ~~~ ??? * Ruby uses _duck typing_ * "If it walks like a duck and quacks like a duck, it's a duck" * We don't care what an object **is**, only what it can **do** * We rarely _ask_ about the type of an object * We just _try_ using it * This makes Ruby **very** dynamic * A **lot** happens at runtime --- # Duck Typing ~~~ ruby class Dog def speak "Woof!" end end class Cat def speak "Meow!" end end # We have an animal --- or maybe we don't. animal = [Dog.new, Cat.new, nil].sample animal.speak # => "Woof!" or "Meow!" or NoMethodError exception ~~~ ??? * Pretty much **any** value you have _could_ be `nil` * So we want to do everything we can to avoid the problems with `nil` --- class: transition, solutions # Solutions ??? * How do we solve these problems? * More importantly: How do we solve these problems without causing other problems? * There are 2 basic strategies: * Handle it * Replace it --- # Nil Check * Check for `nil` before calling methods on it ~~~ ruby user = User.find(123) unless user.nil? puts "Hello, #{user.name}!" else fail "user not found" end if user puts "Hello, #{user.name}!" else fail "user not found" end ~~~ ??? * We can't always get rid of a `nil` so easily * The 1st example is more explicit * It's more clear that we're checking for `nil` * The 2nd example is more _idiomatic_, more concise, and more readable * Community standards prefer `if` over `unless` * Because `unless` is usually harder to read and understand * We think of `if user` as "if there is a user" * As opposed to the former reading as "unless there is not a user" * The 2nd example will also raise an exception if `user` is `false` * This is rarely a problem * It's **extremely** bad practice for a method to return `nil`, `false`, or a "normal" object * Unless the object is only returned to represent "truthy" * But it is something to be aware of * Mostly arises in caching scenarios --- # Nil Check * Check for `nil` before calling methods on it ~~~ ruby user = User.find(123) if user.nil? fail "user not found" else puts "Hello, #{user.name}!" end ~~~ ~~~ ruby user = User.find(123) unless user fail "user not found" else puts "Hello, #{user.name}!" end ~~~ ??? * These are less idiomatic/readable than the previous examples * It's idiomatic to put the "positive" case first * When using an `if`/`else` statement * Because it's easier to read and understand --- # Nil Check - Ternary * Check for `nil` using the ternary operator ~~~ ruby user = User.find(123) puts user ? "Hello, #{user.name}!" : "Sorry, could not find user" ~~~ ~~~ ruby user = User.find(123) puts user ? "Hello, #{user.name}!" : fail "user not found" ~~~ ??? * Using a ternary operator is often a good choice * When the "positive" and "negative" cases are simple --- # Nil Check - Guard Clause * Check for `nil` using a guard clause ~~~ ruby user = User.find(123) return if user.nil? puts "Hello, #{user.name}!" ~~~ ~~~ ruby user = User.find(123) return unless user puts "Hello, #{user.name}!" ~~~ ~~~ ruby user = User.find(123) fail "user not found" if user.nil? puts "Hello, #{user.name}!" ~~~ ??? * A guard clause is usually the best solution ... * for a simple `nil` check * Oddly, in a guard clause, we **have** to cover the "negative" case first * Because we don't want to do anything (interesting) in that case * There's no strong community preference between the first 2 * Because there's no "double negative" like with `unless user.nil?` * Because they're both easy to read and understand * The 3rd option is appropriate if an exception is the right response * Nil checks lead to lots of bugs * It's easy to miss edge cases * They also add a lot of error-handling extra code * There's a better way --- # Rescue Nil ~~~ ruby user = User.find(123) username = company.account.users.first.name rescue "[missing]" ~~~ ??? * This looks like a nice solution! * It's NOT! * This is **not** the better way! --- class: do-not-do-this # Rescue Nil ~~~ ruby user = User.find(123) username = company.account.users.first.name rescue "[missing]" ~~~ ??? * Don't do this! * This will rescue **any** exception * Not just `NoMethodError` * You should never rescue exceptions you aren't expecting * You'll end up hiding bugs from yourself * Hours of fun! --- # Safe Navigation * Ruby 2.3 introduced the safe navigation operator `&.` ~~~ ruby account = nil account.owner.address.zip_code # => undefined method `owner' for nil (NoMethodError) account&.owner&.address&.zip_code # => nil ~~~ ??? * AKA the "lonely operator" * Matz says it looks like a person standing alone, looking around for someone to talk to. * The person is looking at the `.` operator. * JavaScript and other languages use `?.` for this operator. --- # Safe Navigation ~~~ ruby account = nil account&.owner&.address&.zip_code # => nil account && account.owner && account.owner.address && account.owner.address.zip_code # => nil ~~~ ??? * The lonely operator `&.` effectively replaces a chain of growing `&&`s ------ * https://thoughtbot.com/blog/ruby-safe-navigation --- # Safe Navigation > Note that `&.` skips only one next call, so for a longer chain it is necessary to add operator on each level ~~~ ruby account&.owner.address.zip_code # !> NoMethodError: undefined method `address' for nil:NilClass ~~~ ??? * Don't forget to use the safe navigation operator on each level of the chain * It only skips 1 call, not the whole chain ------ * Source: https://docs.ruby-lang.org/en/master/syntax/calling_methods_rdoc.html --- # Try * Before the safe navigation operator, Rails had `try` ~~~ ruby account = nil account.owner.address.zip_code # => undefined method `owner' for nil (NoMethodError) account.try(:owner).try(:address).try(:zip_code) # => nil ~~~ ??? * Rails' ActiveSupport had the `try` method before Ruby had the "safe navigation operator" * Does `responds_to?` to avoid **any** NoMethodError * Try is slightly different than the safe navigation operator * If the receiver is `false`, the safe navigation operator will return `false` * The safe navigation operator treats false as a valid value * Try will return `nil` if any of the methods raise an exception * It's a method call that uses some metaprogramming * It might return `false` instead of `nil` in some cases --- # Try ~~~ ruby Account = Data.define(:owner) Owner = Data.define(:address) Address = Data.define(:zip_code) account = Account.new(Owner.new(Address.new("90802"))) account.try(:owner).try(:address).try(:zip_code) || "00000" # => "90802" account = nil account.try(:owner).try(:address).try(:zip_code) || "00000" # => "00000" ~~~ ??? * Here's an example that provides a default value * There's also `try!` * Equivalent to the safe navigation operator * That's it for **handling** `nil` ------ * [The Safe Navigation Operator (&.) in Ruby](https://mitrev.net/ruby/2015/11/13/the-operator-in-ruby/) * [What's the difference between `try` and `&.`](https://stackoverflow.com/a/45825498/26311) --- # Sentinel Parameters * Use a sentinel value to indicate "not provided" ~~~ ruby def example_default_params(d = :not_provided) d = "1000" if d == :not_provided d.inspect end example_default_params() # => "1000" example_default_params(nil) # => "nil" ~~~ ??? * Now let's look at **replacing** `nil` * In the case where we used `nil` as a default value for a parameter, ... * We were using `nil` as a sentinel value to indicate "not provided" * We can replace the sentinel value with a different value * Here we use a symbol --- # Empty Arrays * Return an empty array instead of `nil` ~~~ ruby # Returns a (possibly empty) collection of users def find_users users = DB.find(:users) users || [] end def list_users users = find_users users.each { |user| puts user.name } end ~~~ ??? * An empty array does nothing when iterated over * Exactly what we're looking for * Consider returning arrays instead of single objects or `nil` * Or any Ruby `Enumerable` * Common pattern in Ruby * It's idiomatic * It's readable * jQuery opened my eyes to this idea in 2007 * Different way of thinking of the results ------ * jQuery was the first to popularize this pattern (in JavaScript) * The primary data type is an array-like object of DOM elements * You work with that instead of individual DOM elements --- # Array.wrap ~~~ ruby def find_users users = DB.find(:users) end def list_users users = Array.wrap(find_users) users.each { |user| puts user.name } end ~~~ ??? * You don't always have control of what a method will return * In Rails, you can use `Array.wrap` to ensure you always have an array ------ * From Rails' ActiveSupport --- # Array.wrap ~~~ ruby require 'active_support/all' Array.wrap(nil) # => [] Array.wrap([]) # => [] Array.wrap([1, 2, 3]) # => [1, 2, 3] Array.wrap(4) # => [4] ~~~ ??? * `Array.wrap` will convert `nil` to an empty array --- # Array() ~~~ ruby Array(nil) # => [] Array([]) # => [] Array([1, 2, 3]) # => [1, 2, 3] Array(4) # => [4] ~~~ ??? * Ruby's `Array()` top-level method is very similar * I prefer the explicitness of `Array.wrap` ------ * I think `Array()` looks too much like a constructor. --- # Array#compact ~~~ ruby a = [1, nil, 2, nil, 3] a.compact # => [1, 2, 3] ~~~ ??? * The `compact` method removes `nil` elements from an array * Good way to get rid of nils * Ruby 3.1 added `compact` to Enumerable --- # Dig ~~~ ruby address = params[:account][:owners][0][:address] address = params.dig(:account, :owners, 0, :address) ~~~ ??? * `dig` was added in Ruby 2.3 * It's used to navigate nested arrays and hashes * These 2 are equivalent * Except the `dig` method won't raise an exception if there's a `nil` anywhere in the chain * It will just return `nil` * TODO: Use the same examples as in the Nil Check, Safe Navigation, and Try sections * TODO: See https://mitrev.net/ruby/2015/11/13/the-operator-in-ruby/ --- # Result Type * Represents a result that is a `Success` or a `Failure` ~~~ ruby require 'resonad' extend Resonad::Mixin def divide(a, b) Success(a / b) rescue ZeroDivisionError Failure('Division by zero') end divide_10_by_2 = divide(10, 2) divide_10_by_2.success? # => true divide_10_by_2.failure? # => false divide_10_by_2.value # => 5 divide_10_by_2.error # => Resonad::NonExistentError ~~~ ??? * Using the "resonad" gem * If we have a Success, we can get the value of the result * But we can't get the error message, because there is none ------ * I chose this gem over dry-monads, because it's easier to understand --- # Result Type * Represents a result that is a `Success` or a `Failure` ~~~ ruby require 'resonad' extend Resonad::Mixin def divide(a, b) Success(a / b) rescue ZeroDivisionError Failure('Division by zero') end divide_10_by_0 = divide(10, 0) divide_10_by_0.success? # => false divide_10_by_0.failure? # => true divide_10_by_0.error # => "Division by zero" divide_10_by_0.value # => Resonad::NonExistentValue ~~~ ??? * This all seems pretty straight-forward * If we have a Failure, we can get the error message * But we can't get the value, because there is none * Failure is often used in a `rescue` clause --- # Result Type * Represents a result that is a `Success` or a `Failure` ~~~ ruby # ... divide_10_by_2.map { _1 + 1 }.value # => 6 divide_10_by_2.and_then { Success(_1 + 2) } .on_success { _1 } .on_failure { logger.warn(_1) } # => 7 # => Success(7) divide_10_by_0.and_then { Success(_1 + 2) } .on_success { _1 } .on_failure { logger.warn(_1) } # Logs "Division by zero" # => Failure("Division by zero") ~~~ ??? * In the first example, we add 1 to the value * We get 6 * In the second example, we add 2 to the value * We can use the value in an `on_success` block * Then we skip the `on_failure` block * But then return the Success object * In the `divide_by_0` example, ... * Because we have a Failure, the `on_success` block is skipped * Then we log the error in an `on_failure` block * But return the Failure object * Result objects are a good solution if: * an operation can fail in multiple different ways * a series of operations are chained together * and any of them might fail --- # Option Types * Wraps a value that can may contain a value or not ~~~ ruby class Option attr_reader :value def initialize(value) = @value = value def self.some(value) = new(value) def self.none = new(nil) def some? = !value.nil? def none? = value.nil? end name = Option.some('Alice') empty_name = Option.none puts name.value # => 'Alice' puts empty_name.none? # => true ~~~ ??? * An Option Type is similar * Instead of a success of failure, ... * It can contain a value or nothing * An option type makes it clear that you may or may not have a value * AKA Maybe * This is a common pattern in statically-typed languages * Especially in functional programming languages ------ * AKA the Option (or Maybe) monad, * A monad is a design pattern that allows chaining of operations * Think of it as a wrapper around a value * NOTE: This is a very simple implementation * There are many libraries that provide more features * Including the `dry-monads` gem --- # Null Object * Replace `nil` with an object that provides default behavior ~~~ ruby class NullUser def name "Guest" end end def find_user(_id) nil end user = find_user(123) || NullUser.new user.name # => "Guest" ~~~ ??? * The Null Object pattern is another excellent solution * The Null Object pattern uses OOP to solve the problem * Instead of `nil`, we use an object * The object responds to the same methods ... * as objects of the class it's associated with * The User class, in this case * Calling the methods on the Null Object results in "default" behavior * Or "degenerate" behavior * The null object has the **same API** as the original object * But behaves differently --- # Null Object * Replace `nil` with an object that provides default behavior ~~~ ruby class User def self.null NullUser.new end def find(id) DB.find(:users, id) || User.null end end user = User.find(123) user.name # => "Guest" ~~~ ??? * It's best if you have the _callee_ return the "null object" * Don't let the caller get a `nil` * This follows the "Make Impossible States Impossible" principle ------ * AKA "Make Impossible States Unrepresentable". * The NullUser class should probably be a singleton here. * [Make Impossible States Impossible](https://kentcdodds.com/blog/make-impossible-states-impossible) * Kent C Dodds (2018) * [Making Impossible States Impossible](https://www.youtube.com/watch?v=IcgmSRJHu_8) * video by Richard Feldman (2016) --- # Black Hole Null Object ~~~ ruby class BlackHole include Singleton def method_missing(*) self end def respond_to_missing?(*) true end def nil? true end end null_object = BlackHole.instance null_object.any_method_call.whatsoever.still_works # => #
~~~ ??? * I want to share the Black Hole Null Object pattern * I'm not sure if I can recommend this or not * Hides bugs * Potential for unexpected behavior * It's definitely interesting --- # Naught ~~~ ruby require 'naught' log = Logging.logger["my.log"] log.info "Logs to my.log" NullLog = Naught.build do |config| config.singleton config.mimic example: log end null_log = NullLog.instance null_log.info "Logs to the void" ~~~ ??? * Avdi Grimm created a gem called Naught * It supports a lot of options * Including the Black Hole Null * It's really a toolkit to build Null Objects * Tons of options * Great traceability ------ * Source: https://github.com/avdi/naught --- # Special Case * Null Object pattern < Special Case pattern * Degenerate cases * Dummy objects * Edge cases * Special values ??? * Null Object pattern is a special case of the Special Case pattern * The less than (`<`) is used to notate that the Null Object pattern is a subclass of the Special Case pattern * Used to represent the absence of some object * The line can get a little fuzzy * Is a NullLogger really a Null Object, or just a Special Case? * Special Case classes handle other special conditions and edge cases * They can be used to handle special cases that don't fit the Null Object pattern * AKA Exceptional Case ------ * The Special Case pattern can be considered to be a refinement of the Strategy pattern --- # Special Case ~~~ ruby class User # ... def can_access?(resource) resource.owner == self end end class Admin < User def can_access?(resource) true end end ~~~ ??? * Special cases can be really simple * The key is that we use polymorphism to handle the special case * Create a new class that responds to the same methods as the original class * Could be a subclass * Could be a completely different class * Implementing the same (pertinent) methods --- class: transition, conclusion # Conclusions --- # Primitive Obsession * We overuse primitive types * It's often be better to use a more specialized type ??? * Programmers have a tendency to reach for language primitives * Even when there is a better solution using the language features * Example: Using a floating point number to represent money * Example: Using a string to represent a URL * Objects allow us to have a richer, more consistent API * More methods * Constraints * Validations ------ * In Ruby, we're more likely to abuse strings in this way. * That's often referred to as "stringly typed". * A play on "strongly typed" languages. * But our examples overuse `nil` a lot. * Technically, everything in Ruby is an object. * I'm talking about richer custom-defined objects here. * Except blocks * Everything that _you can assign to a variable_ is an object --- # Special Case ~~~ ruby module Kernel def nil? false end end class NilClass def nil? true end end ~~~ ??? * Remember this from Rubinius? * This is also an example of the Special Case pattern * Barely even feels like a "pattern" * It's just using polymorphic classes * Classes that respond to the same methods * Ie. they have the same interface * Specialization is really what OOP is all about * Eliminating conditionals is a good way to get there * Or isolate the conditionals in one place * Often a factory method --- # Conclusions * There's a lot of nuance regarding `nil` * It isn't always so simple * Nil is often **not** the best choice * It's usually better to replace it (with a more specialized type) * Rather than handling it ??? * There's more to `nil` than meets the eye. --- # Conclusions * OOP polymorphism **FTW!** * Learn how to use your tools **well** ??? * OOP is our friend * OOP is primarily about encapsulation and polymorphism * Use polymorphism to treat all objects the same way * Without having to check types * Replace conditionals with polymorphism * Think in terms of the messages you can send to objects * Allow the objects to do their job * Lean into OOP and polymorphism! * Powerful tools for using Ruby well --- # Other Billion-Dollar Mistakes * Implicit coercion to Booleans * C string libraries * Major cause of buffer overflows * SQL injection * Failure to sanitize input * Bad cryptographic hygiene * Time zones ??? * Several other programming mistakes have likely crossed the billion dollar mark. * Implicit coercion to Booleans is the other one that's a language design mistake. * In many languages, 0 and empty arrays are "falsey". * For example, in Python, you'll often be confused when you get an empty list when you expected a list of lists. * Usually breaks the principle of least surprise. * Time zones, am I right? :D --- # Resources * Sandi Metz: [Nothing is Something](https://www.youtube.com/watch?v=OMPfEXIlTVE) * Avdi Grimm: [Confident Code](https://youtu.be/T8J0j2xJFgQ?si=t9LgTxijJB7sLBbC) * Avdi Grimm: [Confident Ruby](https://store.avdi.codes/l/rrWapR) * David Copeland: [Eliminating branching, nil, and attributes](https://youtu.be/inU7MEtI51g?si=hHFjRHO_Yrany-eO) * Source Making: [Null Object](https://sourcemaking.com/design_patterns/null_object) (more on individual pages' presenter notes) ??? * If you want to dig into the Null Object pattern more, ... * Sandi's talk is amazing * Avdi's talk and book are great * Shows how to use the Null Object pattern within a larger context * David Copland has a talk that starts by eliminating all branching * Including `nil` checks * There are a couple really good sites on Design Patterns * Source Making is one of the best * It's also great for info on refactoring and anti-patterns --- # Open to Opportunities * Principal (or Staff) Software Engineer * Ruby: 18 years * Devops: 11 years * Network Security: 7 years * Resume: https://resume.craigbuchek.com ??? * I'm currently wrapping up a short-term contract * Looking for my next great opportunity --- class: thanks, image-only # Thanks ![Thank you](images/Thank-you-word-cloud.jpg) ??? * Thank YOU for coming. ------ * Avdi and Sandi for leading the way * Members of LA Ruby meetup for feedback on a preview of the talk * Thanks to Blue Ridge Ruby for selecting my talk. --- # Feedback * GitHub: [booch][github] * Mastadon: [@CraigBuchek@ruby.social][mastadon] * Twitter: [@CraigBuchek][twitter] * Email: craig.buchek@gmail.com * LinkedIn: https://www.linkedin.com/in/craigbuchek * Resume: https://resume.craigbuchek.com * Slides: http://craigbuchek.com/nil * Source: https://github.com/booch/presentations ??? * One reason I give talks at conferences is to start a conversation. * Please don't hesitate to come talk to me. ------ * I used a tool called [Remark][remark] to create and show these slides. [github]: https://github.com/booch [mastadon]: https://ruby.social/@CraigBuchek [twitter]: https://twitter.com/CraigBuchek [remark]: http://remarkjs.com/ --- # Image Credits * https://www.flickr.com/photos/83633410@N07/7658225516 * https://commons.wikimedia.org/wiki/File:Chemical_solutions.jpg * https://freerangestock.com/sample/125787/new-born-baby-.jpg * https://www.flickr.com/photos/38071164@N00/211042959/ *