Extending Ruby's Enumerable
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