Giter Site home page Giter Site logo

stamp-specification's Introduction

Stamp Specification v1.6

Gitter Greenkeeper Badge

Introduction

This specification exists in order to define a standard format for composable factory functions (called stamps), and ensure compatibility between different stamp implementations.

Status

The specification is currently used by the following officially supported implementations:

Reading Function Signatures

This document uses the Rtype specification for function signatures:

(param: Type) => ReturnType

Composable

interface Composable: Stamp|Descriptor

A composable is one of:

  • A stamp.
  • A POJO (Plain Old JavaScript Object) stamp descriptor.

Stamp

A stamp is a composable factory function that returns object instances based on its descriptor.

stamp(options?: Object, ...args?: [...Any]) => instance: object
const newObject = stamp();

Stamps have a method called .compose():

Stamp.compose(...args?: [...Composable]) => Stamp

When called the .compose() method creates new stamp using the current stamp as a base, composed with a list of composables passed as arguments:

const combinedStamp = baseStamp.compose(composable1, composable2, composable3);

The .compose() method doubles as the stamp's descriptor. In other words, descriptor properties are attached to the stamp .compose() method, e.g. stamp.compose.methods.

Overriding .compose() method

It is possible to override the .compose() method of a stamp using staticProperties. Handy for debugging purposes.

import differentComposeImplementation from 'different-compose-implementation';
const composeOverriddenStamp = stamp.compose({
  staticProperties: {
    compose: differentComposeImplementation
  }
});  

Descriptor

Composable descriptor (or just descriptor) is a meta data object which contains the information necessary to create an object instance.

Standalone compose() pure function (optional)

(...args?: [...Composable]) => Stamp

Creates stamps. Take any number of stamps or descriptors. Return a new stamp that encapsulates combined behavior. If nothing is passed in, it returns an empty stamp.

Detached compose() method

The .compose() method of any stamp can be detached and used as a standalone compose() pure function.

const compose = thirdPartyStamp.compose;
const myStamp = compose(myComposable1, myComposable2);

Implementation details

Stamp

Stamp(options?: Object, ...args?: [...Any]) => Instance: Object

Creates object instances. Take an options object and return the resulting instance.

Stamp.compose(...args?: [...Composable]) => Stamp

Creates stamps.

A method exposed by all stamps, identical to compose(), except it prepends this to the stamp parameters. Stamp descriptor properties are attached to the .compose method, e.g. stamp.compose.methods.

The Stamp Descriptor

interface Descriptor {
  methods?: Object,
  properties?: Object,
  deepProperties?: Object,
  propertyDescriptors?: Object,
  staticProperties?: Object,
  staticDeepProperties?: Object,
  staticPropertyDescriptors?: Object,
  initializers?: [...Function],
  composers?: [...Function],
  configuration?: Object,
  deepConfiguration?: Object
}

The names and definitions of the fixed properties that form the stamp descriptor. The stamp descriptor properties are made available on each stamp as stamp.compose.*

  • methods - A set of methods that will be added to the object's delegate prototype.
  • properties - A set of properties that will be added to new object instances by assignment.
  • deepProperties - A set of properties that will be added to new object instances by deep property merge.
  • propertyDescriptors - A set of object property descriptors used for fine-grained control over object property behaviors.
  • staticProperties - A set of static properties that will be copied by assignment to the stamp.
  • staticDeepProperties - A set of static properties that will be added to the stamp by deep property merge.
  • staticPropertyDescriptors - A set of object property descriptors to apply to the stamp.
  • initializers - An array of functions that will run in sequence while creating an object instance from a stamp. Stamp details and arguments get passed to initializers.
  • composers - An array of functions that will run in sequence while creating a new stamp from a list of composables. The resulting stamp and the composables get passed to composers.
  • configuration - A set of options made available to the stamp and its initializers during object instance creation. These will be copied by assignment.
  • deepConfiguration - A set of options made available to the stamp and its initializers during object instance creation. These will be deep merged.

Composing Descriptors

Descriptors are composed together to create new descriptors with the following rules:

  • methods are copied by assignment
  • properties are copied by assignment
  • deepProperties are deep merged
  • propertyDescriptors are copied by assignment
  • staticProperties are copied by assignment
  • staticDeepProperties are deep merged
  • staticPropertyDescriptors are copied by assignment
  • initializers are uniquely concatenated as in _.union().
  • composers are uniquely concatenated as in _.union().
  • configuration are copied by assignment
  • deepConfiguration are deep merged
Copying by assignment

The special property assignment algorithm shallow merges the following properties:

  • The regular string key properties obj.foo = "bla"
  • The Symbol key properties obj[Symbol.for('foo')] = "bla"
  • The JavaScript getters and setters { get foo() { return "bla"; }, set foo(val) { ... } }
Deep merging

Special deep merging algorithm should be used when merging descriptors.

Values:

  • Plain objects are deeply merged (or cloned if destination metadata property is not a plain object)
  • Arrays are concatenated using Array.prototype.concat which shallow copies elements to a new array instance
  • All other value types - Functions, Strings, non-plain objects, RegExp, etc. - are copied by reference
  • The last value type always overwrites the previous value type

Keys:

  • The Symbol object keys are treated as regular string keys
  • The JavaScript getters and setters are merged as if they are regular functions, i.e. copied by reference

Priority Rules

It is possible for properties to collide, between both stamps, and between different properties of the same stamp. This is often expected behavior, so it must not throw.

Same descriptor property, different stamps: Last in wins.

Different descriptor properties, one or more stamps:

  • Shallow properties override deep properties
  • Property Descriptors override everything

Stamp Arguments

It is recommended that stamps only take one argument: The stamp options argument. There are no reserved properties and no special meaning. However, using multiple arguments for a stamp could create conflicts where multiple stamps expect the same argument to mean different things. Using named parameters, it's possible for stamp creator to resolve conflicts with options namespacing. For example, if you want to compose a database connection stamp with a message queue stamp:

const db = dbStamp({
  host: 'localhost',
  port: 3000,
  onConnect() {
    console.log('Database connection established.');
  }
});

const queue = messageQueueStamp({
  host: 'localhost',
  port: 5000,
  onComplete() {
    console.log('Message queue connection established.');
  }
});

If you tried to compose these directly, they would conflict with each other, but it's easy to namespace the options at compose time:

const DbQueue = compose({
  initializers: [({db, queue}, { instance }) => {
    instance.db = dbStamp({
      host: db.host,
      port: db.port,
      onConnect: db.onConnect
    });
    instance.queue = messageQueueStamp({
      host: queue.host,
      port: queue.port,
      onConnect: queue.onConnect
    });
  }]
});

myDBQueue = DbQueue({
  db: {
    host: 'localhost',
    port: 3000,
    onConnect () {
      console.log('Database connection established.');
    }
  },
  queue: {
    host: 'localhost',
    port: 5000,
    onConnect () {
      console.log('Message queue connection established.');
    }
  }
});

Initializer Parameters

Initializers have the following signature:

(options: Object, { instance: Object, stamp: Stamp, args: Array }) => instance?: Object
  • options The options argument passed into the stamp, containing properties that may be used by initializers.
  • instance The object instance being produced by the stamp. If the initializer returns a value other than undefined, it replaces the instance.
  • stamp A reference to the stamp producing the instance.
  • args An array of the arguments passed into the stamp, including the options argument.

Note that if no options object is passed to the factory function, an empty object will be passed to initializers.

Composer Parameters

Composers have the following signature:

({ stamp: Stamp, composables: Composable[] }) => stamp?: Object
  • stamp The result of the composables composition.
  • composables The list of composables the stamp was just composed of.

Note that it's not recommended to return new stamps from a composer. Instead, it's better to mutate the passed stamp.


Similarities With Promises (aka Thenables)

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

Contributing

To contribute, please refer to the Contributing guide.

stamp-specification's People

Contributors

danielkcz avatar ericelliott avatar fredyc avatar greenkeeper[bot] avatar jonathan-soifer avatar koresar avatar taktran avatar troutowicz avatar unstoppablecarl avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

stamp-specification's Issues

Code review of stamp based project

Based on discussion with @ericelliott in #70.

As I mentioned before, I've been using stamps for years -- since before my book was published, and I have never needed dedupe. I would suggest to you that perhaps if you think you need it, you're making your stamps too big -- piecing together things that maybe shouldn't be pieced together in the first place?

Maybe it's better to make smaller, independent stamps rather than merging together some large, complex stamp.

I don't suppose you have some small open code project using stamps more in real life? So far it's more about theory and everyone's interpretation on how to use stamps. It would be really nice to see some more professional approach.

I have this little project I've been writing lately and I've been learning stamps with it. Almost everything is stamp there. It's lacking documentation quite a lot yet, but it's almost 100% test covered. I am not expecting full code review, but some pointers about what do you consider overuse of stamps perhaps?

instance attr is not required for initializers

Initializers have the following signature:
(options, { instance, stamp }) => instance

Instance should be passed as "this" so not needed there. this makes possible to lose the extra object altogether:

initializer(options, stamp)

Pass options to stamp instead of baseObject

It's probably better for general purpose and long-term API stability to pass an options argument as the first parameter of a stamp instead of a baseObject. Then you could do:

const myStamp = compose(events, behaviors, httpStream);
myStamp({ baseObject: foo, httpStream: { url: streamUrl } });

To get at the baseObject in the factory, we just add braces around the { baseObject } parameter. Easy.

Comments?

Set instance.prototype.constructor?

There seems to be at least one interesting use-case for granting access to the stamp from the instance.

Indeed, in Stampit, we did have the notion of creating a "self-aware stamp", which is trivial to achieve with the spec as it's currently agreed to:

const selfAware = init((options, { instance, stamp }) => {
  const proto = Object.getPrototypeOf(instance);
  proto.constructor = stamp;
});

This could be part of the spec... however, it has the same reliability problem as instanceof and Constructor.prototype, since the value is mutable after object instantiation.

Initialiser instance replacement conditions

I want my stamp to return null in some cases:

const stamp = compose({ initializers: [({ condition }) => {
  if (!condition) {
    return null;
  }
}]});

let myObject = stamp({ condition: false }) || otherLogicGoesHere();

Current specs say:

If the initializer returns a different object, it replaces the instance.

And I think this is correct. I consider NaN, 0, '' and null objects. We should be more clear on that:

If the initializer does not return undefined, the returned value replaces the instance.

Agree?

`compose()` function as optional extra

The compose() king of the kings function bothers me more and more as I think of stampit 3.0.
TL;DR: it should be "optional extra" part of the standard.

Take promises A+ as an example (again). :)
You might know that it does not say how you must create your objects, it just describes compatibility mechanism between many different promise implementations.
It should be Composables aim too.

Composables standard should promote compatibility. Whereas it should not enforce a module API.

I'm going to supply a PR which changes the README.md accordingly.

Does single stamp needs multiple initializers?

I am wondering if it wouldn't be worth it have singular initializer along the initializers. Considering that stamps are usually rather small, might be rather rare to actually have multiple initializers there. Can we talk about some use cases where it might be beneficial to split initializer code into multiple functions within single stamp?

`assign` implementation

Currently, ES5 getters and setters aren't usable in the spec due to Object.assign being defined to merge objects literals instead of Object.defineProperties. ES6 exacerbates this problem because it makes using getters and setters so much easier with object literal shorthand.

@koresar hit on the subject of getters and setters being a problem via convertConstructor with ES6 classes in #6. However, I think the problem is larger and not restrained to ES6 classes. I run into the problem when converting a standard encapsulation to stampit e.g.

// before
let point = function({ x, y }) {
    return {
        get len() {
            return Math.sqrt(x * x + y * y);
        }
    };
};

// after
let point = stampit()
    .init(function() {
        let x, y;
        return stampit.assign(this, {
            coords(newX, newY) {
                x= newX;
                y= newY;
                return this;
            },
            get len() {
                return Math.sqrt(x * x + y * y);
            }
        });
    });

// Uncaught TypeError: Cannot read property 'len' of undefined
point().coords(2, 2).len

My thoughts are that we need to be able to handle this in both methods and stampit.assign and define assign in the spec as using Object.defineProperties.

propertyDescriptors new name?

I'm looking for a better name for the propertyDescriptors.

First of all we don't really want people to use this feature directly. assign and merge are more than enough in 99.9% cases.
Although, a convertClass (aka convertConstructor) would need propertyDescriptors to implement various scenarios, like getters and setters.
However, this might lead to unpredictable undesirable dehaviors (see node.js url object numerous problems). Also, it complicates the standard very much.

Thus, I see three options:

  • Leave propertyDescriptors name as is.
  • Rename to props.
  • Drop the propertyDescriptors altogether.

Dunno which way is better. There are 3 Cons to have this. And only one Pro.

Any proposals?

stamp.compose.configuration - what's that?

@ericelliott I'm trying to get my head around configuration

configure(options...) => stamp: Creates a new stamp with stamp options. e.g. descriptor presets. Users can use this to implement custom stamp types via initializer.

Could you please elaborate on that? I do not understand the purpose. What that for? Any examples maybe?

Out of sync docs

I think the docs for stamps init methods is out of date.

This section suggest that init methods are called with one argument, which is, an object containing 3 properties "instance", "stamp" & "args".

While this section ( @stamp-specification repo ) suggest that init methods are called with two arguments, the first is "options"and the second is the same as the one described in the previous paragraph.

To make things even more clear, the first section describes this signature:

( { instance: Object, stamp: Stamp, args: Array }) => instance: Object

While the second describes this signature:

(options: Object, { instance: Object, stamp: Stamp, args: Array }) => instance: Object

Is this intentional? And if so, does it mean stampit version 2.x doesn't follow the specs ?

Remove identical intializers

This is not the first time I'm having the following problem.

  • Stamp1 have a CPU expensive "init" function.
  • Stamp2 is using Stamp1 (Stamp2 = compose(Stamp1, ...)
  • Stamp3 is using Stamp1 to (Stamp3 = compose(Stamp1, ...)
  • Stamp4 is using Stamp1 and Stamp2 (Stamp4 = compose(Stamp1, Stamp2, ...)
  • Finally, Stamp5 is using all of them: Stamp5 = compose(Stamp1, Stamp2, Stamp3, Stamp4, ...)
    The problem: the expensive "init" is called five times. This is my production example.

Proposal: remove duplicate init functions from the .initializers[] array.

The change will touch this single line:

dstDescriptor.initializers = _.uniq(...

The lodash "uniq" function removes the same items form the back. In other words:

_.uniq([1, 2, 3, 1]) ==> [ 1, 2, 3 ]

Any caveats I'm missing here?

The new shared "protected" variable inside intializers

Previously, the only way to share data across stamps were the public properties. From time to time it is necessary to share data between stamp privately.

This is easy to achieve with one more variable passed to each initializer - shared. It should be a simple object {}.

const SetSecretKey = compose({
  initializers: [({ key }, { shared }) => {
    shared.secretKey = key;
  }]
});

const SecureConnect = compose({
  initializers: [(options, { shared }) => {
    this.connect = () => connectToSecureCloud({ secretKey: shared.secretKey });
  }]
});

const SecureCloud = compose(SetSecretKey, SecureConnect);

const connection = SecureCloud('my secret').connect();

/cc @ericelliott @BauchBeinePoe
Original idea: stampit-org/stampit#199

Specify details on how are things deeply merged

This is very vague here as it was simply assuming use of lodash. However in case someone wants to make their own implementation, it should be clearly specified how this merge behaves. Concretely with stampit which now uses own merge function, it does clone functions as a set of properties instead of referencing them (see related issue).

I think that functions should be exception on deep cloning for sure. Do you have other opinion?

Might be also related to the Symbol support #86 which talks mainly about cloning Symbol when used instead of properties names. However Symbol reference should be kept intact even when used as value.

Concat Descriptor Properties (proposal)

Concat Descriptor Properties Proposal

Add descriptor properties:

  • concatProperties
  • concatStaticProperties

(naming consistent with deepProperties and deepStaticProperties).

Reasoning

The descriptor spec currently has many different properties on it for handling different types of composition. I think concatenating arrays when composing is a low level behavior equivalent in specificity of use case to deepProperties and deepStaticProperties. Array concatenation is the the only basic / low level way of composing I can think of that is absent in the descriptor spec.

Spec Changes

Add descriptor properties:

  • concatProperties
  • concatStaticProperties

Example Code

Created Branch:
https://github.com/stampit-org/stamp-specification/tree/concat-properties

Implemented proposed changes: https://github.com/stampit-org/stamp-specification/blob/concat-properties/examples/compose.js

Tested Use Case:

import compose from './examples/compose.js';

const Jsonable = compose({
    concatStaticProperties: {
        toJsonConverters: [],
        fromJsonConverters: [],
    },
    staticProperties: {
        addToJsonConverter(func) {
            console.log('z', this.compose.concatStaticProperties);
            this.compose.concatStaticProperties.toJsonConverters.push(func);
            return this;
        },
        addFromJsonConverter(func) {
            this.compose.concatStaticProperties.fromJsonConverters.push(func);
            return this;
        },
        // convert object instance to json
        toJSON(obj){
            return this.compose.concatStaticProperties.toJsonConverters.reduce(function(json, func, index, array){
                // return new or mutate json object
                return func(obj, json) || json;
            }, {});
        },

        // prepares options object then calls factory
        fromJSON(json){
            const converters = this.compose.concatStaticProperties.fromJsonConverters;

            var preparedOptions = converters.reduce(function(options, func, index, array){
                // return new or mutate options object
                return func(options, json) || options;
            }, {});

            return this(preparedOptions);
        },
    },
    initializers: function (opts, {stamp}) {
        this.toJSON = function () {
            let converters = stamp.compose.concatStaticProperties.toJsonConverters;
            let obj = this;
            console.log('toJSON converters', converters);
            return converters.reduce(function(json, func, index, array){
                // return new or mutate json object
                return func(obj, json) || json;
            }, {});
         };
      }

});

const A = compose({ properties: {A: 'A'} })
.compose(Jsonable)

// console.log(A.addToJsonConverter);
.addToJsonConverter(function toJA(obj, json) {
    json.A = obj.A + ' as JSON';
})
.addFromJsonConverter(function fromJA(options, json) {
    options.A = json.A.replace(' as JSON','');;
});

const B = compose({ properties: {B: 'B'} })
.compose(Jsonable)
.addToJsonConverter(function toJB(obj, json) {
    json.B = obj.B + ' as JSON';
})
.addFromJsonConverter(function fromJB(options, json) {
    options.B = json.B.replace(' as JSON','');;
});

const C = compose(A, B);

console.log('C', C.compose.concatStaticProperties);

var c = C();
console.log('c', c);
var json = c.toJSON();
console.log('c.toJSON()', json);

console.log('C.fromJSON()', C.fromJSON(json));

The `configuration` vs `deepConfiguration`

Up until now this was truly a mystery what is that thing good for. Now thanks to @koresar new episode on Fun with Stamps this has finally become bit more clear.

Perhaps I am still missing some details, but I think its wrong having both of them there. Let me quote here example from the article

const SomeDeepConfiguration = compose({deepConfiguration: {
  some: ['data']
}});
const MoarDeepConfiguration = compose({deepConfiguration: {
  some: ['moar data']
}});
const HaveBoth = compose(
  SomeDeepConfiguration, MoarDeepConfiguration);
console.log(HaveBoth.compose.deepConfiguration);

Now imagine that first stamp would just use configuration instead of deepConfiguration. Suddenly there is big confusion because the configuration is in other object. Obviously this is trivial example, but in case you have bunch more stamps that are composed together, it makes it not that useful as for some values you will check one property, for something else it will be in other one.

Would it make more sense to have just one configuration which would be deeply merged? I mean so far I haven't found use case where I would actually use configuration except collision-stamp, but in there I don't actually see the configuration to be used there, so I bit clueless.

Descriptor name consistency

  • references - A set of properties that will be added to new object instances by assignment.
  • deepProperties - A set of properties that will be added to new object instances by assignment with deep property merge.
  • staticProperties - A set of static properties that will be copied by assignment to the stamp.
  • propertyDescriptors - A set of object property descriptors used for fine-grained control over object property behaviors

One of these names is not like the others. I think references needs to be renamed to properties. Assign by reference is default behavior in JavaScript. I don't think we need to be so explicit about it in the name. deepProperties provides the distinction we need to tell them apart. In other words, I propose this, for consistency:

  • properties - A set of properties that will be added to new object instances by assignment.
  • deepProperties - A set of properties that will be added to new object instances by assignment with deep property merge.
  • staticProperties - A set of static properties that will be copied by assignment to the stamp.
  • propertyDescriptors - A set of object property descriptors used for fine-grained control over object property behaviors

Concat arrays inside deepProperties and deepStaticProperties

We should reconsider using the "lodash/merge" function. Let's just use mergeWith: https://lodash.com/docs#mergeWith

Let me copy&paste the example from the link above:

function customizer(objValue, srcValue) {
  if (_.isArray(objValue)) {
    return objValue.concat(srcValue);
  }
}

var object = {
  'fruits': ['apple'],
  'vegetables': ['beet']
};

var other = {
  'fruits': ['banana'],
  'vegetables': ['carrot']
};

_.mergeWith(object, other, customizer);
// โ†’ { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot'] }

So, my proposal: do not add any new Descriptor Properties. But replace the "merge" with the "mergeWith" code above.

  • Not inflating the specs.
  • Not complicating implementation.
  • I have never used arrays inside the ".merge.*" object, it used to be useless.
  • I'm not talking about "properties" or "staticProperties".
  • It's about deeply merged items - "deepProperties" and "deepStaticProperties".
  • Deep merging of two arrays currently happens index-by-index:
let deep1 = compose({deepProperties: { key: ["a", "b", "c"] } });
let deep2 = compose({deepProperties: { key: [42, 42] } });
let composed = compose(deep1, deep2);
console.log(composed.compose.deepProperties); // { key: [ 42, 42, 'c' ] }

In other words, array indexes are like POJO keys.

let deep1 = compose({deepProperties: { key: {0: "a", 1: "b", 2: "c"} } });
let deep2 = compose({deepProperties: { key: {0: 42, 1: 42} } });
let composed = compose(deep1, deep2);
console.log(composed.compose.deepProperties); // { key: { '0': 42, '1': 42, '2': 'c' } }

I find it useless. For the past 9 months since stampit v2 was released I have never ever used arrays inside the deepProperties just because of that.

  • However, I was constantly finding that array concatenation would help me from time to time.

Proposal: arrays inside the deepProperties and deepStaticProperties should be concatenated instead of merged.

What's the point of stamp({instance}) ?

There is minor issue that it uses the "magic-arg" antipattern. As you have already discussed in #26.

But the major issue is that how this is supposed to be generally useful? I assume you have some sort of use-case in mind, although I can't see it. Current spec also does not descibe any details.

Main problem is that whatever use-case you have in mind, is it really possible to do it safely without specifying non-trivial amount of restrictions what user code is allowed to do with instance attributes? Restrictions for both init functions and rest of the code.

And even when it's possible to spec some behaviour that does not break everything it touches, how will you describe the resulting behaviour to user?

Unless you have really well thought out handling of various interactions that can happen, it seems better to just remove it from spec.

The natural flow I see in current spec is that the 'options' arg of stamp is exactly same object given to initializers, without any modifications and fully user-controlled properties.

properties vs deepProperties priority

Consider the code:

let stampS = properties({
  a: { b: 'shallow' }
});
let stampD = deepProperties({
  a: { b: 'deep' }
});

console.log( stampS.compose(stampD) ); // 1 - what should be printed?
console.log( stampD.compose(stampS) ); // 2 - what should be printed here?

Allow metadata (descriptors) merging override

My mental model of stamps is following:

  • The compose() (and stamp.compose()) function is just the metadata collection functions. It does nothing else but collecting metadata.
  • Using the collected metadata user can create objects by calling the stamp().

Thus, the metadata collection is the corner stone of the stamps philosophy. We should not limit people on how they merge metadata.

I'm thinking of allowing people to overwrite the mergeComposable similar to the initializers way. In other words - if user specified a merging callback function and it returned non-undefined then use the returned value as a new descriptor. (Will provide code examples later.)

It's time to start version the specs

This repository requires versioning using SemVer.

Proposal. Start with 0.1. As soon as we feel ready we publish the v1.

I'll do that tomorrow in case of absence of a discussion. :)

Terminology proposal

Stampit and this stamp-specification are somewhat different in terminology used. I propose to unify the wording.

  • Composable Descriptor (aka Descriptor) is a meta data object which contains the information necessary to create an object instance. (copy&pasted from the README)
  • Composable is an object which has property .compose which is a Composable Descriptor.
  • Stamp - is both factory function and a Composable.

Thus, the following checks are becoming clear:

const isDescriptor = obj => _.isObject(obj);
const isStamp = obj => _.isFunction(obj) && _.isFunction(obj.compose) && isDescriptor(obj.compose);
const isComposable = obj => isDescriptor(obj) || isStamp(obj);

Is logic present here?

UPD: Updated the code above in accordance to the discussion below and the latest README.md.

Stamp descriptor property names

Currently proposed property names are:

  • methods - A set of methods that will be added to the object's delegate prototype.
  • references - A set of properties that will be added to new object instances by assignment.
  • deepProperties - A set of properties that will be added to new object instances by assignment with deep property merge.
  • initializers - A set of functions that will run in sequence and passed the data necessary to initialize a stamp instance.
  • staticProperties - A set of static properties that will be copied by assignment to the stamp.
  • propertyDescriptors - A set of object property descriptors used for fine-grained control over object property behaviors
  • configuration - A set of options made available to the stamp and its initializers during object instance creation. Configuration properties get deep merged.

And then we have stamp creation via the compose() function:

var stamp = compose({
  methods: ...
  assign: ...
  merge: ...
  ...
});

While implementing an sample implementation of the composables standard I found it very hard to rename the properties back and forth. assign <-> references, merge <-> deepProperties, etc.

Also, it would be harder for people to match those in the head every time they come across a descriptor.
Less is more.

It doesn't help with code readability either.

I believe we should not have two names for an essentially same thing.

Let's have single short unified name for each of them (intentionally plural as well as in Present Sinple tense).

  • methods
  • statics
  • assigns
  • merges
  • runs
  • configs
  • propertyDescriptors

Use smaller assign instead of lodash.assign

Another finding regarding bundle size.

At first it might seem that lodash.assign is winner here with 6.8K minified vs 7.6K for polyfilled Object.assign. However there are two main reasons why would I stick to Object.assign.

  • It's standardized so eventually need for polyfill may begone (essentially only IE11, old Safari and Android needs it)
  • It has support for Symbol included thus solving #86 in most cases. If lodash ever adds support for it, I am sure that size will go up much more.

Double initializer behaviour

I think the spec should clarify what happens when same initializer comes from different stamps:

  1. Initializer will be run several times.
  2. Double initializer will be removed during compose (based on === equality).

Looks like 1) is what happens now. Users need to be aware about it if this behaviour stays.

But seems 2) might be more useful for users - they don't need to explicitly guard against multiple executions.

Properties inside the descriptor.methods

In example implementation check this line:
https://github.com/stampit-org/stamp-specification/blob/master/examples/compose.js#L59

As you can see anything (like string and plain objects) can get into the descriptor.methods.

let stamp = compose({ methods: { s: 'my string', o: {} } });
console.log(stamp().prototype.s); // prints "my string"
console.log(stamp().prototype.o); // prints "{}"

And I think this is good. We should leave it as is.

Then why I'm creating this issue?
A while ago Eric Elliot was strongly against putting any kind of properties to the prototype, thus stampit filters non-methods out. Most likely he's chaged his mind.

What are other people's opinion?
@troutowicz @sethlivingston @unstoppablecarl

Replace "deepProps" with composable utility stamp

Looking at the usage of the "deepProps", and at the complexity it adds to both the standard and the implementation, I would propose to ditch the "deepProps" and "deepStaticProps". Replace it with a utility stamp(s) (aka composable behaviour(s)).

Thoughts?

deepConfiguration and configuration

So, yesterday I got the case when I needed an object in .configuration to be completely replaced (not merged!).

const ReactRef = compose({configuration: {React: window.React || global.React}});
const MyComponent = ReactRef.compose(bla bla bla);

//... later on in another file ...
import React from 'react';
const MyFinalComponent = MyComponent.compose({configuration: {React: React}}).compose(other stuff);

I don't want the React to be deeply merged because they can be different instances/versions.

Proposal: have deepConfiguration which would be deeply merged. And configuration for assigning.

Proposal: rename or alias `compose()` to `blend()`

Proposal: use more accurate and a less confusing word for the compose() function.

The core of this specification is the compose() function. However, it is

  • slightly related to the object composition, and
  • not at all related to the function composition.

Almost every person out there confuses what that compose() function actually is. I have a dozen of examples. As well as many face to face discussions.

What the compose() function actually does is it merges the descriptors using the standardized algorithm. We could rename the function merge() instead. However... We can't use this word. In JavaScript it means object (deep/shallow) merging.

But the word BLEND is unique to software development.

Pros:

  • The blend() word sounds exactly what the compose() function is actually doing - blends stamps.
  • There will be much less confusion among software dev gurus, and less argument out in the Internets.
  • The blend() is shorter. :)))))))))))))

Cons:

  • This specification would need to be released as v2.0.0 since it's a breaking change.

The "composers" proposal

There are many issues we can solve with that proposal. But first let's list the issues.

  1. What if I want to separate behaviors to be composable: one to collect all the stamps a final stamp is composed with, and another one to make sure there are no method name collisions.

Surely, both can be solved with the infected compose. But we cannot easily double infect. Second infection must know that first infection exists to make sure both of them work. Basically, infection is great when there is only one. The second infection typically overwrites (removes) first infection unintentionally.

So, if you want to have two infections - the collectStampsCompose and the detectOverridesCompose - you might get only one.

  1. If an initializer overrides an object instance (e.g. with a function), then the next initializer(s) in the chain might not be ready for that. So, in that case I want to enforce my initializer to be last in the initializers array.

So, sometimes it is really needed to have an initializer either to be invoked the first or the last.

  1. I found that typically when someone is using infected compose all she/he want is to track composition. Like, collect all the composables a stamp was created from. Infected compose is a difficult concept to understand. People are really struggling with it. We should provide a simple solution.

  2. Also, I'm seeking for a way to replace "class and property decorators" ECMAScript proposal with a composable behavior (aka stamp). This would be a huge win towards a proposal to the TC39.

  3. ... other composition-controlling logic.

You might have noticed, that the default metadata merging algorithm does not suffice the above needs. Each example requires the merging logic to act a little different. This little difference can be part of the stamp's metadata.

The proposal

Have one more metadata - composers. It should be an array of functions (just like the initializers). Stamp.compose.composers.

The composers should be invoked sequentially (one by one, just like the initializers) at the time the metadata composition happens.

The composers function examples (really not final, help me here please):
1)

/**
@param {Composable[]} composables - the list of composables which will be merged together.
@param {Stamp} stamp - the resulting stamp made of the `composables` array.
*/
function (composables, stamp) {
  return undefined; // means this composer does not change the resulting stamp
  return something; // means this composer replaces the resulting stamp
}
/**
@param {Descriptor[]} descriptors - the list of descriptors which will be merged together.
@param {Descriptor} result - the resulting descriptor made of the `composables` array.
*/
function (descriptors, result) {
  return undefined; // means this composer does not change the resulting metadata
  return something; // means this composer replaces the resulting descriptor
}
/**
@param {Descriptor} target - the new (resulting) descriptor
@param {Descriptor} descriptor - the descriptor which should be merged into the `target`
*/
function (target, descriptor) {
  return undefined; // means this composer does not change the resulting target
  return something; // means this composer replaces the resulting target
}

Currently, I like the number 3 as the most flexible and simple of all.

Ask your questions, please.

Find a less bulky alternative to mergeWith

Coming from investigation in stampit-org/stampit#212

Do we need to write something own or is there some implementation that doesn't take 24K of minified code? It's almost bigger than React!

First candicate that looks promising is https://github.com/KyleAMathews/deepmerge. It already has array merging included. However it's not maintained for 2 years and it has 8 open issues. Apparently there was attempt to change maintainer, but they lost interest. Could we take it under the wings?

It's quite puzzling how in general these deep merging libraries are about 1-2K in source code while lodash needs 24K minified ๐Ÿ˜–

Consistent Static Descriptor Property Naming

Consistent Descriptor Property Naming Proposal

Change names of descriptors to be more consistent.

Reasoning

The current 'Instance` and 'Static' descriptor names are inconsistent. Descriptor names should be made up of words ordered by importance (as much as possible) that describe it. This ordering should be consistent across all descriptor names.

Proposal

  1. For completeness all current and future 'Instance' descriptors will have an equivalent 'Static' descriptor with identical composition behavior used to compose properties onto the stamp object.
  2. Static descriptors will be named by prefixing 'static' to the equivalent 'Instance' descriptor name.

Changes To Current Descriptors

change deepProperties to propertiesDeep
change deepStaticProperties to staticPropertiesDeep

New Descriptor List

Core

  • methods
  • initializers
  • configuration

Instance

  • properties
  • propertiesDeep
  • propertyDescriptors

Static

  • staticProperties
  • staticPropertiesDeep
  • staticPropertyDescriptors

Avoiding collisions of stamp's options argument

So, now we have standardised instance and stamp properties of the stamp's "options" argument.

let myObject = myStamp(options);

I afraid we can get property name collisions (again) in the future. The following code is a WTF code:

let myStamp = init(({ instance, stamp }) => {
  console.log(instance); // will not print "I am instance" !
  console.log(stamp); // will print "I broke your code" !
});
let myObject = myStamp({ instance: "I am instance", stamp: "I broke your code" });

As long as we ditched "args" array in favour of "options" object (so that "convertConstructor" became just one more behaviour - Yay! this is so cool!) I believe we can and should afford two arguments to stamps and three to init functions:

let myStamp = init(({ opt1, op2 }, instance, stamp) => {
  console.log(instance); // will print "I am instance" as expected
  console.log(stamp); // will print the stamp object as expcted
});
let myObject = myStamp({ opt1: 1, op2: 2 }, "I am instance");

Justification:

  • No name collisions!
  • From my practice it is very rare when one need to mutate an existing object.
  • It is even more rare occasion when one need a stamp reference (third argument).
  • The syntax of the init functions become simple and easy to read - what you pass (to the stamp factories) is what you get straight in the init function. KISS ftw!
let myStamp = init(({ opt1, op2 }) => {
  // ...
});
let myObject = myStamp({ opt1: 1, op2: 2 });
  • In the future we can always use ES6 Symbols to pass instance and stamp as part of the options object.

Do you like it as much as I do?

Utility functions should not be part of the standard

We should keep the standard as short as possible. Less is more.
Here is a good example of a perfect specification: https://promisesaplus.com/
Basically, it defines the .then() functions and it's two arguments.

The utility functions described in the original stampit issue should be purely implementation details.

Composables standard should define two things:

  1. .compose() (and compose()) functions and its arguments.
  2. The stamp descriptor object structure.

Discuss.

Should static methods/props be assigned to a stamp's prototype?

This is an open question. I have no strong opinion on that yet.

Sometime some tools check prototype of a class/function/component/stamp/etc. For example:

function isReactComponent(component) {
  return typeof component.prototype.render === 'function';
}

This would always return false for regular stamps. There is no standard way to attach anything to a stamp's prototype.

There are two solutions to this:

  1. Standardize that methods are also assigned as a stamp's .prototype. Or,
  2. Do not change anything and advise people to use infected compose for that need.

Currently, I'm inclined towards the second solution.

It would be great to hear React devs opinion.

Should options default to empty object?

Without default it means that every initializer that wants to access options object has to take care of it on its own. Imagine using stamps in some bigger application with many initializers. Should really each one of them do it again and again?

const MyStamp = compose({
    initializers: [({ mydata } = {}) => {
        ...
    }]
});

MyStamp();

Frankly it already looks less readable with so much brackets. Initializer has to validate mydata anyway, so why not lift this small burden and give there a little insurance that object will be there if nothing was passed in?

Also if using anonymous fat arrow functions, the stack trace would be actually somewhat unhelpful locating which initializer is actually failing. With named functions it's bit easier, but that heavily depends on how good name has been used.

TypeError: Cannot read property 'mydata' of undefined
    at Object.stampit.default.compose.initializers (pen.js:5062:29)
    at http://s.codepen.io/boomerang/f4a63016cac103fc2fbac879211b8a111464779436316/index.html:4795:43
    at Array.reduce (native)
    at Stamp (http://s.codepen.io/boomerang/f4a63016cac103fc2fbac879211b8a111464779436316/index.html:4794:69)
    at pen.js:5067:5

See the Pen Playground for stampit by Daniel K. (@FredyC) on CodePen.

<script async src="//assets.codepen.io/assets/embed/ei.js"></script>

In case there is a really need to use something else than a plain object, it's still possible to pass it to factory anyway given that all initializers in the mix can handle that properly. I think it's good to promote the pattern of using options object with this.

spec's test suite

Quoting @troutowicz

Something I'd like to see before v1.0 is the ability to import the spec's test suite into a spec related project. Right now I'm using a copied version for react-stamp. If I recall right, the only change I required was a different compose import path in each test file.

I'll implement that as part of this project. JFYI this project is going to be published to the NPM, and the test suite is the main reason to do that.

Pass configuration to initializers

Should we pass configuration to the initializers?

compose({ 
  initializers({ configuration }) {
    console.log(configuration);
  }
});

Otherwise, how do I use the configuration? At what point of time?

The king of the kings - the `compose()` function

A single compose() function should serve the following:

  1. Create stamps from other composables.
compose(composable1, composable2);
  1. Create stamps from discriptors.
    2.1) Descriptor is the argument(s) you pass to the compose(). Exactly like stampit(arg). Example:
compose({
  methods: ...
});

In the code above we are calling the compose() function with a single argument which is a descriptor object.

  1. Can be attachmed to any object to make the object a composable.
var anObject = {};
anObject.compose = compose;

In the code we are making a usual object a composable.


How about that?

Controlling the Stamp Composition (Proposal)

Original idea: Christopher
(IIRC I was talking about it somewhere sometime too.)

Carl's proposal: stampit-org/stampit#188 (comment)

Add .initStamp() method to stamps. It behaves exactly the same as .init() as far as setting, storing, and composing goes (internally stored as an array of functions, these arrays are concatenated when composing stamps). When a stamp is composed / created, after all other composition behavior (including merging the initStamp array), each function in the initStamp array is called on the resulting composed stamp. Each function is expected to return the same or new stamp to be passed to the next function in the array etc. No intermediate stamp objects are created maintaining non-enumerable properties on the stamp.

I use stamps. Very often (literally very) there is the need to debug/analyze a stamp's composition process/chain/order. See full list here.

Also, this will allow us to implement the last feature Traits have but stamp doesn't. It can be done in a flexible fashion - as a composable stamp/behavior.

In addition to extending the specification we should also develop few utility stamps which help with debugging/analysis of a stamp.

  • Track the list of stamps composed - trackComposition stamp
  • Track/Protect/Reject a property/method overlapping - trackOverlap stamp
  • Find duplicate stamp composition - trackDuplication stamp
  • etc

Sample code:

const logComposition = compose({ initStamp({left, right, result}) { // three descriptors
  console.log('LEFT', left.properties);
  console.log('RIGHT', right.properties);
  console.log('RESULT', result.properties);
}});

const A = { properties: { a: 1 } };
const B = { properties: { b: 2 } };
const C = { properties: { c: 3 } };
logComposition.compose(A, B, C);
// Will print:
// LEFT { a: 1 }
// RIGHT { b: 2 }
// RESULT { a: 1, b: 2 }
// LEFT { a: 1, b: 2 }
// RIGHT { c: 3 }
// RESULT { a: 1, b: 2, c: 3 }

Back to basics with stamp/initializer args

Discussion in #35 made me think about argument passing in general. And have a proposal:

  • Instance is passed to initializers as this.
  • The stamp is visible as instance.constructor. Simplest way to get there is use stamp.prototype as compose.methods. This makes instanceof work also.
  • The stamp factory function will take it's arguments and pass them to initializers directly. No need to extra values.

Note - all this does not change the fact that single options object is best way to pass args to multiple initializers. All examples showing the code should use that style. But now I see there is no need to hardcode that in spec - why should this spec disallow the React-style 3-arg stamps?

It's based on following ideas:

  • There is no need to support fat-arrow functions as initializers as there is better ES6 class-method syntax available for that: { init(){} }
  • There is no need to pass instance some other way than this.
  • There is no need to use different arg passing between stamp and initializers.
  • There is no need for magic keys in options or magic extra args that are specific to stamp launcher. All stamp args belong to initializers.
  • If there is existing JavaScript pattern for something, then it should be used instead of inventing ad-hoc patterns. (this and constructor and instanceof)

Here the "no need" means that although for each idea there is pattern that breaks it, there is no such pattern that must be supported by this spec. All the code that uses this spec will be new.

Stampit 1/2 used first arg as properties to be installed to instance, the rest were passed to initializers. Composables drop the idea of direct properties which is good. Now the one and only argument is purely for initializers. This proposal takes it logical conclusion.

Extending instance with other stamps on fly?

It feels little but strange that you can essentially add methods, statics and stuff in initializer, but you cannot actually compose any further. Even the fact that you have access to compose method itself and it does nothing might confuse some people.

function initializeNodeModel(nodeInstance, { stamp }) {
    stamp.compose(
        Getter('nodeInstance', nodeInstance)
    );
}

I tried also this, which might have been working, but it wont copy my Symbol properties (#86), so not much useful either.

function initializeNodeModel(nodeInstance, { instance, stamp }) {
    return stamp.compose(
        instance,
        Getter('behaviorNode', nodeInstance)
    ).create();
}

Maybe after adding support for Symbol this might work, but still it's bit insane that you have instance and you throw it away to make a new one. Perhaps I should just rethink the way I am doing this in here...

More control on composition

I wish there was a feature when I, as a stamp author, can get access to both left, right and resulting stamps in a single context.

Fanasy code:

const CompositionControl = compose({
  postCompose({left, right, result}) {
    if (left.methods.foo && right.methods.foo) { // Dummy implementation, don't pay attention please
      result.methods._foo1 = left.methods.foo;
      result.methods._foo2 = right.methods.foo;
      result.methods.foo = function(...args) { this._foo1(...args); this._foo2(...args); }
    }
  }
});

const FooStamp = CompositionControl.compose({ methods: {
  foo() { console.log('foo'); }
}});

const BarStamp = compose({ methods: {
  foo() { console.log('OVERRIDE'); }
}});

const Final = compose(FooStamp, BarStamp);

Final(); // will print both "foo" and "OVERRIDE"

This could be beneficial to handle/maintain/log collision issues.

Thoughts?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.