Skip to Content

Extending Ruby's Enumerable

Posted on 5 mins read

One of the things I love most about Ruby is the built-in functional programming methods such as Enumerable#map, Enumerable#select, and Enumerable#inject. These allow you to write terse yet readable code with fewer side effects. This article looks at some ways to add more exotic functional operators to make Enumerable even more powerful. To start off, here’s an example of the canonical FizzBuzz problem using functional Ruby:

rules = { 3 => 'Fizz', 5 => 'Buzz' }
(1..100).each do |n|
  string = rules.inject('') { |s,rule| s += n % rule[0]  == 0 ? rule[1] : '' }
  puts s != '' ? string : n.to_s
end

Here we use inject to build a string by checking each rule. If no string is built, (ie string == '') then it simply prints the number itself, n.to_s. Inject repeatedly applies a function to each element of a list and keeps track of the result, like so:

[a, b, c, d].inject { |result, element| result += function(element) }
=> function(a) + function(b) + function(c) + function(d)

Zip Map

Another useful functional operator is zip. This allows you to merge lists using a custom function. Here is fizz buzz using zip to merge three lists:

fizzes = (1..100).map { |n| n % 3  == 0 ? 'Fizz' : '' }
buzzes = (1..100).map { |n| n % 5  == 0 ? 'Buzz' : '' }
(1..100).zip(fizzes, buzzes).map { |n,f,b| f+b == '' ? n.to_s : f+b }

What if we could combine zip and map into one function? Well thanks to Ruby’s fantastic powers of introspection, we can. Let’s call this zip_map and add it to the enumerable module.

module Enumerable
  def zip_map *lists
    self.map.with_index do |elem, idx|
      args = [elem] + lists.each.with_object(idx).map(&:[])
      yield *args, idx
    end
  end
end

We can now rewrite fizzbuzz using the new zip_map as follows:

fizzes = (1..100).map { |n| n % 3  == 0 ? 'Fizz' : '' }
buzzes = (1..100).map { |n| n % 5  == 0 ? 'Buzz' : '' }
(1..100).zip_map(fizzes, buzzes) { |n, f, b| f+b == '' ? n.to_s : f+b }

Map at Level

What if we want to apply map to a list of lists? just using map will apply a function to each element of the main list, but what if we want to apply map to each element of each sublist? We can implement map_at_level like so

module Enumerable
  def map_at_level level = 1, &block
    if level <= 1
      self.map &block
    else
      self.map { |e| e.map_at_level level-1, &block }
    end
  end
end

This recursively calls itself until the lowest level is reached then maps &block to the list at that position.

Transpose

Sometimes, when we have a list of lists and we’d like to swap the columns and rows. For example, if we were trying to solve a Sudoku puzzle, we might have some functions that act on the rows, such as checking if the row contains every number from 1 to 9. If we had a transpose method, we could reuse those functions on the columns as well. Transpose can be implemented like so:

module Enumerable
  def transpose
    self.with_index do |i|
      yield self.with_object(i).map &:[]
    end
  end
end

This allows us to do things like:

[[1,2,3],['a','b','c']].transpose
=> [[1, "a"], [2, "b"], [3, "c"]]

Or in the sudoku example something like this:

def columns_valid? sudoku
  rows_valid? sudoku.transpose
end

Group and Blockify

But in sudoku you also need to check each 3x3 block. What if we had a method that turned a list of lists into a list of blocks? We want a function that does the following:

[[1,1,2,2],[1,1,2,2],[3,3,4,4],[3,3,4,4]].blockify(2,2) # splits into 2x2 'blocks'
=> [[[1,1],[1,1]],[[2,2],[2,2]],[[3,3],[3,3]],[[4,4],[4,4]]]

We can do this by first creating a group method which splits a list into sublists, then using transpose to apply this method to both the rows and the columns like so:

module Enumerable
  def group size # groups list into sublists of size 'size'
    self.each_slice(size).to_a
  end

  def blockify width, height = nil # groups 2D array into sub-2D-arrays of size widthxheight
    self.each_slice(height||width).map { |n| n.transpose.group(width) }.flatten(1)
  end
end

Flatten at Level

In our Sudoku problem we don’t need the blocks to be 3x3 lists of lists, each block can simply be a list of all the numbers in that block. Therefore we need a flatten_at_level method to flatten each block into a list.

module Enumerable
  def flatten_at_level level, depth = nil
    self.map_at_level(level) { |list| list.flatten depth }
  end
end

We can use flatten_at_level like so:

list = [[1,1,2,2],[1,1,2,2],[3,3,4,4],[3,3,4,4]].blockify(2,2)
=> [[[1,1],[1,1]],[[2,2],[2,2]],[[3,3],[3,3]],[[4,4],[4,4]]]
list.flatten_at_level(2)
=> [[[1, 1, 1, 1], [2, 2, 2, 2]], [[3, 3, 3, 3], [4, 4, 4, 4]]]

This means we can check our sudoku as follows

def blocks_valid? sudoku
  rows_valid? sudoku.blockify(3).flatten_at_level(2)
end

Assuming a sudoku object is a 9x9 list of lists, the full list of methods to check a sudoku is valid could look like this:

def is_valid? sudoku
  rows_valid?(sudoku) && columns_valid?(sudoku) && blocks_valid?(sudoku)
end

def rows_valid? sudoku
  sudoku.all? { |row| row.sort == (1..row.length).to_a }
end

def columns_valid? sudoku
  rows_valid? sudoku.transpose
end

def blocks_valid? sudoku
  rows_valid? sudoku.blockify(Math.sqrt(sudoku.length).to_i).flatten_at_level(2)
end

Notice this implementation is generalised to work with any square sudoku, not just 9x9. By only implementing rows_valid? once and recycling it for columns and blocks, we can easily reimplement our definition of a valid sudoku by simply changing the implementation for rows_valid?.

All code in this article can be found in the following Github Gist