Where There's a `nil`, There's a (safer) Way

Where There's a `nil`, There's a (safer) Way

We've all been burned by `NoMethodError: undefined method "___" for nil:NilClass`. One common path that leads us there is indexing arrays and hashes and attempting to chain method calls. Trying to capitalize the first string in an array? What if the list is empty? Trying to get a user profile bio? What if `profile` in `user.profile.bio` is not assigned? Let's take a step back from feature development and meditate on some Ruby fundamentals. In this talk, I'll cover some history and dangers of `nil` as a concept and how you can mitigate these vulnerabilities with Ruby's builtin `fetch` and `dig` methods for arrays and hashes.

481a1f18bdd124c255bcf9e79a281ec3?s=128

tmikeschu

May 22, 2019
Tweet

Transcript

  1. Where There's a nil, There's a Way Detroit.rb Mike Schutte

    May 22, 2019 @tmikeschu (36 slides) 1
  2. — ! — " — # $ % — &

    ' — @tmikeschu (36 slides) 2
  3. @tmikeschu (36 slides) 3

  4. Victory Conditions — Know how/when to use fetch and dig

    — Understand nothingness as a concept more generally — Feel more suspect of nil entering your Ruby programs @tmikeschu (36 slides) 4
  5. nil ! @tmikeschu (36 slides) 5

  6. Roadmap — API of fetch and dig — Maybe values

    (as a concept) — Message Passing — Type Coercion — Review @tmikeschu (36 slides) 6
  7. fetch and dig — Array — Hash @tmikeschu (36 slides)

    7
  8. Basic API lost = [4, 8, 15, 16, [23, 42]]

    lost[1] == lost.fetch(1) lost[4][1] == lost.dig(4, 1) user = { name: "John Locke", physical_attributes: { hair: :bald, height: :tall }, bio: "looking for meaning", } user[:name] == user.fetch(:name) user[:physical_attributes][:hair] == user.dig(:physical_attributes, :hair) @tmikeschu (36 slides) 8
  9. Defaults for fetch lost = [4, 8, 15, 16, [23,

    42]] # lost[10] || 0 (10 < lost.length ? lost[10] : 0) == lost.fetch(10, 0) user = { name: "John Locke", physical_attributes: { hair: :bald, height: :tall }, bio: "looking for meaning", } # user[:nickname] || "no nickname" (user.has_key?(:nickname) ? user[:nickname] : "no nickname") == user.fetch(:nickname, "no nickname") @tmikeschu (36 slides) 9
  10. Index and Key Errors for fetch lost = [4, 8,

    15, 16, [23, 42]] lost.fetch(10) # => IndexError (index 10 outside of array bounds: -5...5) user = { name: "John Locke", physical_attributes: { hair: :bald, height: :tall }, bio: "looking for meaning", } user.fetch(:height) # => KeyError (key not found: :height) @tmikeschu (36 slides) 10
  11. What about dig's defaults and errors? — ! @tmikeschu (36

    slides) 11
  12. user = { name: "John Locke", physical_attributes: { hair: :bald,

    height: :tall }, bio: "looking for meaning", } user.fetch(:height) # => KeyError (key not found: :height) user.dig(:height) # => nil user.dig(:bio, :about) # => TypeError: String does not have #dig method user.dig(:physical_attributes, :eye_color) || "no eyes" user.dig(:physical_attributes, :eye_color).to_s @tmikeschu (36 slides) 12
  13. Performance — fetch: negligible difference — https://keepthecodesimple.com/ruby-fetch/ — dig: 1.5x

    slower (still fast) — https://tiagoamaro.com.br/2016/08/27/ruby-2-3- dig/ @tmikeschu (36 slides) 13
  14. When is this useful? — Rails controllers or services parsing

    params — Getting the first item in an array — Parsing config and option hashes @tmikeschu (36 slides) 14
  15. So what? @tmikeschu (36 slides) 15

  16. Maybe find : (a -> Bool) -> List a ->

    Maybe a find predicate xs = xs |> List.filter predicate |> List.head ages : List Int ages = [10, 15, 21, 22] firstAdult = find (\age -> age >= 18) ages -- => Just 21 firstBaby = find (\age -> age < 3) ages -- => Nothing @tmikeschu (36 slides) 16
  17. find : (a -> Bool) -> List a -> Maybe

    a find predicate xs = xs -- List a |> List.filter predicate -- List a (elements that pass predicate) |> List.head -- Maybe a @tmikeschu (36 slides) 17
  18. ages : List Int ages = [10, 15, 21, 22]

    firstAdult = find (\age -> age >= 18) ages -- => Just 21 firstBaby = find (\age -> age < 3) ages -- => Nothing @tmikeschu (36 slides) 18
  19. case firstBaby of Just age -> age Nothing -> 0

    -- OR Maybe.withDefault 0 firstBaby -- => 0 Maybe.withDefault 0 firstAdult -- => 21 @tmikeschu (36 slides) 19
  20. Overcomplicated? @tmikeschu (36 slides) 20

  21. Back to Ruby... @tmikeschu (36 slides) 21

  22. @tmikeschu (36 slides) 22

  23. user.fetch(:please_dont_blow_up, "!") @tmikeschu (36 slides) 23

  24. Sending Messages I’m sorry that I long ago coined the

    term "objects" for this topic because it gets many people to focus on the lesser idea. The big idea is messaging. -- Alan Kay @tmikeschu (36 slides) 24
  25. lost = [4, 8, 15, 16, [23, 42]] lost[0] ==

    lost.[](0) # => true lost.[](0) == lost.send(:[], 0) # => true @tmikeschu (36 slides) 25
  26. 100[0] # => ? @tmikeschu (36 slides) 26

  27. 100[0] # => 0 @tmikeschu (36 slides) 27

  28. Interact with [] and {} with a more consistent pattern.

    — i.e., messages with explicit names @tmikeschu (36 slides) 28
  29. Safe Navigation Operators A test might belong to a classroom

    A classroom might belong to a teacher A teacher might belong to a school test&.classroom&.teacher @tmikeschu (36 slides) 29
  30. Meaningful Nothingness coercers = (NilClass.instance_methods - Object.methods). select { |method|

    method.to_s.match(/^to_/) }. concat([:to_s]) # => [:to_a, :to_f, :to_i, :to_h, :to_r, :to_c] coercers.map { |coercer| nil.send(coercer) } # => [[], 0.0, 0, {}, (0/1), (0+0i), ""] @tmikeschu (36 slides) 30
  31. # => [[], 0.0, 0, {}, (0/1), (0+0i), ""] 0.round

    => 0 # => 0 "".upcase # => "" [].map { |x| x + x } # => [] {}.keys # => [] 10 + 0 # => 10 "Harry Potter" + "" # => "Harry Potter" [1, 2, 3] + [] # => [1, 2, 3] { a: 1 }.merge({}) # => { a: 1 } @tmikeschu (36 slides) 31
  32. What about other classes? class Animal def self.find(id) # fetch

    animal from from data store end # ... end class MissingAnimal def name "missing animal" end # ... any other public methods on the Animal class end class GuaranteedAnimal def self.find(id) Animal.find(id) || MissingAnimal.new end end @tmikeschu (36 slides) 32
  33. Review — fetch: defaults, errors — dig — Maybe —

    Message passing — Type coercion — Null object pattern @tmikeschu (36 slides) 33
  34. What You Can Do Tomorrow — Use fetch with defaults

    — params.fetch(:username, "") — Use dig with trailing type coercion — params.dig(:user, :password).to_s @tmikeschu (36 slides) 34
  35. Thank you! @tmikeschu (36 slides) 35

  36. Resources — Nothing is Something by Sandi Metz: https:// www.youtube.com/watch?v=29MAL8pJImQ

    — Elm Maybe: https://package.elm-lang.org/packages/ elm/core/latest/Maybe — Hash#fetch: https://apidock.com/ruby/Hash/fetch — Hash#dig: https://apidock.com/ruby/Hash/dig — Array#fetch: https://apidock.com/ruby/Array/fetch @tmikeschu (36 slides) 36