peter.michaux.ca - Lazy Function Definition Pattern (original) (raw)

This article examines a functional-programming design pattern I've been calling Lazy Function Definition. I've repeatedly found this pattern useful in JavaScript especially when writing cross-browser libraries that are efficient at runtime.

Warm-Up Problem

Write a function foo that returns a Date object that holds the time that foo was first called.

Solution #1: Ancient Technology

This simplistic solution uses a global variable t to hold the Date object. The first time foo is run it stores the time in t. In subsequent executions, foo just returns the value stored in t.

var t;
function foo() {
    if (t) {
        return t;
    }
    t = new Date();
    return t;
}

The above code has two problems. First, the variable t is an extra global variable and could be altered between calls to foo. Second, at runtime the code is not optimally efficient because the conditional must be evaluated each time foo is called. In this example, evaluating the conditional is inexpensive but in real-world examples there are often several expensive conditionals in a if-else-else-... structure.

Solution #2: The Module Pattern

We can remedy one problem in the first solution using the Module Pattern attributed to Cornford and Crockford. By using a closure we can hide the global t variable so that only the code in foo has access.

var foo = (function() {
    var t;
    return function() {
        if (t) {
            return t;
        }
        t = new Date();
        return t;
    }
})();

This is still not optimal at runtime because the conditional must be evaluated for each call to foo.

The module pattern is very powerful tool but the wrong one in this case, I believe.

Solution #3: Functions are Objects

By recognizing JavaScript functions are objects that can have properties, we can achieve a similar quality solution as the module pattern solution.

function foo() {
    if (foo.t) {
        return foo.t;
    }
    foo.t = new Date();
    return foo.t;
}

The fact that function objects can have properties results in very clean solutions in some situations. In my mind, this solution is conceptually simpler than the module pattern solution for this situation.

This solution avoids the t global variable of the first solution; however, the conditional is still run with each execution of foo.

Solution #4: Lazy Function Definition

And now, the reason you all came...

var foo = function() {
    var t = new Date();
    foo = function() {
        return t;
    };
    return foo();
};

When foo is called the first time, we instantiate a new Date object and reassign foo to a new function which has that Date object in it's closure. Then before the end of the first call to foo the new function value of foo is called and supplies the return value.

Subsequent calls to foo simply return the value of t that is stored in it's closure. This is a fast lookup and efficient especially if the conditionals used in the previous solutions are many and complex.

Another way of thinking about this this pattern is that the outer function that is first assigned to foo is a promise. It promises that the first time it is run it will redefine foo to a more useful function. The term "promise" loosely comes from Scheme's lazy evaluation mechanism. Any JavaScript programmer really ought to study Scheme as there is more written about functional programming for Scheme then exists for JavaScript.

Determining Page Scroll

When writing cross-browser JavaScript, frequently several different browser-specific algorithms are wrapped by a single JavaScript function name. This normalizes browser APIs by hiding browser discrepancies and makes building and maintaining complex page-specific JavaScript much simpler. When the wrapper function is called, the appropriate browser-specific algorithm must be run.

In a drag and drop library it is usually necessary to use the cursor location information supplied by mouse events. Mouse events give cursor coordinates relative to the browser window not the page. Adding the amount by which the page has been scrolled to the mouse's window coordinates gives the mouse's page coordinates. So we need a page scroll reporting function. For demonstration, this example defines a function called getScrollY. Since drag and drop libraries are working continuously during a drag, our getScrollY must be as efficient as possible.

Unfortunately there are three different browser-specific page scroll reporting algorithms. Richard Cornford wrote about these four algorithms in his feature detection article. The big catch is that one of the four page scroll reporting algorithms uses document.body. JavaScript libraries are usually loaded in the of an HTML document and the document.body property does not exist at that time. So we can't use feature detection to determine which of the four algorithms to use when the library is loaded.

Given these problems most JavaScript libraries do one of two things. The first option is to browser sniff navigator.userAgent and create an efficient, minimal getScrollY specific to the browser. Browser sniffing is repugnant because it is brittle and error prone. The second and far better option is to use feature detection to determine the correct algorithm each time getScrollY is run. This second option is not efficient, however.

The good news is that getScrollY in a drag and drop library will not be used until the user interacts with elements in the page. If elements in the page exist then document.body property will also exist. The first time getScrollY is run, we can use the Lazy Function Definition pattern in combination with feature detection to create an efficient getScrollY.

var getScrollY = function() {

    if (typeof window.pageYOffset == 'number') {

        getScrollY = function() {
            return window.pageYOffset;
        };

    } else if ((typeof document.compatMode == 'string') &&
               (document.compatMode.indexOf('CSS') >= 0) &&
               (document.documentElement) &&
               (typeof document.documentElement.scrollTop == 'number')) {

        getScrollY = function() {
            return document.documentElement.scrollTop;
        };

    } else if ((document.body) &&
               (typeof document.body.scrollTop == 'number')) {

      getScrollY = function() {
          return document.body.scrollTop;
      }

    } else {

      getScrollY = function() {
          return NaN;
      };

    }

    return getScrollY();
}

Just as a side note, the above may seem to be a surprisingly large effort to determine page scroll. Many people and libraries are satisfied with the following attempt to determine page scroll. This technique is even mentioned in the comments of this article.

var getScrollY = function() {
  return window.pageYOffset ||
         (document.documentElement && document.documentElement.scrollTop) ||
         (document.body && document.body.scrollTop);
}

With the above code, if the page has not been scrolled and either of the first two options are appropriate and document.body.scrollTop is undefined then the function returns undefined rather than 0. This make the getScrollY function insufficient for checking if the browser is capable of scroll reporting.

Summary

The Lazy Function Definition pattern has allowed me to write some dense, robust, efficient code. Each time I encounter this pattern I take a moment to admire JavaScript's functional programming abilities.

JavaScript supports both functional and object-oriented programming. There are book shelves worth of object-oriented design pattern books available that can be apply to JavaScript programming. Unfortunately there are few books with examples of functional programming design patterns. It will take time for the JavaScript community to aggregate a collection of good functional patterns.

Translations Chinese, Russian, Ruby

Update August 13, 2007 With Firefox/Safari/Opera getter methods it is possible to simulate lazy definition for properties that aren't functions.

this.__defineGetter__("foo", function() {
  var t = new Date();
  this.__defineGetter__("foo", function() {
    return t;
  });
  return t;
});

// To the user, foo appears as a plain old
// non-function valued property of the global object.
console.log(this.foo);
setTimeout(function(){console.log(this.foo);}, 3000);

Update August 15, 2007 If you are interested in this type of pattern Oliver Steel's JavaScript Memoisation Article is well worth reading. He explores other uses of this pattern avoiding repeated expensive computations in situations outside feature detection.

Update August 17, 2007 In his comment below, FredCK (of FCKeditor) makes a well reasoned argument that closures and memory leaks must be considered when implementing the lazy function definition pattern. The examples I have used in the article do not suffer from these problems but naive application of this pattern (or any JavaScript using closures) could suffer from memory leaks.

Update August 17, 2007 There are also reader comments on Ajaxian and reddit

Update August 22, 2007 Added an aside to the article about why a common and shorter version of getScrollY is not enough.

Update September 12, 2007 Richard Cornford posted this pattern to Usenet's comp.lang.javascript under the name The Russian Doll Pattern at least as early as August 3, 2004.

Update November 9, 2007 A variation of lazy function definition with reset.