def things_n_stuff

Adventures in Code

Parallel Assignment in Ruby

The Problem:

How do you iterate over two collections, and at the same time generate a new, third collection based on elements selected from the previous two? In other words, how do I create a list of paired things by ticking down two others?

The things that I’ve been wondering about and now love:

  1. Enumerator (new love)
  2. Enumerable (rediscovered love)
  3. Inject (constantly blows my mind)

I first came across this problem during the decoder exercise early on at the Flatiron School. I had an encoded message and a cypher, and I wanted to make a new hash by iterating over both and setting the cypher as a key and the code as its value. There are tons of workarounds but I got a bit fixated on doing it more or less in one go.

More recently I’ve been perplexed by the Enumerator class. One super cool thing it lets you do is keep track of internal position. What this means is that after you set it up, you can do:

# incomplete:

thing = ['oogie', 'boogie', 'blork']
> thing.next
=> "oogie"
> thing.next
=> "boogie"
> thing.next
=> "blork"

All that’s needed to unlock this capability is to call the collection with an enumerable method, sans block. And the only requirement is that the method includes an implementation of each, which as far as I know is every method in Enumerable. So:

> thing = ['oogie', 'boogie', 'blork'].each
=> #<Enumerator: ...>

Once you’ve made an Enumerator object, you can access this amazing ‘internal tracker’.

> thing = ['oogie', 'boogie', 'blork'].each
=> #<Enumerator: ...>
> thing.next
=> "oogie"
> thing.next
=> "boogie"
> thing.next
=> "blork"

Side note, Ruby also has the nifty to_enum method that turns anything into an object of the Enumerator class. So (1..4).each is equivalent to (1..4).to_enum, though they are different instances of the Enumerator class.

> (1..4).each.inspect
=> "#<Enumerator: 1..4:each>"
> (1..4).to_enum.inspect
=> "#<Enumerator: 1..4:each>"
> (1..4).each === (1..4).to_enum
=> false
> (1..4).each.object_id
=> 70349656520460
> (1..4).to_enum.object_id
=> 70349656484240

Next, lets add some inject to the mix.

Inject is mindblowing and has tons of uses and applications, but it basically works by taking a collection and doing something to each element in context of the previous element. A basic example is summing numbers:

> [1,2,3,4,5].inject {|sum, next_thing| sum + next_thing}
=> 15

Here inject is grabbing the first item in the array, adding the second item to it, and then using that as the new total, before continuing to add subsequent items in the same way. Interestingly, inject also lets you pass an argument before the block to use as the starting value.

> [1,2,3,4,5].inject(50) {|sum, next_thing| sum + next_thing}
=> 65

And because you can easily pass things into arrays and hashes, you can also slip in an empty one as your starter. Don’t forget to specify that you want the new collection returned.

> [1,2,3,4,5].inject({}) {|array, item| array[item] = []; array}
=> {1=>[], 2=>[], 3=>[], 4=>[], 5=>[]}

Lastly, by combining Enumerator and inject you can make one neat, succinct, clear, and logical iterator that’s (sort of) running 2 parallel iterations.

> letters = ('a'..'z').each
=> #<Enumerator: ...>
> gg = (1..26).inject({}){|hash, key| hash[key]=[letters.next]; hash}
=> {1=>["a"],
    2=>["b"],
    3=>["c"],
    4=>["d"],
    5=>["e"],
    6=>["f"],
    7=>["g"],
    8=>["h"],
    9=>["i"],
    10=>["j"],
    11=>["k"],
    12=>["l"],
    13=>["m"],
    14=>["n"],
    15=>["o"],
    16=>["p"],
    17=>["q"],
    18=>["r"],
    19=>["s"],
    20=>["t"],
    21=>["u"],
    22=>["v"],
    23=>["w"],
    24=>["x"],
    25=>["y"],
    26=>["z"]}  

Nifty eh?

After writing this I became fascinated with making a method that could take 2 collections of arrays, hashes, or ranges, and a last argument for the rate of iterating over each collection relative to the other. In the works, stay tuned.