Skip to content

Latest commit

 

History

History
203 lines (176 loc) · 11.4 KB

File metadata and controls

203 lines (176 loc) · 11.4 KB

This project is a demonstration of an idea that closure-compiler doesn't seem to be able to follow all the time. A simple interface is declared, with a registry to declare and look up instances. Libraries can depend on the interface and registry itself, but usually won't specify implementations. In turn, an application will specify the library, and any implementations of the underlying interface.

graph LR;
    App1-->Library;
    App1-->Impl1;
    App1-->Impl2;
    
    App2-->Library;
    App2-->Impl1;
    App2-->Impl3;
    
    Library-->Registry;

    Impl1-->Registry;
    Impl2-->Registry;
    Impl3-->Registry;

Loading

Each different application might depend on different implementations, and use runtime logic, or defines to pick a specific implementation at build time.

The part that doesn't work consistently is using defines for this. For example, suppose that this is our registry:

var map = {};
function register(key, creator) {
    map[key] = creator;
}
function lookup(key) {
    return map[key]();
}


// Specify a default, and allow it to be overridden at build time
/** @define {string} */
const exampleDefault = goog.define('exampleDefault', 'python');
function lookupDefault() {
    return lookup(exampleDefault);
}

and a simple interface to implement

/**
 * 
 * @interface
 */
function ProgrammingLanguage() {
    
}
/**
 * @return {string}
 */
ProgrammingLanguage.prototype.getName = function() {};

Then, we have two implementations, which register themselves (rather than require that our application write code for each implementation):

/**
 * 
 * @constructor
 * @implements ProgrammingLanguage
 */
function PythonLanguage() {
    
}
PythonLanguage.prototype.getName = function() {
    return "Python";
};
register("python", () => new PythonLanguage());
/**
 *
 * @constructor
 * @implements ProgrammingLanguage
 */
function JavaLanguage() {
    
}
JavaLanguage.prototype.getName = function() {
    return "Java";
};
register("java", () => new JavaLanguage());

Now our library can use this any way it wants, and expect to have an instance provided.

function sayHello() {
    var defaultLanguage = lookupDefault();
    return "Hello, " + defaultLanguage.getName() + "!";
}

Our application need not use the original factory or implementations, as long as it correctly includes their sources as inputs to closure-compiler, but can just call on the library:

console.log(sayHello());

Invoking this in closure-compiler from the command line looks like this:

export CLOSURE_LIBRARY=~/workspace/closure-library/
java -jar closure-compiler-shaded.jar --compilation_level ADVANCED \
  --js src/languages/registry.js \
  --js src/languages/language.js \
  --js src/java/java.js \
  --js src/python/python.js \
  --js src/library/library.js \
  --js src/app.js \
  --js $CLOSURE_LIBRARY/closure/goog/base.js

Our naive expectation is that such an application should be optimized out to practically nothing:

console.log("Hello, Python!");

Instead, we get this (here using the web service, so that we can easily pretty print the output, and make it reproducible on other computers):

var a = {};
function b() {
}
b.prototype.g = function() {
  return "Python";
};
a.python = function() {
  return new b();
};
function c() {
}
c.prototype.g = function() {
  return "Java";
};
a.java = function() {
  return new c();
};
console.log("Hello, " + a.python().g() + "!");

But, if we take that closure output, and run it again, we do get the correct result, albeit with two warnings for "omitting" our required @constructor jsdocs:

console.log("Hello, Python!");

It seems as though the compiler can tell that an string key could be replaced by a property, but then once it has done so, it cannot remove unused properties. However, if the input already has that property with no string key, it is safe to remove the unreachable code. We still want to key this off of --define, so this might limit our options somewhat.

Removing the @define, and just using a constant string has no effect - this is probably what the compiler is doing already, so this makes sense. We then tried to go a step further and replace our string literals with either properties, or use goog.reflect.objectProperty(...) to indicate that the given key would correspond to a known property on that object:

goog.require('goog.reflect');
//...

// Specify a default, and allow it to be overridden at build time
function lookupDefault() {
    return map[goog.reflect.objectProperty('python', map)]();
    //return lookup('python')
}

//...

map.python = () => new PythonLanguage();
//register("python", () => new PythonLanguage());

//...

map.java = () => new JavaLanguage();
//register("java", () => new JavaLanguage());

This gets us slightly closer, but only in that the properties python() and java() are rewritten right away, so a subsequent recompile should still solve this, but still can't seem to be done within a single compile:

var a = {};
function b() {
}
b.prototype.g = function() {
    return "Python";
};
a.h = function() {
    return new b();
};
function c() {
}
c.prototype.g = function() {
    return "Java";
};
a.i = function() {
    return new c();
};
console.log("Hello, " + a.h().g() + "!");

Even if this did work, we would need the extra step of passing a string reference (from define, or from another method) to objectProperty() - from the documentation it is not clear that we could do that.