The Memoization of Lazy Attributes

| 5 Comments

Great software changes the way you use computers. Great languages, libraries, and tools change the way you code. Moose qualifies as great.

Moose includes a feature known as attribute builders. When you declare the attributes of an object, you can specify default values:

use Moose;

has 'name', is => 'ro', default => 'Binky';

When someone builds a new object of this type, they can provide a name for the object, or get a default name of Binky.

For more dynamic behavior, provide a function reference as the default. For example, if an object needs to know its creation time (and not the time the system started):

use Moose;
use DateTime;

has 'birthtime', is => 'ro', default => sub { DateTime->now };

For even more flexibility, you can use the name of a method as the builder. This allows you to customize the behavior further with a subclass, role, or other mechanism:

use Moose;
use DateTime;

has 'birthtime', is => 'ro', builder => '_build_birthtime';

sub _build_birthtime { DateTime->now };

Overriding this builder is easy (far easier than intercepting all calls to the object's constructor.) As another benefit, you know that because Moose calls this builder during object construction, your object always gets constructed in a consistent, known-good state. You can rely on it being correct throughout the rest of its lifespan. (Obviously you have to avoid poking into it to do the wrong things, but that's up to you.)

This feature in and of itself would be good, but Moose went further to make it great. If you specify the attribute builder as lazy, Moose will only call the builder when someone uses the attribute's accessor:

use Moose;
use DateTime;

has 'first_access_time', is   => 'ro', builder => '_build_firstaccesstime',
                         lazy => 1;

sub _build_firstaccesstime { DateTime->now };

This makes the most sense when calculating an attribute is somehow expensive or the use of an attribute is rare. For example, I have an object which represents the calculation and projection of ten year free cash flow growth of a stock (like earnings, but measures liquid assets and less prone to manipulation via accrual accounting methods). This object is responsible for calculating the projected ten year average growth rate in free cash flow and produces a graph of ten year trailing free cash flow, the trendline for those ten years and ten years in the future, and projected growth curve ten years in the future.

That requires some math.

That math has a lot of interdependencies. Finding a good trendline for earnings which fluctuate means statistical analysis, such as with Statistics::Basic::LeastSquareFit. As it happens, the lazy lsf attribute nicely encapsulates that statistics object. Further, the growth rate is another lazy attribute, as are the sets of points of the trendline and the trend curve.

While the original proof of concept of this code performed all of the calculations in one large function of a couple of hundred lines, the current code is several methods of 10 - 50 lines apiece, with much better calculation accuracy, and better factoring. This is all thanks to Moose's lazy attributes.

The monolithic code was monolithic because I wanted to calculate everything once and only once: growth rate, number of points on the lines and curves, everything. Variables would stay in scope over tens and hundreds of lines because I'd need to use them later.

By factoring individual calculations into their own lazy builders, I can call the accessors when I need attribute data and everything snaps into place behind the scenes thanks to Moose. Oh, and if I've already calculated the data for the object, it's already there, and I don't have to recalculate it.

Lazy attributes give me memoization for free, and I don't even have to think about the data dependency graph of my calculations. It's an emergent property of the source code. That's one more step in expressing what I want to happen and not how to make it happen. Thanks, Moose!

5 Comments

You forgot "lazy => 1" in that second example.

One thing I would point out is that it's possible to create a circular dependency between lazy attributes, and Moose won't detect that for you. I think Perl itself will give a "deep recursion" warning if that happens, though.

Good point. You have to understand your data dependencies and manage them appropriately for this technique to work.

Yes, I've noticed myself doing this increasingly.

One other advantage of this is that it allows you inject explicit values (via the constructor, or attribute setters) into the midpoints of your calculation, overriding the values that would have been calculated. This can be very useful for testing.

The disadvantage of the technique is that it can lead to the the kingdom of nouns problem. Rather than having function calls like:

my $output = fibonacci($input);

You end up with nonsense pseudo-OO code:

my $calculator = FibonacciCalculator->new(input => $input);
my $output     = $calculator->result;

Something that is naturally a verb becomes a noun with a constructor and method call hanging off it.

Of course the solution is to also provide a plain old function which wraps the instantiation and method call.

sub fibonacci {
  return FibonacciCalculator->new(input => shift)->result;
}

And nobody needs know about how you've implemented it internally.

Modern Perl: The Book

cover image for Modern Perl: the book

The best Perl Programmers read Modern Perl: The Book.

sponsored by the How to Make a Smoothie guide

Categories

Pages

About this Entry

This page contains a single entry by chromatic published on February 27, 2012 10:45 AM.

Nagged by a Test Harness was the previous entry in this blog.

Modern Perl 2011-2012 PDFs Available! is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.


Powered by the Perl programming language

what is programming?