[gjs/wip/gobj-kitchen-sink: 1/19] lang: Introduce meta classes
- From: Jasper St. Pierre <jstpierre src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/wip/gobj-kitchen-sink: 1/19] lang: Introduce meta classes
- Date: Fri, 3 Feb 2012 19:21:06 +0000 (UTC)
commit c5edb874cad3e0744c6eaabab3fb7fa70afab40f
Author: Giovanni Campagna <gcampagna src gnome org>
Date: Wed Dec 7 22:36:13 2011 +0100
lang: Introduce meta classes
Classes created with new Lang.Class are now first class objects
themselves, and Lang.Class is itself a Lang.Class (to the extent
permitted by JS). It is therefore possible to inherit from Lang.Class
and create custom python-like metaclasses, instances of which are
actual instantiable classes.
modules/lang.js | 106 +++++++++++++++++++++++++++++++-------
test/js/testClass.js | 17 ++++++
test/js/testMetaClass.js | 126 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 229 insertions(+), 20 deletions(-)
---
diff --git a/modules/lang.js b/modules/lang.js
index d6903f8..b44b0e2 100644
--- a/modules/lang.js
+++ b/modules/lang.js
@@ -134,13 +134,19 @@ function defineAccessorProperty(object, name, getter, setter) {
// https://github.com/mootools/moootools-core
function _Base() {
+ throw new TypeError('Cannot instantiate abstract class _Base');
}
+_Base.__super__ = null;
_Base.prototype._init = function() { };
+_Base.prototype._construct = function() {
+ this._init.apply(this, arguments);
+ return this;
+};
_Base.prototype.__name__ = '_Base';
_Base.prototype.toString = function() {
return '[object ' + this.__name__ + ']';
-}
+};
function _parent() {
if (!this.__caller__)
@@ -158,7 +164,36 @@ function _parent() {
return previous.apply(this, arguments);
}
-function wrapFunction(obj, name, meth) {
+function getMetaClass(params) {
+ if (params.MetaClass)
+ return params.MetaClass;
+
+ if (params.Extends && params.Extends.prototype.__metaclass__)
+ return params.Extends.prototype.__metaclass__;
+
+ return null;
+}
+
+function Class(params) {
+ let metaClass = getMetaClass(params);
+
+ if (metaClass && metaClass != this.constructor) {
+ // Trick to apply variadic arguments to constructors --
+ // bind the arguments into the constructor function.
+ let args = Array.prototype.slice.call(arguments);
+ let curried = Function.prototype.bind.apply(metaClass, [,].concat(args));
+ return new curried();
+ } else {
+ return this._construct.apply(this, arguments);
+ }
+}
+
+Class.__super__ = _Base;
+Class.prototype = Object.create(_Base.prototype);
+Class.prototype.constructor = Class;
+Class.prototype.__name__ = 'Class';
+
+Class.prototype.wrapFunction = function(name, meth) {
if (meth._origin) meth = meth._origin;
function wrapper() {
@@ -171,17 +206,25 @@ function wrapFunction(obj, name, meth) {
wrapper._origin = meth;
wrapper._name = name;
- wrapper._owner = obj;
+ wrapper._owner = this;
return wrapper;
}
-function Class(params) {
+Class.prototype.toString = function() {
+ return '[object ' + this.__name__ + ' for ' + this.prototype.__name__ + ']';
+};
+
+Class.prototype._construct = function(params) {
if (!params.Name) {
throw new TypeError("Classes require an explicit 'Name' parameter.");
}
let name = params.Name;
+ let parent = params.Extends;
+ if (!parent)
+ parent = _Base;
+
let newClass;
if (params.Abstract) {
newClass = function() {
@@ -191,23 +234,43 @@ function Class(params) {
newClass = function() {
this.__caller__ = null;
- return this._init.apply(this, arguments);
+ return this._construct.apply(this, arguments);
};
}
- let parent = params.Extends;
- if (!parent)
- parent = _Base;
+ // Since it's not possible to create a constructor with
+ // a custom [[Prototype]], we have to do this to make
+ // "newClass instanceof Class" work, and so we can inherit
+ // methods/properties of Class.prototype, like wrapFunction.
+ newClass.__proto__ = this.constructor.prototype;
+
+ newClass.__super__ = parent;
+ newClass.prototype = Object.create(parent.prototype);
+ newClass.prototype.constructor = newClass;
+
+ newClass._init.apply(newClass, arguments);
+
+ Object.defineProperty(newClass.prototype, '__metaclass__',
+ { writable: false,
+ configurable: false,
+ enumerable: false,
+ value: this.constructor });
+
+ return newClass;
+};
+
+Class.prototype._init = function(params) {
+ let name = params.Name;
let propertyObj = { };
- let propertyDescriptors = Object.getOwnPropertyNames(params).forEach(function(name) {
+ Object.getOwnPropertyNames(params).forEach(function(name) {
if (name == 'Name' || name == 'Extends' || name == 'Abstract')
return;
let descriptor = Object.getOwnPropertyDescriptor(params, name);
if (typeof descriptor.value === 'function')
- descriptor.value = wrapFunction(newClass, name, descriptor.value);
+ descriptor.value = this.wrapFunction(name, descriptor.value);
// we inherit writable and enumerable from the property
// descriptor of params (they're both true if created from an
@@ -215,16 +278,19 @@ function Class(params) {
descriptor.configurable = false;
propertyObj[name] = descriptor;
- });
-
- newClass.__super__ = parent;
- newClass.prototype = Object.create(parent.prototype, propertyObj);
- newClass.prototype.constructor = newClass;
- newClass.prototype.__name__ = name;
- newClass.prototype.parent = _parent;
-
- return newClass;
-}
+ }.bind(this));
+
+ Object.defineProperties(this.prototype, propertyObj);
+ Object.defineProperties(this.prototype, {
+ '__name__': { writable: false,
+ configurable: false,
+ enumerable: false,
+ value: name },
+ 'parent': { writable: false,
+ configurable: false,
+ enumerable: false,
+ value: _parent }});
+};
// Merge stuff defined in native code
copyProperties(imports.langNative, this);
diff --git a/test/js/testClass.js b/test/js/testClass.js
index f61de3f..14d8aaf 100644
--- a/test/js/testClass.js
+++ b/test/js/testClass.js
@@ -103,6 +103,14 @@ const AbstractImpl2 = new Lang.Class({
// no _init here, we inherit the parent one
});
+const CustomConstruct = new Lang.Class({
+ Name: 'CustomConstruct',
+
+ _construct: function(one, two) {
+ return [one, two];
+ }
+});
+
function testClassFramework() {
let newMagic = new MagicBase('A');
assertEquals('A', newMagic.a);
@@ -185,4 +193,13 @@ function testCrossCall() {
assertEquals(50, res);
}
+function testConstruct() {
+ let instance = new CustomConstruct(1, 2);
+
+ assertTrue(instance instanceof Array);
+ assertTrue(!(instance instanceof CustomConstruct));
+
+ assertArrayEquals([1, 2], instance);
+}
+
gjstestRun();
diff --git a/test/js/testMetaClass.js b/test/js/testMetaClass.js
new file mode 100644
index 0000000..fd2df91
--- /dev/null
+++ b/test/js/testMetaClass.js
@@ -0,0 +1,126 @@
+// application/javascript;version=1.8 -*- mode: js; indent-tabs-mode: nil -*-
+
+if (!('assertEquals' in this)) { /* allow running this test standalone */
+ imports.lang.copyPublicProperties(imports.jsUnit, this);
+ gjstestRun = function() { return imports.jsUnit.gjstestRun(window); };
+}
+
+const Lang = imports.lang;
+
+function assertArrayEquals(expected, got) {
+ assertEquals(expected.length, got.length);
+ for (let i = 0; i < expected.length; i ++) {
+ assertEquals(expected[i], got[i]);
+ }
+}
+
+const NormalClass = new Lang.Class({
+ Name: 'NormalClass',
+
+ _init: function() {
+ this.one = 1;
+ }
+});
+
+let Subclassed = [];
+const MetaClass = new Lang.Class({
+ Name: 'MetaClass',
+ Extends: Lang.Class,
+
+ _init: function(params) {
+ Subclassed.push(params.Name);
+ this.parent(params);
+
+ if (params.Extended) {
+ this.prototype.dynamic_method = this.wrapFunction('dynamic_method', function() {
+ return 73;
+ });
+
+ this.DYNAMIC_CONSTANT = 2;
+ }
+ }
+});
+
+const CustomMetaOne = new MetaClass({
+ Name: 'CustomMetaOne',
+ Extends: NormalClass,
+ Extended: false,
+
+ _init: function() {
+ this.parent();
+
+ this.two = 2;
+ }
+});
+
+const CustomMetaTwo = new MetaClass({
+ Name: 'CustomMetaTwo',
+ Extends: NormalClass,
+ Extended: true,
+
+ _init: function() {
+ this.parent();
+
+ this.two = 2;
+ }
+});
+
+// This should inherit CustomMeta, even though
+// we use Lang.Class
+const CustomMetaSubclass = new Lang.Class({
+ Name: 'CustomMetaSubclass',
+ Extends: CustomMetaOne,
+ Extended: true,
+
+ _init: function() {
+ this.parent();
+
+ this.three = 3;
+ }
+});
+
+function testMetaClass() {
+ assertArrayEquals(['CustomMetaOne',
+ 'CustomMetaTwo',
+ 'CustomMetaSubclass'], Subclassed);
+
+ assertTrue(NormalClass instanceof Lang.Class);
+ assertTrue(MetaClass instanceof Lang.Class);
+
+ assertTrue(CustomMetaOne instanceof Lang.Class);
+ assertTrue(CustomMetaOne instanceof MetaClass);
+
+ assertEquals(2, CustomMetaTwo.DYNAMIC_CONSTANT);
+ assertUndefined(CustomMetaOne.DYNAMIC_CONSTANT);
+}
+
+function testMetaInstance() {
+ let instanceOne = new CustomMetaOne();
+
+ assertEquals(1, instanceOne.one);
+ assertEquals(2, instanceOne.two);
+
+ assertRaises(function() {
+ instanceOne.dynamic_method();
+ });
+
+ let instanceTwo = new CustomMetaTwo();
+ assertEquals(1, instanceTwo.one);
+ assertEquals(2, instanceTwo.two);
+ assertEquals(73, instanceTwo.dynamic_method());
+}
+
+function testMetaSubclass() {
+ assertTrue(CustomMetaSubclass instanceof MetaClass);
+
+ let instance = new CustomMetaSubclass();
+
+ assertEquals(1, instance.one);
+ assertEquals(2, instance.two);
+ assertEquals(3, instance.three);
+
+ assertEquals(73, instance.dynamic_method());
+ assertEquals(2, CustomMetaSubclass.DYNAMIC_CONSTANT);
+}
+
+gjstestRun();
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]