Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Humble Hash

Ariel Caplan
November 17, 2020

The Humble Hash

Hashes seem simple. Set a key to a corresponding value, retrieve the value by key. What else is there to know?

A whole lot, it turns out! Ruby makes several surprising choices about how hashes work, which turn hashes into dynamic powerhouses of functionality.

We'll dive into how hashes work, understand how they form the groundwork for some extremely useful standard library utilities, and learn patterns to leverage the unparalleled versatility of the humble hash to write concise, performant, beautiful code.

Ariel Caplan

November 17, 2020
Tweet

More Decks by Ariel Caplan

Other Decks in Technology

Transcript

  1. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • • Ruby

    programmer since 2014 • Backend Developer at • Find me online: @amcaplan • https://amcaplan.ninja Hi! I’m Ariel Caplan.
  2. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • • Performant

    key-value pair store • Keys and values can be any Ruby object • Literal syntax: • Explicit syntax: • Strict fetching: • Fetching with default value: • Setting a different default value: What is a Hash? { key: "value" } hash = Hash.new hash[:key] = "value" hash[:key] #=> "value" hash[:non_key] #=> nil hash.fetch(:key) hash = Hash.new(4) hash[:non_key] #=> 4
  3. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Set require

    'set' set = Set.new([1,2,3]) #=> #<Set: {1, 2, 3}> set.include?(3) #=> true set.include?(4) #=> false set.add(4) #=> #<Set: {1, 2, 3, 4}> set.add(3) #=> #<Set: {1, 2, 3, 4}>
  4. def add(o) @hash[o] = true self end def merge #

    ... do_with_enum(enum) { |o| add(o) } # ... end class Set def initialize(enum = nil, &block) @hash ||= Hash.new(false) # some other stuff merge(enum) end RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Set def include?(o) @hash[o] end end
  5. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Disclaimers #

    disclaimer { key: "network", value: "tier_a", text: "To receive the lowest out-of-pocket costs, please choose a provider in the Tier A network." } # provider metadata { specialty: "periodontist", network: "tier_a" }
  6. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Disclaimers #

    500 x 300 = 150,000 operations # once per disclaimer - 300 disclaimers disclaimers.select { |disclaimer| # once per provider - 500 providers providers.any? { |provider| provider.metadata[disclaimer[:key]] == disclaimer[:value] } }.map { |disclaimer| disclaimer[:text] }
  7. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Disclaimers #

    500 + 300 = 800 operations # once per provider - 500 providers provider_data = Set.new providers.each do |provider| provider.metadata.each do |key, value| provider_data << { key: key, value: value } end end # once per disclaimer - 300 disclaimers disclaimers.select { |disclaimer| data = { key: disclaimer[:key], value: disclaimer[:value] } provider_data.include?(data) }.map { |disclaimer| disclaimer[:text] }
  8. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • ! •

    String • Symbol • Numeric • !1.0.eql?(1) • Array • Hash " • Anything for which equality isn’t obvious Hash Key Warning! Containing valid types {
  9. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • • 1

    object that will always be returned • Doesn’t set a key-value pair • Set at hash initialization: • Set after hash initialization Defaults Deep Dive: Default Value Hash.new(4) hash = {} hash.default = 4
  10. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • • Gotcha:

    It will always be the same object! Defaults Deep Dive: Default Value hash = Hash.new("abc") str = hash[:key] str << "d" hash[:another_key] #=> "abcd"
  11. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • • Proc

    that will be run when a key is not found • Can set a key-value pair, if you want • Set at hash initialization: • Set after hash initialization Defaults Deep Dive: Default Proc hash = Hash.new { |h, k| h[k] = 4 } hash = {} hash.default_proc = ->(h, k) { h[k] = 4 }
  12. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Default Proc

    Can Reference Itself! default_proc = ->(h, k) { h[k] = Hash.new(&default_proc) } hash = Hash.new(&default_proc) hash[:a][:b][:c] = 4 hash #=> {:a=>{:b=>{:c=>4}}}
  13. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Fibonacci Sequence

    Fn = Fn-2 + Fn-1 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55…
  14. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Fibonacci Sequence:

    Iterative def fib(n) i, j = 0, 1 counter = 0 while counter < n i, j = j, i + j counter += 1 end i end
  15. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Fibonacci Sequence:

    Recursive def fib(n) case n when 0, 1 n else fib(n - 2) + fib(n - 1) end end
  16. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Fibonacci Sequence:

    Default Proc hash = Hash.new { |h, k| h[k] = h[k-2] + h[k-1] } hash[0] = 0 hash[1] = 1
  17. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Fibonacci Performance

    fib(4) fib(2) fib(1) fib(0) fib(3) fib(2) fib(1) fib(1) fib(0)
  18. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Fibonacci Performance

    hash[4] hash[2] hash[1] hash[0] hash[1] hash[3] hash[2]
  19. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Fibonacci Performance

    hash[4] hash[2] hash[1] hash[0] hash[1] hash[3] hash[2] fib(4) fib(2) fib(1) fib(0) fib(3) fib(2) fib(1) fib(1) fib(0) 9 calls 7 calls
  20. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Fibonacci Performance

    hash[4] hash[0] hash[1] hash[3] hash[2] fib(4) fib(2) fib(1) fib(0) fib(3) fib(2) fib(1) fib(1) fib(0) fib(5) fib(1) fib(3) fib(2) fib(1) fib(0) hash[5] hash[1] hash[3] hash[2] 15 calls 9 calls
  21. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Fibonacci Performance

    hash[4] hash[0] hash[1] hash[3] hash[2] fib(4) fib(2) fib(1) fib(0) fib(3) fib(2) fib(1) fib(1) fib(0) fib(5) fib(1) fib(3) fib(2) fib(1) fib(0) hash[5] hash[1] hash[3] hash[2] 25 calls 11 calls fib(6) fib(4) fib(2) fib(1) fib(0) fib(3) fib(2) fib(1) fib(1) fib(0) hash[6] hash[4]
  22. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Conway’s Game

    of Life def life_func(x, y, step) if step == 0 INITIAL_STATES[[x, y]] || 0 else neighbor_diffs = [[-1, -1], [-1, 0], [-1, 1], [ 0, -1], [ 0, 1], [ 1, -1], [ 1, 0], [ 1, 1]] live_neighbors = neighbor_diffs.sum { |x_diff, y_diff| life_func((x + x_diff) % WIDTH, (y + y_diff) % HEIGHT, step - 1) } if life_func(x, y, step - 1) == 1 [2, 3].include?(live_neighbors) ? 1 : 0 else live_neighbors == 3 ? 1 : 0 end end end
  23. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Conway’s Game

    of Life life_hash = Hash.new do |h, (x, y, step)| h[[x, y, step]] = if step == 0 INITIAL_STATES[[x, y]] || 0 else neighbor_diffs = [[-1, -1], [-1, 0], [-1, 1], [ 0, -1], [ 0, 1], [ 1, -1], [ 1, 0], [ 1, 1]] live_neighbors = neighbor_diffs.sum { |x_diff, y_diff| h[[(x + x_diff) % WIDTH, (y + y_diff) % HEIGHT, step - 1]] } if h[[x, y, step - 1]] == 1 [2, 3].include?(live_neighbors) ? 1 : 0 else live_neighbors == 3 ? 1 : 0 end end end
  24. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Conway’s Game

    of Life life_hash = Hash.new do |h, (x, y, step)| h[[x, y, step]] = if step == 0 INITIAL_STATES[[x, y]] || 0 else neighbor_diffs = [[-1, -1], [-1, 0], [-1, 1], [ 0, -1], [ 0, 1], [ 1, -1], [ 1, 0], [ 1, 1]] live_neighbors = neighbor_diffs.sum { |x_diff, y_diff| h[[(x + x_diff) % WIDTH, (y + y_diff) % HEIGHT, step - 1]] } if h[[x, y, step - 1]] == 1 [2, 3].include?(live_neighbors) ? 1 : 0 else live_neighbors == 3 ? 1 : 0 end end end def life_func(x, y, step) if step == 0 INITIAL_STATES[[x, y]] || 0 else neighbor_diffs = [[-1, -1], [-1, 0], [-1, 1], [ 0, -1], [ 0, 1], [ 1, -1], [ 1, 0], [ 1, 1]] live_neighbors = neighbor_diffs.sum { |x_diff, y_diff| life_func((x + x_diff) % WIDTH, (y + y_diff) % HEIGHT, step - 1) } if life_func(x, y, step - 1) == 1 [2, 3].include?(live_neighbors) ? 1 : 0 else live_neighbors == 3 ? 1 : 0 end end end
  25. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev require

    'abbrev' Abbrev.abbrev(["conf", "code"]) #=> {"conf"=>"conf", "con"=>"conf", "code"=>"code", "cod"=>"code"}
  26. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev def

    abbrev(words, pattern = nil) table = {} seen = Hash.new(0) if pattern.is_a?(String) pattern = /\A#{Regexp.quote(pattern)}/ # regard as a prefix end words.each do |word| next if word.empty? word.size.downto(1) { |len| abbrev = word[0...len] next if pattern && pattern !~ abbrev case seen[abbrev] += 1 when 1 table[abbrev] = word when 2 table.delete(abbrev) else break end } end words.each do |word| next if pattern && pattern !~ word table[word] = word end table end
  27. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev Seen

    Table conf => conf conf 1 con => conf
  28. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev Seen

    Table conf => conf conf 1 con con 1 => conf => conf
  29. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev Seen

    Table conf conf 1 con con 1 co co 1 c c 1 => conf => conf => conf => conf code => code
  30. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev Seen

    Table conf conf 1 con con 1 co co 1 c c 1 => conf => conf => conf => conf code => code code 1 => code cod cod 1 => code co
  31. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev Seen

    Table conf conf 1 con con 1 co co 1 c c 1 => conf => conf => conf => conf code => code code 1 => code cod cod 1 => code co 2
  32. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev Seen

    Table conf conf 1 con con 1 co 1 c c 1 => conf => conf => conf code => code code 1 => code cod cod 1 => code 2
  33. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev Seen

    Table conf conf 1 con con 1 co 1 c c 1 => conf => conf => conf code => code code 1 => code cod cod 1 => code 2 c
  34. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev Seen

    Table conf conf 1 con con 1 co 1 c 1 => conf => conf code => code code 1 => code cod cod 1 => code 2 2
  35. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev def

    abbrev(words) table = {} seen = Hash.new(0) words.each do |word| word.size.downto(1) { |len| abbrev = word[0...len] case seen[abbrev] += 1 when 1 table[abbrev] = word when 2 table.delete(abbrev) else break end } end table end Default Value Return Value Add Abbreviations Seen Once Remove Ambiguous Abbreviations Never set seen[abbrev] to 0! Iterate over each word portion
  36. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev-like Default

    Value totals = Hash.new(0) items = ["a", "a", "b", "c", "c", "c", "d", "b"] items.each { |item| totals[item] += 1 } totals #=> {"a"=>2, "b"=>2, "c"=>3, "d"=>1} totals["e"] #=> 0
  37. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Real-Life Defaults!

    totals = Hash.new(0) # { "sum" => 1000 } by_company = Hash.new { |h,k| h[k] = Hash.new(0) } # { "a_company" => { "sum" => 100 }} totals["unset_key"] #=> 0 by_company["some_company"]["sum"] #=> 0
  38. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Real-Life Defaults!

    def some_expensive_calculation @some_expensive_calculation ||= _do_the_expensive_calculation end
  39. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Real-Life Defaults!

    def some_expensive_calculation_a @some_expensive_calculation_a ||= _do_the_expensive_calculation(:a) end def some_expensive_calculation_b @some_expensive_calculation_b ||= _do_the_expensive_calculation(:b) end def some_expensive_calculation_c @some_expensive_calculation_c ||= _do_the_expensive_calculation(:c) end
  40. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Real-Life Defaults!

    def some_expensive_calculation(argument) @some_expensive_calculation ||= Hash.new do |h, k| h[k] = _do_the_expensive_calculation(k) end @some_expensive_calculation[argument] end
  41. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • •Flexibility: any

    object can be a key or value •Dynamicity and Resilience: default value/proc •Recursion-ish •Caching strategies What have we covered?