<![CDATA[Mingmin on Hacks]]>2013-07-09T01:54:45+09:00http://melvinxie.github.io/Octopress<![CDATA[Ruby Lazy Infinite Stream in the SICP Way]]>2013-05-04T13:45:00+09:00http://melvinxie.github.io/blog/2013/05/04/ruby-lazy-infinite-stream-in-the-sicp-wayAfter reviewing the beautiful
Streams in SICP
again recently, while believing Ruby can be an acceptable Lisp, I wonder whether
Ruby can express infinite streams in the same way. There are several
existingimplementations already,
but they either have performance problem due to the lack of result caching or
don’t have the expressiveness I want, so I decided to implement my own:
lazy_stream. You can install it by:
1
$ gem install lazy_stream
Here I am going to explain the implementation and walk through some examples
from SICP.
Streams Are Delayed Lists
A stream is just a delayed list. We do lazy evaluation in Ruby with code blocks:
Lazy Stream Definition
123456789101112131415161718192021222324252627
classLazyStreamdefinitialize(first=nil,&proc)@first=first@proc=block_given??proc:lambda{LazyStream.new}endattr_reader:firstdefrest@rest||=@proc.callenddefempty?first.nil?endendmoduleKerneldeflazy_stream(first=nil,&rest)LazyStream.new(first,&rest)endends=lazy_stream(1){lazy_stream(2){lazy_stream(3)}}s.first#=> 1s.rest#=> A LazyStream object for the rest of the list [2, 3]lazy_stream#=> An empty LazyStream object
The rest of a stream is a closure @proc which returns another stream. We get
it by calling the closure and cache it meanwhile. Caching is like the
memo-proc function in SICP that caches the result of the closure, which is
important to greatly reduce computational complexity as we can see later.
A stream is empty when there is no first. We return an empty stream when there
is no code block given for the rest. The rest of an empty stream is also
another empty stream.
We can easily implement the methods at, drop, each, map, reduce, select, take,
to_a just like the methods of Array as you can see in my full implementation. Note
that drop, map, select, take are lazy too and return a LazyStream object that
satisfies the conditions.
Infinite Streams
With those simple structures, we can already construct a lot of cool infinite
streams explicitly, like these:
The streams are all infinite. We define them with recursive functions which will
only get evaluated when we actually need them. We can generate elements from the
stream as many as we want with take and then loop them all with each or
to_a.
The way we construct primes is called sieve of Eratosthenes. The short code here is
barely the definition of the sieve of Eratosthenes, which shows the power and
beauty of definitive programming.
Streams above were defined by specifying generating procedures that explicitly
compute the stream elements one by one. An alternative way to specify streams is
to take advantage of delayed evaluation to define streams implicitly. We need
the help of the class methods LazyStream.add which adds multiple streams into
one:
ones=lazy_stream(1){ones}ones.take(10).to_a#=> [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]integers=lazy_stream(1){LazyStream.add(ones,integers)}integers.take(10).to_a#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]# fibs is a stream beginning with 0 and 1, such that the rest of the stream can# be generated by adding fibs to itself shifted by one place:# 1 1 2 3 5 8 13 21 ... = fibs.rest# 0 1 1 2 3 5 8 13 ... = fibs# 0 1 1 2 3 5 8 13 21 34 ... = fibsfibs=lazy_stream(0){lazy_stream(1){LazyStream.add(fibs.rest,fibs)}}fibs.take(10).to_a#=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]# The reason this definition works is that, at any point, enough of the primes# stream has been generated to test the primality of the numbers we need to# check next.@primes=lazy_stream(2){integers_starting_from(3).select(&method(:prime?))}defprime?(n)iter=->psdoifps.first**2>ntrueelsifn%ps.first==0falseelseiter.call(ps.rest)endenditer.call(@primes)end@primes.take(10).to_a#=> [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
To get n Fibonacci numbers from fibs, the time complexity is just O(n)
because of the result caching of rest (we don’t need to calculate two fibs
to get the final fibs because we made use of the cache that already got
calculated).
Exploiting the Stream Paradigm
We can exploit the stream paradigm further like SICP did. We can define
several streams to give approximations to π, with Euler’s transform and the
tableau sequence accelerator:
The last super-acceleration is amazing. Taking 8 terms of the sequence yields
the correct value of π to 14 digits. The stream formulation is particularly
elegant and convenient.
Streams and Delayed Evaluation
Stream models of systems with loops may require uses of delay evaluation beyond
the delay evaluation supplied by the code block of lazy_stream. For example,
this figure shows a signal-processing system for solving the differential
equation dy/dt=f(y) where f is a given function.
With the help of the promise gem, we can setup a delayed argument for the
feedback signal in our stream definition:
Solving the differential equation dy/dt=f(y)
123456789101112131415
require'promise'# integrand is a delayed argument that promises to give result after a loopdefintegral(integrand,initial,dt)int=lazy_stream(initial){LazyStream.add(integrand.scale(dt),int)}end# Setup the delayed integrand with promisedefsolve(f,y0,dt)y=integral(promise{y.map(&f)},y0,dt)end# Approximating e ~= 2.718 by computing the value at y = 1 of the solution to# the differential equation dy/dt = y with initial condition y(0) = 1:solve(lambda{|y|y},1,0.001).at(1000)#=> 2.716923932235896
Tail Call Optimization
Last but not the least, since the functions are all recursive here, to make them
actually work for large data set, we need to enable the
tail call optimization
in Ruby, otherwise you will hit the stack level too deep error soon.
All the example code here can be found in my
Ruby playground.