A PlanarFe Adventure

LearnLoveCode

A New Hash#map

https://www.etsy.com/shop/bananastrudel?ref=l2-shopheader-name

The Problem

The Enumerable mixin’s #map/#collect methods are fantastic for mutating the values in an array but have an inconsistent behavior when applied to a hash. So since using #map/#collect on an array gives you this:

1
2
[1,2,3].map{|i| i*i}
#=> [1,4,9]

you might expect the hash to behave something like this:

1
2
{a: 1, b: 2, c: 3}.map{|k,v| k: v*v}
#=> {a: 1, b: 4, c: 9}

but instead you get: SyntaxError: (irb):1: syntax error, unexpected ':', expecting '}'

You can get around this by wrapping your key-value pair in curly braces

1
2
{a: 1, b: 2, c: 3}.map{|k,v| {k: v*v} }
#=> [{:k=>1}, {:k=>4}, {:k=>9}] 

but now you have an array of key-value pairs which still is not the behavior that we are shooting for.

The Solution

It’s not pretty but we can use the Enumerable#inject combined with the Hash#merge methods to hack our way toward our goal:

1
2
{a: 1, b: 2, c: 3}.inject( {} ){| hash, (k, v) | hash.merge( k=> v*v )}
#=> {:a=>1, :b=>4, :c=>9} 

Pretty straight forward if you have seen inject, right? The hash (creatively called hash inside the block) acts as a collector. Passing a key-value pair into the #merge method called on the collector hash essentially says ‘Hey, Ruby! Here’s a hash with one key-value pair to merge into #inject’s collector!’ Then the merged hash is passed back to #inject to be the collector for the next key-value pair.

But that’s odd…

The weird bit is between the pipes. What’s going on there?

1
2
{a: 1, b: 2, c: 3}.inject( {} ){| hash, pair | pair}
#=> [c: , 3]

So the pair is passed into the block as an array containing the key and value as separate elements. By replacing pair with (k,v)

1
{a: 1, b: 2, c: 3}.inject( {} ){| hash, (k, v) | hash.merge( k=> v*v )}

we match up k to the first value in the array and v to the second value in the array through Multiple Assignment along the lines of:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
a = [1,2,3]
(x,y,z)=a #=> [1,2,3]
x #=> 1
y #=> 2
z #=> 3

#OR

x,y,z = a
x #=> 1
y #=> 2
z #=> 3



b = [6,7,8]
(q,r) = b
q #=> 6
r #=> 7

So, inside the block we tell Ruby to make

1
2
3
(k,v)=[some_key, some_value]
k #=> some_key
v #=> some_value

then recreate the key-value pair with some sort of mapping executed on the key or value (or maybe even both) k => v*v and merge them into the collector hash hash.merge( k=> v*v ) and then let inject do its' thing and return the collector hash.

Vois là! You’ve built a more semantic Hash#map. Don’t go monkey-patching anything. Maybe try a refinement instead?

Sources:

Ruby Doc Enumerable#map Chris Holtz- Let’s Make a Ruby Hash Map Method That Returns a Hash Instead of an Array