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

The Humble Hash

7b5a451ee25044b9c869e3e98b79425d?s=47 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.

7b5a451ee25044b9c869e3e98b79425d?s=128

Ariel Caplan

November 17, 2020
Tweet

More Decks by Ariel Caplan

Other Decks in Technology

Transcript

  1. RUBYCONF 2020 Ariel Caplan • @amcaplan • amcaplan.ninja The Humble

    Hash
  2. None
  3. LIFE

  4. None
  5. None
  6. cell.next_state = if cell.alive? [2,3].include?(cell.live_neighbors_count) ? 1 : 0 else

    cell.live_neighbors_count == 3 ? 1 : 0 end
  7. None
  8. None
  9. None
  10. None
  11. None
  12. None
  13. None
  14. None
  15. None
  16. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN •

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

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

  19. 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
  20. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • talk[:obj_key][:set]

  21. 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}>
  22. 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
  23. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • talk[:obj_key][:cache]

  24. 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" }
  25. 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] }
  26. 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] }
  27. 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 {
  28. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • talk[:defaults][:wizardry]

  29. 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
  30. 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"
  31. 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 }
  32. 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}}}
  33. None
  34. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Fibonacci Sequence

    Fn = Fn-2 + Fn-1 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55…
  35. 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
  36. 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
  37. 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
  38. 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)
  39. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Fibonacci Performance

    hash[4] hash[2] hash[1] hash[0] hash[1] hash[3] hash[2]
  40. 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
  41. 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
  42. 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]
  43. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Fibonacci Performance

    29,860,703 calls 69 calls fib(35) hash[35]
  44. 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
  45. 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
  46. 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
  47. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Conway’s Game

    of Life
  48. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • talk[:defaults][:abbrev]

  49. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev require

    'abbrev' Abbrev.abbrev(["conf", "code"]) #=> {"conf"=>"conf", "con"=>"conf", "code"=>"code", "cod"=>"code"}
  50. 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
  51. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev Seen

    Table conf => conf
  52. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Abbrev Seen

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

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

    Table conf => conf conf 1 con con 1 => conf => conf
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • talk[:defaults][:app_code]

  63. 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
  64. 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
  65. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Real-Life Defaults!

    def some_expensive_calculation @some_expensive_calculation ||= _do_the_expensive_calculation end
  66. 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
  67. 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
  68. 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?
  69. RUBYCONF 2020 • ARIEL CAPLAN • @AMCAPLAN • Please say

    hi in the Slack chat!