Fun with Stamps. Episode 1. Stamp basics

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



What are Stamps?

Stamps are an evolution in software development. After a decade of doing classic OOP and a couple of attempts to dive into functional programming (FP) I fell in love with stamps.

Many people feel that classic OOP is just bad. Some feel that FP is also not an answer.

Stamps are a new programming paradigm intended to be a better alternative to classic OOP and an addition to FP. Stamps are not limited to JavaScript, although only the JavaScript implementation exist to date.

Many have been amazed after embracing the concept. Here is what they say:

thanks for a library that makes javascript really fun :)
i’ve been using stamps for an internal project and it has made it easier to write reusable, expressive code
and it’s fun to use

Or:

every time i talk about stamps i get all excited and my colleagues have to settle me down

And one more quote from here:

Stamps are like mixins on steroids. They offer a great declarative API to create and compose your factories of objects (stamps) with baked in support for composing prototypes, mixing in features, deep copying composition and private variables.

All that is powered by these 80 lines of code.

So, what is it?

  • Stamps are composable (mixable?, combinable?) factory functions
  • Stamps are used as a replacement for classes
  • Stamps are an addition to functional programming
  • Stamps are a different software development paradigm

First example

Stamps are created using the special compose function or various utilities.

const EmptyStamp = compose(); // creating an empty stamp
const obj = EmptyStamp(); // it can create empty objects!
console.log(obj); // {}

Let’s make our stamp produce objects with the property foo.

const HasFoo = compose({
properties: {
foo: 'default foo!'
}
});
const obj = HasFoo();
console.log(obj); // { foo: 'default foo!' }

Now, let’s create a stamp which will produce objects with the method printFoo.

const PrintFoo = compose({
methods: {
printFoo() {
console.log(this.foo || 'There is no foo');
}
}
});
const obj = PrintFoo();
obj.printFoo(); // There is no foo

Boring, eh? But let’s compose those two:

const Foo = compose(HasFoo, PrintFoo);
const obj = Foo();
obj.printFoo(); // default foo!

Alternatively you can compose them like this:

const Foo = HasFoo.compose(PrintFoo);
// or
const Foo = PrintFoo.compose(HasFoo);
// or
const Foo = compose().compose(HasFoo).compose(PrintFoo);
// or
const Foo = compose(HasFoo).compose(PrintFoo);
// or
// ...well you get it :)

Factory arguments

We all want to pass arguments to our factories. Also, how do I insert my logic when an object is being created (aka constructor in classic OOP)? Here is how you do it:

const InitFoo = compose({
initializers: [function (foo) {
if (foo) this.foo = foo;
}]
});
const obj = InitFoo('incoming foo!');
console.log(obj); // { foo: 'incoming foo!' }

A bit long. But a shorter syntax is possible by using latest stampit or stampit v3.

import {init} from '@stamp/it';
const InitFoo = init(function(foo) {
if (foo) this.foo = foo;
});

Of course you can compose that stamp too:

const Foo = compose(HasFoo, PrintFoo, InitFoo);
const obj = Foo('incoming foo!');
obj.printFoo(); // incoming foo!

Stamps can have as many initializers as you want. Each initializer would receive the same set of arguments.

Statics — properties on stamp

You can declare the so called “static” properties. Those are attached to the stamp itself. The stamp below declares a single static property trace, which is a function:

const TraceStampMetaData = compose({
staticProperties: {
trace() {
console.log(`This stamp consists of ${this.compose}`);
}
}
});

And, of course, you can compose that stamp with other stamps. All derived stamps will have that property too:

const Foo2 = Foo.compose(TraceStampMetaData);
Foo2.trace(); // will print the Foo2 stamp meta-data. See below!

Breaking down a stamp into pieces

Each stamp has the .compose() method. It also serves as the meta-data holder. Let’s print the Foo stamp from above.

const Foo = compose(HasFoo, PrintFoo, InitFoo);
console.log(Foo);

Will print this:

{ [Function: Stamp]
compose:
{ [Function]
methods: { printFoo: [Function: printFoo] },
properties: { foo: 'default foo!' },
initializers: [ [Function] ] } }

Foo is the function which has the property .compose which is also a function and has properties .methods.properties, and .initializers.

You, the developers, are allowed and even encouraged to manipulate that meta-data! The data is called the “stamp descriptor” or just “descriptor”.

Objects created by stamps

Let’s take a look at the objects created by this stamp:

const obj = Foo();
console.log(obj); // { foo: 'default foo!' }

It is very similar to the “properties:“ line printed above. Right?

Let’s look at the prototype of the object:

console.log(Object.getPrototypeOf(obj));
// { printFoo: [Function: printFoo] }

Woah! That’s very similar to the “methods:”. Indeed, it is the “.methods” object itself:

console.log(Object.getPrototypeOf(obj) === Foo.compose.methods);
// true

Similarities with Promises (aka Thenables)

  • Thenable ~ Composable
  • .then().compose()
  • Promise ~ Stamp
  • new Promise(function(resolve, reject)) ~ compose(…composables)

When to use stamps?

Conclusion

You have just read everything you need to know to start using stamps. Although, this is only the tip of the iceberg.