Hashing Things Out
So an Array is a collection of stuff right? A Hash is just a collection of stuff that points to other stuff.
Think dictionaries. A word points to a definition. We’d call the word our key, what we use to unlock the value from the hash.
Unlike an array we use keys to get at what we want instead of indexes like 0 and 1:
hash = { 'a' => 1, 'b' => 2 }
hash['a'] # => 1
So same way of getting at the value except we give it the key we want instead. Now thing is, a key could be anything. We could even make it a Number if we really wanted to and play like we’re an Array:
hash = { 0 => 1, 1 => 2 }
hash[0] # => 1
So why don’t you just use Hashes? Well, do you really want to keep track of what Number you’re on? I sure don’t. Arrays are also a bit more efficient to boot.
Each and Every Time
See, there’s a fun difference between Array each
and Hash. Hash gives us two arguments instead of just one! What a deal!
{'a' => 1, 'b' => 2}.each { |key, value| puts "#{key}: #{value}" }
Best to remember we could name those arguments whatever we want to, but key and value make a lot of sense for us right now so let's use those. I'm partial to foo
and bar
myself though, keeps people on their toes:
{'a' => 1, 'b' => 2}.each { |foo, bar| puts "#{foo}: #{bar}" }
Methods, Blocks, and other concepts in Ruby can take more than one argument, and you can name those whatever you want.
Defaultly Yours
What happens when we ask about a key which doesn’t exist? We check the default value!
Normally that's nil:
hash = {}
hash['a'] # => nil
But if we make our Hash a different way we get a few more fun things:
hash = Hash.new(0)
hash['a'] # => 0
Nifty! What if we try and do that with an Array?
2.3.3 :001 > hash = Hash.new([])
=> {}
2.3.3 :002 > hash['a'].push(*%w[some words because fun])
=> ["some", "words", "because", "fun"]
2.3.3 :003 > hash
=> {}
2.3.3 :004 > hash['b'].push(*%w[some words])
=> ["some", "words", "because", "fun", "some", "words"]
2.3.3 :005 > hash
=> {}
2.3.3 :006 > hash['b']
=> ["some", "words", "because", "fun", "some", "words"]
....waitasecond.
No no no no no no n...
sighs Fine. Turns out we can't do that with certain types of values like Hashes and Arrays.
Whenever we set it to default to that Array it's using the same Array for every single default value. Our Hash knows how to find it but it won't show unless we ask it real nicely. That simply won't do.
To fix this we have to use what's called a block constructor instead:
2.3.3 :001 > hash = Hash.new { |hash, key| hash[key] = [] }
=> {}
2.3.3 :002 > hash['a'].push(*%w[some words because fun])
=> ["some", "words", "because", "fun"]
2.3.3 :003 > hash
=> {"a"=>["some", "words", "because", "fun"]}
2.3.3 :004 > hash['b'].push(*%w[some words])
=> ["some", "words"]
Now why's that? Because each time we ask for a key our Hash doesn't know about it's forced to make a new Array and set that key on our Hash.
There are some really fun things we can do with this but that's way outside the scope of this book.
Lucky us though, that's already written: https://medium.com/@baweaver/abusing-hash-constructors-65d59c0a5b27
Just be careful of the sections labeled black magic, it's been known to scare more than a few seasoned Ruby programmers.
Does your Word Count?
How about we use programming for something? Say we have ourselves a nice string:
quote = 'A fool thinks himself to be wise but a wise man knows himself to be a fool'
Now using what we already know (split
, each
, Hash
, and some Integer
) how would you get a count of the words in that sentence?
Give it a second and try it out in irb before you go looking at the next section. Write down what confuses you, it'll help for later.
2.3.3 :001 > quote = 'A fool thinks himself to be wise but a wise man knows himself to be a fool'
=> "A fool thinks himself to be wise but a wise man knows himself to be a fool"
2.3.3 :002 > counts = Hash.new(0)
=> {}
2.3.3 :003 > quote.split.each { |w| counts[w] += 1 }
=> ["A", "fool", "thinks", "himself", "to", "be", "wise", "but", "a", "wise", "man", "knows", "himself", "to", "be", "a", "fool"]
2.3.3 :004 > counts
=> {"A"=>1, "fool"=>2, "thinks"=>1, "himself"=>2, "to"=>2, "be"=>2, "wise"=>2, "but"=>1, "a"=>2, "man"=>1, "knows"=>1}
Now granted there are some better ways to do this later on, but for now you get the gist of it.
Notice something odd about this? A
is capitalized. How would you deal with that one? Take a look back through the String documentation and see if you can figure it out!