Clojure's iterate in Ruby
Using Ruby Enumerator to emulate sequences
In functional languages, the key building blocks are functions and data. Clojure has a particularly interesting data structure, sequences, not featured in the Ruby standard library. A Clojure sequence is an immutable collection that representing the result of an algorithm. Previously, I described how to generate Clojure-like sequences in Ruby (without the immutability anyways), including Pascal's Triangle using Enumerator
, which allows us to package up an algorithm as an object that can emit values as any "eager" collection can, like Array
and Hash
.
Clojure provides a few functions that can be used to generate sequences,
including iterate
. According to the docs,
Returns a lazy sequence of x, (f x), (f (f x)) etc. f must be free of side-effects
In other words, iterate
will emit values starting with the first and repeatedly call the given function with the return value of the previous call.
The signature in Clojure looks this:
(iterate f x)
So, we can generate a simple sequence of numbers using the inc
function and some start value:
=> (iterate inc 1)
(1 2 3 4 5 ...)
Of course, we have a terse was of generating a sequence like this in Ruby:
irb(main)> (1..5).to_a
=> [1, 2, 3, 4, 5]
But this solution doesn't generalize to other types of sequences like, for instance,
generating a sequence of the powers of 2. In the example below, (partial * 2)
returns a function that multiplies a single argument by 2.
=> (iterate (partial * 2) 1)
(1 2 4 8 16 32 64 128 ...)
To get this result in Ruby, we might try something like:
irb> (1..7).each_with_object([]) { |n, seq| seq << (seq.last.nil? ? n : seq.last * 2) }
=> [1, 2, 4, 8, 16, 32, 64]
Not very pretty (ok, I admit that's a strawman). But this also is an "eagerly" generated collection whereas we want something that can be lazily generated to get closer to Clojure.
While there may be a number of ways to generate these sequences in Ruby, for this
exercise, we also want something that has a similar signature to Clojure's iterate
,
like this:
iterate(x, &block)
We'll leverage Ruby's method block convention in place of the function, f
.
Usage:
irb> iterate(1) { |n| n + 1 }
=> [1, 2, 3, 4, 5, ...]
irb> iterate(1) { |n| n * 2 }
=> [1, 2, 4, 8, 16, 32, 64, ...]
The two examples now have the same "surface area" and have a lot in common with the Clojure companions. So how would we implement this?
First a test. By the way, all the code found in the following examples is on Github.
require 'minitest/autorun'
require_relative './sequence'
class TestSequence < Minitest::Test
include Sequence
def test_iterate_increment
sequence = iterate(1) { |x| x + 1 }
assert_equal sequence.first(5), [1, 2, 3, 4, 5]
end
def test_iterate_power_of_2
sequence = iterate(1) { |x| x * 2 }
assert_equal sequence.first(5), [1, 2, 4, 8, 16]
end
end
We're going to implement iterate
in a Ruby module called Sequence
. Our test
for iterate
will return an instance of Enumerator
(the sequence
variable).
The enumerator allows use to generate the sequence on demand with the call to
#first
.
Here's the implementation:
module Sequence
def iterate(arg)
Enumerator.new do |yielder|
current = arg
loop do
yielder << current
current = yield(current)
end
end
end
end
Our implementation of iterate
returns an Enumerator
that will first yield
the given arg
and repeatedly call the given block with the result of the
previous call. The loop
construct means this enumeration can potentially
continue forever - capturing the spirit of a Clojure sequence. That means
we need to use a terminating functions like #first
or #take
to limit the
results, just like we would in Clojure:
=> (take 5 (iterate (partial * 2) 1))
(1 2 4 8 16)
The Ruby equivalent:
iterate(1) { |n| n * 2 }.take(5)
=> [1, 2, 4, 8, 16]
We could go one step further an make this method work as a mixin. Below is a
test for using iterate
as an instance method of a class using in our tests
that will simply delegate missing methods to the object passed in on
instantiation.
class TestSequence < Minitest::Test
include Sequence
class Sequenced < SimpleDelegator
include Sequence
end
def test_iterate_include
num = Sequenced.new(0)
sequence = num.iterate { |x| x - 1 }
assert_equal sequence.first(5), [0, -1, -2, -3, -4]
end
end
To make this pass, we need only set the default arg to self
:
module Sequence
def iterate(arg = self)
# ...
end
end
So what? Ok, well, you may be hard pressed to use iterate
in your daily work,
but there is certainly more room to think about data processing as functional
operations (free of side effects) on sequences (values that can be generated on demand). Something like iterate
need not apply to only numbers; you can imagine sequences of letters, time objects, or POROs also being generated. At times, Rubyist are too quick to wrap collections in other classes when simpler, more generalizable "functional" transforms could suffice.
When I started learning Clojure last year, I got really excited about the functional aspects of Ruby. "Wait, I thought everything in Ruby is an object." Yes, but a great thing about Ruby is its ability to adopt aspects of other languages. As Piotr Solnica illustrates in his recent talk, blending functional techniques with our OO code can have a lot of benefits including avoidance of side effects and favoring composability. Introducing sequence-generating methods, like we saw here, is just one small idea to help sprinkle a little functional flavor into your Ruby code.