Really simple anamorphisms in Ruby
Anamorphisms are functions that map from some object to a more complex structure containing the type of the object. They are the dual—a fancy word for complement—of Catamorphisms, functions that map from some complex structure down to a simpler object.
In simpler terms, a Catamorphism is a function like
inject: In Ruby, Inject takes a collection and produces something simpler. It is also called
fold or
reduce in other languages. Inject can do something like produce the sum of a list:
(1..5).inject &'+' => 15
Unfold does the reverse: it takes a single value and turns it into a collection. Now, a proper unfold can be configured with a seed value, a transformation, a stopping predicate, maybe a distinction between states and output values, and even the type of structure you want to create. A proper unfold would even work with
lazy lists if it didn’t have a stopping condition.
But sometimes you want something really simple. The state and the output value could be the same thing, eliminating a transformation from state to output. The stopping predicate could simple, it could stop when it reaches
nil. And it could always return Arrays. So you could use such a simple unfold whenever you have a seed value and some sort of function (expressed as a block) that returns nil when it has no more values.
For example:
10.unfold { |n| n-1 unless n == 1 }.inspect => [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
10.class.unfold(&:superclass).inspect => [Fixnum, Integer, Numeric, Object]
Hey, what happens if you combine really simple anamorphisms with really simple catamorphisms?
5.unfold(&'_-1 unless _==1').inject(&'*') => 120
Here is the code
unfold.rb. (If
overloading core classes with too much responsibility is not to your taste, it is trivial to express unfold as a function taking a seed and a block as arguments).
Now that I’ve whet your appetite, here’re
Really useful anamorphisms in Ruby. And for another implementation,
everything you ever wanted to know about implementing a true unfold in Ruby.
Enjoy!
(Code courtesy of
Mobile Commons. Thanks!)
Here’s an interesting email from Hugh Sasse:I was reading Code Complete 2 last night and Steve McConnell says that he wouldn’t hire a programmer who wrote a recursive factorial function. [I’m not sure that would be such a crime in languages like Lua with proper tail recursion, but still. (I’m probably wrong, now I’ve written that! “Open mouth, insert foot, echo internationally” as they used to say on Fidonet :-))] I thought “that seems a bit harsh”, especially since in real life they’d get one from a library most of the time, anyway.
So, as I have often run out of stack on Ruby, I’ve tried to rewrite unfold as an iterative function. It seems to be working. The script as modified blows up at 5000 on my system with the recursive version, but the iterative version succeeds at 5000.
Thanks for this blog entry. There’s something REALLY nice about this idea, which I can’t put my finger on. Might be something to do with loops terminating when they should, and The Pragmatic Programmers’ article about “cook until done”
http://www.pragprog.com/articles/cook-until-done
Unfold feels like a “do until finished” loop.
class Object
# As above, but iterative, rather than recursive.
def unfold2 &block
result = [self]
x = block.call(self)
while not x.nil?
result.push x
x = block.call(x)
end
return result
end
end
Labels: lispy, popular, ruby