/call_by_need.rb

A memoization thing, intended for recreational use.

Primary LanguageRubyMIT LicenseMIT

Call By Need

So yeah, uhm, you can read about what that means on Wikipedia (it's like an on-line encyclopedia type thing). Go and read it!

Now that you know what it is, let me disappoint you by narrowing the definition a little:

How does one do lazy evaluation in Ruby? By wrapping computations in lambdas and passing those around instead of their evaluated results. An instance of CallByNeed::Context is a somewhat usable container for lambdas. You're supposed to fill it with some names and computations that when evaluated yield the corresponding values for those names. That makes the evaluation of what's inside a Context lazy from the point of view of the part of the program that decided to pass around the ability to do certain things in the form of that Context.

The other ingredient is memoization. Context instances only ever evaluate names once (by calling the block assigned to that name), and reuse the result after.

If you use that more constrained definiton, the name makes a little more sense.

Let's see what using it looks like!

Examples

Simple memoization

require 'call_by_need'

c = CallByNeed::Context.new do |c|
  c.declare(:b) { |x| x.a + 10 }
  c.declare(:a) { Time.now.utc }
end

Time.now.utc
# => 2013-09-07 14:48:50 UTC
sleep(5)
c.a
# => 2013-09-07 14:48:55 UTC
sleep(2)
c.b
# => 2013-09-07 14:49:05 UTC
c.a
# => 2013-09-07 14:48:55 UTC

Merging of contexts

require 'call_by_need'

c1 = CallByNeed::Context.new do |c|
  c.declare(:a, &:b) # b doesn't exist yet
  c.declare(:x) { |x| x.b + 'x' }
end

c1.a
# NoMethodError: undefined method `b'

c2 = CallByNeed::Context.new(c1) do |c|
  c.declare(:b) { 'b' }
end

c2.a
# => 'b'
c2.x
# => 'bx'

Every declaration yields the Context instance that it belongs to at the time of its evaluation into its block argument (named x in the example). That means you can combine multiple Context instances into one, and a Context field that was defined earlier is able to see anything that exists at the time of its evaluation.

You can also give your contexts names

The next example shows some more elaborate potential usage context that involves slow things and fast things. It is also meant to demonstrate that using call_by_name doesn't harm readability in cases where memoization is not that essential. Just use it for everything! :D

require 'call_by_need'
require 'benchmark'

class Genderizer
  def self.genders(name)
    [].tap do |_genders|
      _genders << :girl if girl?(name)
      _genders << :boy  if boy?(name)
    end
  end

  # slow method!
  def self.girl?(name)
    sleep(0.5)
    name =~ /^[amk]/
  end

  # another slow method!
  def self.boy?(name)
    sleep(0.5)
    name =~ /^[bk]/
  end
end

class People
  include CallByNeed

  def initialize(genderizer)
    declare(:kids) do
      [ 'alice', 'bob', 'marie', 'kim' ]
    end
    declare(:genders) do
      kids.inject({}) do |acc, name|
        acc.merge!(name => genderizer.genders(name))
      end
    end
    declare(:girls) do
      kids.select { |name| genders[name].include?(:girl) }
    end
    declare(:boys) do
      kids.select { |name| genders[name].include?(:boy) }
    end
    declare(:unknown) do
      boys & girls
    end
  end
end

people = People.new(Genderizer)
Benchmark.realtime { people.girls }
# => 4.005
people.evaluated?(:unknown)
# => false
Benchmark.realtime { people.unknown }
# => 0.00003

Okay…?

So the whole thing is basically a generalized ||=. But if you get used to passing around Context objects instead of lambdas or plain values, you get the laziness aspect on top of it.

The nice thing about it is that you can use it in an ad-hoc way to define anonymous memoizing contexts as a replacement for variables (add some non-determinism to your imperative code!), or as part of regular class definitions. And in both cases the syntax is pretty much the same.

But it doesn't do anything you couldn't do without it.