Making Closure less verbose with goog.scope

Objective

Background

Proposal

How it works

Style rules

Migration

Future work

Objective

Type fewer characters and wrap fewer lines.

Background

Many names in Closure are very long due to namespacing - take for example:

goog.dom.browserrange.AbstractBrowserRange

or

goog.ui.AutoComplete.EventType.SELECT

Names of this length are arduous to type, which slows developer productivity.  Furthermore, they cause line wraps which reduce code readability.

We aim to provide a way that these examples could be referred to as just AbstractBrowserRange or SELECT, similar to imports in Java.

When uncompiled, the code should still work in all browsers.  When compiled, there should be no size penalty compared to not using imports.

Non-Goals

Lexical hiding: goog.scope should not be used for preventing ‘locals’ for leaking into the global scope. Anonymous functions already solve this problem.

Proposal

goog.require('goog.dom');

goog.require('goog.dom.TagName');

goog.scope(function() {

  var createElement = goog.dom.createElement;

  var dom = goog.dom;

  document.body.appendChild(createElement(dom.TagName.DIV));

});

How it works

For uncompiled code, the implementation is trivial.

At compile time, names will be expanded to their full form in a compiler pass that occurs before current passes.  The compiler will throw away the function wrapper. If there are local variables that cannot be inlined, then the compiler will emit an error.

Why do it this way?

What isn’t great about this approach?

Style rules

goog.scope(function() {

  var Button = goog.ui.Button; // good

  var dom = goog.dom; // good.

  var tree = goog.ui.TreeControl; // BAD!

  document.body.appendChild(createElement(dom.TagName.DIV));

});

Migration

You auto-migrate your code to use goog.scope in 3 easy steps

1) Build the main js binary of your app, and save the compiled binary in some safe place.

2) Run the scopify.py script in your main app directory. It will automatically walk every JS file in the directory, g4 edit it, wrap it in a goog.scope, and create any obvious aliases. The scopify script is in the bin/ directory.

3) Build the main js binary of your app again, and diff the result against the binary you built in step (1). If they are the same, then the migration successfully scopified your code. Congratulations!

There are some gotchas that you should watch out for:

When goog.scope creates an alias, it captures the value of that global name. If the global variable is modified after it is aliased, then it may break the uncompiled code (but not the compiled code).

Indenting a file 2 spaces usually causes a merge conflict with any open files. Make sure your co-workers know that you are doing this before you begin. In many cases, merging the scopify change manually will be too hard, and it will be easier for them to reject changes, and re-run the scopify script in their local client.

Future work


Appendix 1 - working proposals that were considered

Manual aliasing with an annotation

goog.require('goog.dom');

goog.require('goog.dom.TagName');

/** @package */

(function() {

  var dom = goog.dom;

  var createElement = goog.dom.createElement;

  document.body.appendChild(createElement(dom.TagName.DIV));

})();

Why not use this:

Integration with goog.require

(function() {

var DIV = goog.require('goog.dom.TagName.DIV');

var createElement = goog.require(’goog.dom.createElement’);

mypackage.myfunction = function() {

  document.body.appendChild(createElement(DIV));

};

// other declarations go here

})();

Why not use this:

List match aliasing

goog.require('goog.dom');

goog.require('goog.dom.TagName');

goog.usingImports(

  goog.dom.createElement, goog.dom.TagName.DIV,

  function(createElement, DIV) {

    document.body.appendChild(createElement(DIV));

  });

How it works: Objects from the first list are matched with parameters to the function.

Why not use this:

Parameter-name aliasing

goog.require('goog.dom');

goog.require('goog.dom.TagName');

goog.usingImports(function(

  goog_dom,

    createElement, DomHelper,

  goog_dom_TagName,

    DIV, SPAN, A) {

  document.body.appendChild(createElement(DIV));

  // ...

});

How it works: The function is converted to a string, and the argument names are parsed from the string.  Parameter names containing underscores (that are not all upper case and therefore just static constants) are assumed to be namespaces for all following imports until the next namespace.

Why not use this:


Appendix 2 - broken proposals that were considered

Explicit sub-tree in require

To import goog.dom.createElement as ce and goog.dom.TagName.DIV as myDiv, use the following which displays the import tree with local identifier names as leaves.

function () {

  var ce, myDIV;

  goog.require({

     goog: {

        dom: {

          createElement: 'ce',

          TagName: { DIV: 'myDIV' }

        }

     }

   });

  ...

}

This can be implemented in non-compiled code as:

goog.require = function (__imports) {

  function __doImport(__prefix, __imports) {

    if (typeof __imports === 'string') {

      // TODO: assert __imports is a simple left hand side

      eval(__imports + ' = this' + __prefix);

      return

    }

    for (var k in __imports) {

      if (!{}.hasOwnProperty.call(__imports, k)) { continue; }

      __doImport(__prefix + '.' + k, __imports[k]);

    }

  }

  __doImport('', __imports);

}

Why it’s broken:

Doesn’t work due to evaling in the wrong scope

Temporary variable replacement

goog.require('goog.dom');

goog.defineClass('A',

    goog.imports('goog.dom.getElement'),

    goog.constructor(function(name) {

      this.name = name;

    }),

    function show(id) {

      getElement(id).innerHTML = this.name;

    });

goog.defineClass('B',

    goog.imports('goog.dom'),    goog.superClass(A),

    goog.constructor(function(name) {

      base(name);

    }),

    goog.member('id', null),

    function show(id) {

      base(id);

      this.id = id;

    },

    function louder() {

      dom.getElement(this.id).style.fontWeight = 'bold';

    });

goog.defineClass('C',

    goog.superClass(B),

    goog.constructor(function(name) {

      base(name + '!!!');

    }),

    function show(id) {

      base(id);

      this.louder();

    });

var a = new A('Hello World!');

a.show('div');

var b = new B('Bold');

b.show('div2');

b.louder();

var c = new C('Exclaim');

c.show('div3');

How it would work:

Every function has a pre and post step added to it that stores values of imported names from window, then replaces them, then calls the function, then restores them.

Why it’s broken:

It breaks in the presence of closures