Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solution without generators #4

Merged
merged 4 commits into from
Jan 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 11 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Making Layer Cakes

Use generators for class/traits-like composition of objects-as-closures
Helpers for class/traits-like composition of objects-as-closures

## The Objects-as-closures Pattern

The classic object or object-oriented programming has public methods and private instance variables. There are several ways to encode this in JavaScript. The following code uses JavaScript's class syntax with the new support for private instance variables.
The classic object or object-oriented programming has public methods and private instance variables. There are several ways to encode this in JavaScript. The following code uses JavaScript's class syntax with the [new support for private instance variables](https://github.com/tc39/proposal-class-fields#private-fields).

```js
class Point {
Expand Down Expand Up @@ -70,16 +70,16 @@ The `WobblyPoint` example demonstrates the features of class inheritance, where
The wobbly point instances created by the `WobblyPoint` constructor above are each made of two layers: One expressed directly by the `Point` class, and one expressed directly by the `WobbyPoint` class. The *layer-cake* library provided by this repository supports such layer combination for the objects-as-closure pattern.

```js
function* BasePointLayer(x, y) {
const [self] = yield {
function BasePointLayer(x, y) {
return self => {
getX() { return x; },
getY() { return y; },
toString() { return `<${self.getX()},${self.getY()}>`; },
};
}

function* WobblyPointLayer(wobble) {
const [_self, supr] = yield {
function WobblyPointLayer(wobble) {
return (_self, supr) => {
getX() { return supr.getX() + wobble++; },
};
}
Expand All @@ -91,27 +91,23 @@ function makeWobblyPoint(x, y, wobble) {

The pattern above has more of the flexibility associated with traits or mixins. Each layer is expressed separately. The layers are then combined by a distinct maker function `makeWobblyPoint`. Different making functions can combine overlapping sets of layers in different manners.

The code for expressing the combinable layers is written in the peculiar manner shown above, using generators. We introduce this peculiar generator pattern to solve a hard problem: The code in each layer needs to capture a lexical variable that refers to the object as a whole. However, the object-as-a-whole is not yet assembled, and will be assembled only by a distinct piece of code written in a distinct scope.

In the generator function pattern shown above, the methods of a given layer are defined in the scope of variables such as `self` and `supr` that are bound on the left of the `yield`. These are bound to values extracted from the value that the `yield` returns. However, before that happens, the generator yields the argument to `yield`, the object containing that layer's methods, to be combined to make the object itself. The helper function `makeClassCake` exported by this repository first runs through the list of generators it is given, extracting the layer functions from each, combining them into the overall object. Only when the object is complete does it go back to these generators, in order to bind that object to each layer's `self` variable.

The list of layers given to `makeClassCake` is in order from super-class-like to subclass-like. This enables each layer to also bind a `supr` variable to serve a function analogous to the `super` keyword supported by classes. The `supr` variable is bound to a combination of all layers above (to the left of) the given layer.

## Class-like vs Trait-like Layer Combination

This repository also exports a `makeTraitCake` providing a simple form of trait combination. Trait combination is a non-hierarchical alternative to class inheritance. Each trait defines a separate layer as above. But the methods defined by each layer must be disjoint. This simple form of trait combination does not support any form of override or renaming. The corresponding example

```js
function* AbstractPointLayer(x, y) {
const [self] = yield {
function AbstractPointLayer(x, y) {
return self => {
baseGetX() { return x; },
getY() { return y; },
toString() { return `<${self.getX()},${self.getY()}>`; },
};
}

function* WobblyPointLayer(wobble) {
const [self] = yield {
function WobblyPointLayer(wobble) {
return self => {
getX() { return self.baseGetX() + wobble++; },
};
}
Expand All @@ -126,4 +122,4 @@ Variants of these examples are found in the test cases of this repository.
# References

[TraitsJS](https://traitsjs.github.io/traits.js-website/) is an earlier ES5-based traits library for the objects-as-closure pattern that is more full featured. It is explained at [traits.js:
Robust Object Composition and High-integrity Objects for ECMAScript 5](https://traitsjs.github.io/traits.js-website/files/traitsJS_PLASTIC2011_final.pdf). It predates generators, and so uses `this` to solve the self-reference problem.
Robust Object Composition and High-integrity Objects for ECMAScript 5](https://traitsjs.github.io/traits.js-website/files/traitsJS_PLASTIC2011_final.pdf). It uses `this` to solve the self-reference problem.
39 changes: 19 additions & 20 deletions lib/layer-cake.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import harden from '@agoric/harden';

const {
create: objCreate,
getOwnPropertyDescriptors: gopds,
Expand All @@ -7,32 +9,29 @@ const {
} = Object;

function makeCakeMaker(combineLayers) {
return function makeCake(layerGens) {
let self;
const suprs = layerGens.map(layerGen => {
const supr = self;
const layer = layerGen.next().value;
// ... order? disjoint? requiredHoles?
self = combineLayers(self, layer);
return supr;
});
layerGens.forEach((layerGen, i) => {
layerGen.next([self, suprs[i]]);
});
return self;
return function makeCake(layerFuns) {
const finalObject = {};
const suprs = []

for(const layerFun of layerFuns){
const supr = suprs[suprs.length - 1];
combineLayers(finalObject, layerFun(finalObject, supr));
const nextSupr = objCreate(objPrototype, gopds(finalObject))
suprs.push(nextSupr)
}

return harden(finalObject);
};
}

// The lower layer overrides the upper layer
const makeClassCake = makeCakeMaker((upper = {}, lower = {}) =>
objCreate(objPrototype, { ...gopds(upper), ...gopds(lower) }),
// The additionalLayer overrides existing layers
const makeClassCake = makeCakeMaker((objectInConstruction, additionalLayer = {}) =>
defineProperties(objectInConstruction, gopds({...additionalLayer}))
);

// The layers must be disjoint
const makeTraitCake = makeCakeMaker((upper = {}, lower = {}) => {
const result = objCreate(objPrototype, gopds(freeze({ ...upper })));
defineProperties(result, gopds(lower));
return result;
const makeTraitCake = makeCakeMaker((objectInConstruction, additionalLayer = {}) => {
defineProperties(objectInConstruction, gopds(freeze({ ...additionalLayer })));
});

export { makeCakeMaker, makeClassCake, makeTraitCake };
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"name": "@agoric/layer-cake",
"version": "0.0.1",
"description": "???",
"main": "dist/layer-cake.cjs.js",
"module": "dist/layer-cake.esm.js",
"browser": "dist/layer-cake.umd.js",
"main": "lib/layer-cake.js",
"module": "lib/layer-cake.js",
"browser": "lib/layer-cake.js",
"scripts": {
"test": "tape -r esm 'test/**/*.js'",
"lint-fix": "eslint --fix '**/*.{js,jsx}'",
Expand Down
62 changes: 8 additions & 54 deletions test/test-class-cake.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import test from 'tape';
import harden from '@agoric/harden';
import { makeClassCake } from '../lib/layer-cake';

test('cajita-wobbly-point-class test', t => {
test('hardened-wobbly-point-class test', t => {
try {
function* BasePointLayer(x, y) {
const [self] = yield {
function BasePointLayer(x, y) {
return self => harden({
getX() {
return x;
},
Expand All @@ -15,63 +15,17 @@ test('cajita-wobbly-point-class test', t => {
toString() {
return `<${self.getX()},${self.getY()}>`;
},
};
});
}
harden(BasePointLayer);

function* WobblyPointLayer(wobble) {
const [_self, supr] = yield {
function WobblyPointLayer(wobble) {
return (_self, supr) => harden({
getX() {
// eslint-disable-next-line no-plusplus
return supr.getX() + wobble++;
},
};
}

function makeWobblyPoint(x, y, wobble) {
return makeClassCake([BasePointLayer(x, y), WobblyPointLayer(wobble)]);
}

const wp1 = makeWobblyPoint(3, 5, 0.1);
const wp2 = makeWobblyPoint(3, 5, 0.1);

t.equal(`${wp1}`, '<3.1,5>');
t.equal(`${wp1}`, '<4.1,5>');
t.equal(`${wp2}`, '<3.1,5>');
} catch (e) {
t.isNot(e, e, 'unexpected exception');
} finally {
t.end();
}
});

test('hardened-wobbly-point-class test', t => {
try {
function* BasePointLayer(x, y) {
const [self] = harden(
yield harden({
getX() {
return x;
},
getY() {
return y;
},
toString() {
return `<${self.getX()},${self.getY()}>`;
},
}),
);
}
harden(BasePointLayer);

function* WobblyPointLayer(wobble) {
const [_self, supr] = harden(
yield harden({
getX() {
// eslint-disable-next-line no-plusplus
return supr.getX() + wobble++;
},
}),
);
})
}
harden(WobblyPointLayer);

Expand Down
66 changes: 9 additions & 57 deletions test/test-trait-cake.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import test from 'tape';
import harden from '@agoric/harden';
import { makeTraitCake } from '../lib/layer-cake';

test('cajita-wobbly-point-trait test', t => {

test('hardened-wobbly-point-trait test', t => {
try {
function* AbstractPointLayer(x, y) {
const [self] = yield {
function AbstractPointLayer(x, y) {
return self => harden({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're only testing the hardening cases, you can just remove redundant cases.

baseGetX() {
return x;
},
Expand All @@ -15,66 +16,17 @@ test('cajita-wobbly-point-trait test', t => {
toString() {
return `<${self.getX()},${self.getY()}>`;
},
};
});
}
harden(AbstractPointLayer);

function* WobblyPointLayer(wobble) {
const [self] = yield {
function WobblyPointLayer(wobble) {
return self => harden({
getX() {
// eslint-disable-next-line no-plusplus
return self.baseGetX() + wobble++;
},
};
}

function makeWobblyPoint(x, y, wobble) {
return makeTraitCake([
AbstractPointLayer(x, y),
WobblyPointLayer(wobble),
]);
}

const wp1 = makeWobblyPoint(3, 5, 0.1);
const wp2 = makeWobblyPoint(3, 5, 0.1);

t.equal(`${wp1}`, '<3.1,5>');
t.equal(`${wp1}`, '<4.1,5>');
t.equal(`${wp2}`, '<3.1,5>');
} catch (e) {
t.isNot(e, e, 'unexpected exception');
} finally {
t.end();
}
});

test('hardened-wobbly-point-trait test', t => {
try {
function* AbstractPointLayer(x, y) {
const [self] = harden(
yield harden({
baseGetX() {
return x;
},
getY() {
return y;
},
toString() {
return `<${self.getX()},${self.getY()}>`;
},
}),
);
}
harden(AbstractPointLayer);

function* WobblyPointLayer(wobble) {
const [self] = harden(
yield harden({
getX() {
// eslint-disable-next-line no-plusplus
return self.baseGetX() + wobble++;
},
}),
);
});
}
harden(WobblyPointLayer);

Expand Down