Fun with Stamps. Episode 4. Implementing stamps in 30 LOC

Vasyl Boroviak
6 min readMay 8, 2016

Hello. I’m developer Vasyl Boroviak and welcome to the fourth episode of Vasyl Boroviak presents Fun with Stamps.

Stamps core is very easy to implement. In this episode we will implement the “compose” function step by step ourselves.

For simplicity we will implement properties, methods, and initializers only. So that the following should work. Consider it’s our unit test:

const PropStamp = compose({properties: {p: 1}});
const MethodStamp = compose({methods: {m: 1}});
const InitStamp = compose({initializers: [function() {
console.log('init arg 1:', arguments[0]);
console.log('init arg 2:', arguments[1]);
console.log('init "this" context:', this);
}]});
const Stamp = PropStamp.compose(compose(MethodStamp, InitStamp));
const obj = Stamp({foo: 'bar'}, 42);
// should log the following
init arg 1: Object { foo: 'bar' }
init arg 2: { instance: { p: 1 },
stamp:
{ [Function: Stamp]
compose:
{ [Function]
properties: [Object],
methods: [Object],
initializers: [Object] } },
args: [ Object { foo: 'bar' }, 42 ] }
init "this" context: { p: 1 }
console.log('Stamp descriptor:', Object.assign({}, Stamp.compose));
console.log('Object instance:', obj);
console.log('Object proto:', obj.__proto__);
// should log the following
Stamp descriptor: Object {
properties: { p: 1 },
methods: { m: 1 },
initializers: [ [Function] ] }
Object instance: { p: 1 }
Object proto: { m: 1 }

“compose” function interface

According to the specification the compose function can accept any number of composable objects and return a new stamp. A composable object is nothing else but a stamp or a stamp descriptor (POJO).

function compose(...composables) {}

The function should merge all the arguments together to generate a new descriptor.

function compose(...composables) {
const newDescriptor = composables.reduce(mergeComposable, {});
}

I have just introduced the new function — mergeComposable.

Use the new descriptor to create a new stamp and return it:

function compose(...composables) {
const newDescriptor = composables.reduce(mergeComposable, {});
return createStamp(newDescriptor);
}

One more function was introduced — createStamp.

Merging composables

Let’s implement the mergeComposable function first. It will take two arguments:

function mergeComposable(dstDescriptor, srcComposable) {}

The dstDescriptor will be mutated and returned from the function because it’s what the Array.reduce function need.

function mergeComposable(dstDescriptor, srcComposable) {  return dstDescriptor;
}

Next step — convert the srcComposable to a descriptor.

function mergeComposable(dstDescriptor, srcComposable) {
const srcDescriptor = (srcComposable && srcComposable.compose) ?
srcComposable.compose : srcComposable;
if (!srcDescriptor) return dstDescriptor; // ignore rubbish
return dstDescriptor;
}

Done. Now, according to the specification, we must mixin the properties and the methods to the new (destination) descriptor. Initializers must be concatenated.

function mergeComposable(dstDescriptor, srcComposable) {
const srcDescriptor = (srcComposable && srcComposable.compose) ?
srcComposable.compose : srcComposable;
if (!srcDescriptor) return dstDescriptor;
dstDescriptor.properties = Object.assign(
dstDescriptor.properties || {}, srcDescriptor.properties);
dstDescriptor.methods = Object.assign(
dstDescriptor.methods || {}, srcDescriptor.methods);
dstDescriptor.initializers = (dstDescriptor.initializers || [])
.concat(srcDescriptor.initializers || []);
return dstDescriptor;
}

Creating a stamp out of a descriptor

The last function to implement is the createStamp. It takes a descriptor and returns a stamp.

function createStamp(descriptor) {
const stamp = createFactory(descriptor);
return stamp;
}

I’ve just introduced the new function — createFactory. We’ll get to it later.

The new stamp should have property compose with the descriptor properties attached to it. Let’s copy the original compose function and assign properties to it.

function createStamp(descriptor) {
const stamp = createFactory(descriptor);
stamp.compose = function() {
return compose.apply(this, arguments);
};
Object.assign(stamp.compose, descriptor);
return stamp;
}

Factory function internals

Now, let’s implement the last function for today — createFactory. It should accept a descriptor and return a naked factory function.

function createFactory(descriptor) {
return function Stamp() {
}
}

The Stamp function should return a new object with the prototype set to the descriptor.methods object.

function createFactory(descriptor) {
return function Stamp() {
const instance = Object.create(descriptor.methods);
return instance;
}
}

The properties of the object instance must be copied from descriptor.properties.

function createFactory(descriptor) {
return function Stamp() {
const instance = Object.create(descriptor.methods);
Object.assign(instance, descriptor.properties);
return instance;
}
}

Now, the last part — descriptor.initializers.

Each one must be called with the context set to instance. The arguments must be the options object — the first factory argument, and the special object bringing stamp instance, factory arguments (args), and the object instance itself. See italics text:

function createFactory(descriptor) {
return function Stamp(options, ...args) {
const instance = Object.create(descriptor.methods);
Object.assign(instance, descriptor.properties);
(descriptor.initializers || []).forEach(initializer => {
initializer.call(instance, options,
{stamp: Stamp, args: [options].concat(args), instance});
});
return instance;
}
}

`this` context of the `compose` method

Now, the hardest part. Please note that we are reusing the same compose function and attaching it to the Stamp object, so that it becomes a method.

function createStamp(descriptor) {
const stamp = createFactory(descriptor);
stamp.compose = function() {
return compose.apply(this, arguments); // Reusing the `compose`
};
Object.assign(stamp.compose, descriptor);
return stamp;
}

This means that the original compose function we wrote requires a light change. It should take the this context into account to make sure the following works:

const Stamp = PropStamp.compose(something);

When compose is a method the this context points to the stamp (PropStamp) itself. So, here is the new compose function.

function compose(...composables) {
const newDescriptor = [this].concat(composables) // `this` is first
.reduce(mergeComposable, {});
return createStamp(newDescriptor);
}

The final result

Let’s put it all together.

function createFactory(descriptor) {
return function Stamp(options, ...args) {
let instance = Object.create(descriptor.methods);
Object.assign(instance, descriptor.properties);
(descriptor.initializers || []).forEach(initializer => {
initializer.call(instance, options,
{stamp: Stamp, args: [options].concat(args), instance});
});
return instance;
}
}
function createStamp(descriptor) {
const stamp = createFactory(descriptor);
stamp.compose = function() {
return compose.apply(this, arguments);
};
Object.assign(stamp.compose, descriptor);
return stamp;
}
function mergeComposable(dstDescriptor, srcComposable) {
const srcDescriptor = (srcComposable && srcComposable.compose) ?
srcComposable.compose : srcComposable;
if (!srcDescriptor) return dstDescriptor;
dstDescriptor.properties = Object.assign(
dstDescriptor.properties || {}, srcDescriptor.properties);
dstDescriptor.methods = Object.assign(
dstDescriptor.methods || {}, srcDescriptor.methods);
dstDescriptor.initializers = (dstDescriptor.initializers || [])
.concat(srcDescriptor.initializers || []);
return dstDescriptor;
}
export default function compose(...composables) {
const newDescriptor = [this].concat(composables)
.reduce(mergeComposable, {});
return createStamp(newDescriptor);
}

~30 lines of code. All 4 functions are pure. Our unit test works:

const PropStamp = compose({properties: {p: 1}});
const MethodStamp = compose({methods: {m: 1}});
const InitStamp = compose({initializers: [function() {
console.log('init arg 1:', arguments[0]);
console.log('init arg 2:', arguments[1]);
console.log('init "this" context:', this);
}]});
const Stamp = PropStamp.compose(compose(MethodStamp, InitStamp));
const obj = Stamp({foo: 'bar'}, 42);
// should log the following
init arg 1: Object { foo: 'bar' }
init arg 2: { instance: { p: 1 },
stamp:
{ [Function: Stamp]
compose:
{ [Function]
properties: [Object],
methods: [Object],
initializers: [Object] } },
args: [ Object { foo: 'bar' }, 42 ] }
init "this" context: { p: 1 }
console.log('Stamp descriptor:', Object.assign({}, Stamp.compose));
console.log('Object instance:', obj);
console.log('Object proto:', obj.__proto__);
// should log the following
Stamp descriptor: Object {
properties: { p: 1 },
methods: { m: 1 },
initializers: [ [Function] ] }
Object instance: { p: 1 }
Object proto: { m: 1 }

Conclusion

The production ready implementation is three times larger (>100 LOC). But I hope you get the idea that stamps are simple, although, very-very new.

--

--