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

Remove identical intializers #70

Closed
koresar opened this issue Feb 19, 2016 · 57 comments
Closed

Remove identical intializers #70

koresar opened this issue Feb 19, 2016 · 57 comments

Comments

@koresar
Copy link
Member

koresar commented Feb 19, 2016

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?

@unstoppablecarl
Copy link
Contributor

This seems reasonable.

I personally cannot think of any case where you would want the same initializer repeated. If we can think of ANY case where you would want to repeat the same initializer function multiple times, this should not be added as there will be no way to undo the filtering. Can anyone think of such a case?

If there is a use case for repeating initializers I think the unique filter should be an option to the user not an assumed need.

Thinking out loud

Is there a case where you would want to consider 2 initializer functions equivalent in a way other than === ?

Implementation ideas


Implementing this behavior your self is extremely low effort.

let stamp = compose(A, B, C, D);
stamp.initializers = _.uniq(stamp.initializers);

Maybe change nothing and add documentation?


You could have it both ways:

Specify a way to identify which functions should be unique filtered. Such as considering an initializer unique if it is a named function or has a .isUnique property or something.

This is similar to the problem I ran into here with naming the composer functions #63 (comment)

@koresar koresar changed the title (Proposal) Remove identical intializers Remove identical intializers Feb 22, 2016
@ericelliott
Copy link
Contributor

Implementing this behavior your self is extremely low effort.

let stamp = compose(A, B, C, D);
stamp.initializers = _.uniq(stamp.initializers);

Maybe change nothing and add documentation?

This gets my vote. In fact, it's a good general rule of thumb. If something can be easily done in userland, it should not be added to the spec. Consider a utility stamp, instead.

@koresar
Copy link
Member Author

koresar commented Feb 26, 2016

👍

@koresar
Copy link
Member Author

koresar commented May 14, 2016

Hi @unstoppablecarl and @ericelliott

In this issue submitted by @FredyC (Daniel K.) it is described the same problem as I had. Please, take a thoughtful read of the issue.

I also wonder why only me and Daniel met that problem...

@danielkcz
Copy link
Contributor

danielkcz commented May 14, 2016

Hey, I am just starting to learn about stamps, but I cannot think of any use case where you might want to have same initializer called more than once. Among my first thoughts when I started using stamps was the actual question "what will happen if I add some stamp more than once in there?". I could not find satisfying answer anywhere so I just went with it as it seemed only logical that it should work that way. Since it works with everything else in the package, why initializers should be different?

let stamp = compose(A, B, C, D);
stamp.initializers = _.uniq(stamp.initializers);

Frankly this seems very hacky to me. What about doing that in opposite way? By default duplicate initializers would be filtered with a following function. Anybody can override that and change behavior if necessary.

stamp.initGuard = function(initializers) {
    return _.uniq(initializers);
}

Considering that there will be probably very low number of cases where you want to run initializer multiple times, a cost of increased API surface seems very low.

@ericelliott
Copy link
Contributor

I'm all for a uniqueInitialzers utility stamp mentioned in the documentation, but I don't see a compelling reason to complicate the spec for something so trivial to implement, which is also not always desired behavior. I can think of many use-cases where you might want the same initializer to run more than once.

@danielkcz
Copy link
Contributor

danielkcz commented May 14, 2016

I can think of many use-cases where you might want the same initializer to run more than once.

@ericelliott Can you talk about some of them?

As I said, it doesn't really feel consistent this way. Other parts of stamps like methods, refs, etc... can be composed multiple times and nothing wrong happens. Then suddenly there are initializers which can break stuff with multiple call.

but I don't see a compelling reason to complicate the spec for something so trivial to implement

Perhaps trivial, but very repetitive. Imagine you would do that for tens of stamps in your codebase. It can get rather tricky and error prone.

@koresar
Copy link
Member Author

koresar commented May 14, 2016

@FredyC In the future releases of the stampit module the hacky way you have mentioned is not only allowed, but even encouraged.

Stampit v3 will be implemented (actually it is already implemented, but not yet released) using the specifications from this repository. So that you can manipulate those initializers the way you want.

One of the ways to implement unique initializers is to override the ".compose()" static method. Here is an example.

const UniqueInitializersComposeOverride = compose({statics: {
  compose: function(...args) { // overriding the ".compose" function of any future stamp
    const stamp = this.compose(...args);
    stamp.compose.initializers = _.uniq(stamp.compose.initializers);
    return stamp;
  }
}});

const WhateverUniqueInit = 
  UniqueInitializersComposeOverride.compose(Whatever); // overriden compose()
const MyStamp = 
  compose(WhateverUniqInit, Foo) // the regular (not overriden) compose()
  .compose(Bar, Baz); // overriden compose()

const objInstance = MyStamp();

Please note, that this is not available in stampit v2.

Is the example understandable? (as it might be not, because I'm a lousy teacher)

@koresar
Copy link
Member Author

koresar commented May 14, 2016

@ericelliott That initializer will be invoked while stampit is iterating over the same array. So, you probably meant to mutate the .initializers array while we are iterating over it. Moreover, it should be passed the first, not the last.

const UniqueInitializersInitMutate = compose({initializers: [function(options, {stamp}) {
  const inits = stamp.compose.initializers;
  const length = inits && inits.length;
  for (let i = 0; i < length; i++) {
    const sameIndex = inits.indexOf(init[i], i + 1);
    if (sameIndex >= 0) {
      inits[sameIndex] = undefined; // mutating the array
    }
  }
}]});

const WhateverUniqueInit = 
  UniqueInitializersInitMutate.compose(Whatever); // must be first
const MyStamp = 
  compose(WhateverUniqInit, Foo) // must be first, and it is first
  .compose(Bar, Baz); // must be first, and it is first

const objInstance = MyStamp();

Correct?

This approach might can have side effects. Although, I am yet to think of one.

@danielkcz
Copy link
Contributor

danielkcz commented May 14, 2016

I don't know why all that hassle really. I still haven't seen any use case of why it might be useful to run initializer multiple times. Shouldn't default behavior be like something that is used in 95% of cases while 5% (need multiple invocation) can use hack? Sorry, but however supported it will be in v3, it still feel hacky. You need to search documentation and then figure out how to fit that in your code base.

@koresar
Copy link
Member Author

koresar commented May 15, 2016

Sorry, but however supported it will be in v3, it still feel hacky.

Stamps is a new paradigm which is hard to switch to, I know. I felt the same when stopped doing classes and started doing stamps. The things you're calling "hacky" are quite normal and encouraged in stamps.

You need to search documentation and then figure out how to fit that in your code base.

That's my bad. The UniqueInitializersComposeOverride and UniqueInitializersInitMutate should be part of the stamp-utils module (but under different names). The module is yet to be released. It would contain various helper stamps with detailed docs and examples. My apologies.

I still haven't seen any use case of why it might be useful to run initializer multiple times.

Me too @FredyC!

Several times I needed those initializers to be unique. And 0 times I needed them to run multiple times. Just hear the words - multiple initialization of the same thing. WAT?

@ericelliott could you please provide few examples of:

I can think of many use-cases where you might want the same initializer to run more than once.

@koresar
Copy link
Member Author

koresar commented May 16, 2016

Reopening as many people request that feature.

@koresar koresar reopened this May 16, 2016
@koresar
Copy link
Member Author

koresar commented May 18, 2016

This issue need more opinions.

I invite @JosephClay @sethlivingston @troutowicz @yasinuslu @zebulonj to express an opinion if you have one.

Thanks!

@zebulonj
Copy link

I don't have enough experience using Stamps on production scale projects to offer an opinion. Would be interested (pure curiousity) in an example use-case where you'd want to run the duplicate initializer more than once (as @ericelliot mentioned). I can't think of any off the top of my head, but—like I said—don't have much experience with Stamps in production-scale projects.

@ericelliott
Copy link
Contributor

I'm OK with dedupe by default. Seems to be the consensus.

@koresar
Copy link
Member Author

koresar commented May 19, 2016

If someone what to submit a PR - your very welcome.
Otherwise, I will do it ... sometime. :)

@troutowicz
Copy link
Member

It makes sense to dedupe if duplicates existing causes trouble more times than not.

@unstoppablecarl
Copy link
Contributor

unstoppablecarl commented May 19, 2016

As I said before when we discussed this: I think there should be a way to mark which initializers should be deduped and not.

On the other hand, no one has produced an example where you would need to run the same initializer multiple times so maybe it is not an issue.

@danielkcz
Copy link
Contributor

danielkcz commented May 19, 2016

As I said before when we discussed this: I think there should be a way to mark which initializers should be deduped and not.

I don't know, sounds like trying to prepare for something that might never happen. We are six people here with various level expertise. If none of us can come up with reason why it should be allowed, why not just do it easy way? Only if someone comes with real use case, expand it then instead of thinking way too ahead and creating suboptimal part of code because of that.

@ericelliott
Copy link
Contributor

If none of us can come up with reason why it should be allowed

Just because I don't have time to share examples doesn't mean I can't come up with them. =)

@koresar
Copy link
Member Author

koresar commented May 20, 2016

Btw, if one would need to avoid deduping then there are multiple ways to implement it yourself:

const initializer = function(...){...};

// 1
const MyStamp = compose({initializers: [
  function (...args) { return initializer.apply(this, ...args); } // make a wrapper of the function
]});

// 2
const MyStamp = compose({initializers: [
  initializer.bind() // make a copy of the function
]});

@koresar
Copy link
Member Author

koresar commented May 21, 2016

Please, merge: #89

@danielkcz
Copy link
Contributor

danielkcz commented May 21, 2016

Resolved in #89

@danielkcz
Copy link
Contributor

@koresar I believe that @boneskull has brough up other questions, deduplicating initializers is just current solution, but is it correct one?

@koresar
Copy link
Member Author

koresar commented Jul 26, 2016

Could be not. I'm ready for other solutions. Please, propose!

@danielkcz
Copy link
Contributor

Well I did...although it was crazy.

@boneskull
Copy link

boneskull commented Jul 26, 2016

So, whether this is right or wrong, in ES6 classes, you must explictly call your superclass's constructor. What if, when composing Stamps, initializer execution had to be explicit? Just tossing out ideas.

@danielkcz
Copy link
Contributor

@boneskull What do you mean by explicit execution of initializer? Can you show some example? Since there is no superclass here, you are ensured that initializers of all stamps will always called at least once.

If by explicit execution you mean to call some logic of other stamp, then you are better with method I guess.

@boneskull
Copy link

Yes, basically that upon composition, merging of init functions would be skipped. Then a stamp would only have one (1) initializer.

given you need a stamp to use it in composition, accessing the initializer is trivial:

const A = stampit().init(myFunc);
const B = stampit().compose(A).init(function() {
  A.fixed.init.apply(this, arguments);
});

Hope that looks right. Tough to code on a phone

@boneskull
Copy link

I'm not suggesting this as the syntax but would expect a more covenient api

@boneskull
Copy link

And yes I realize this clobbers the API but wonder if it doesn't address a design flaw.

@koresar
Copy link
Member Author

koresar commented Aug 1, 2016

To achieve that kind of behaviour I'd recommend Infected Compose.

import compose from 'somehwere';

function shadowImplicitInitializer(opts, specialOpts) {
  // Add one more special options
  specialOpts.inits = stamp.compose.configuration.explicitInit.inits;

  // Call the only initializer provided by the user
  return stamp.compose.configuration.explicitInit.theOnlyInitializer(opts, specialOpts);
}

function explicitInitCompose(...args) {
  args.push({ staticProperties: {
    compose: explicitInitCompose, // infecting stamps

    explicitInit: function (theOnlyInitializer) { // new API function
      return compose.apply(this, { // return a new stamp with an additional conf
        deepConfiguration: {
          explicitInit: { 
            theOnlyInitializer, // overwritten each time the explicitInit() called
            inits: [] // concatenated each time the explicitInit() called
          }
        }
      }
    }
  }});
  const Stamp = compose.apply(this, args); // creating an infected stamp

  // Check if we need to wrap the initializers
  if (!Stamp.compose.configuration.explicitInit.theOnlyInitializer) return Stamp;

  // moving original initializers functions to configuration
  let inits = Stamp.compose.configuration.explicitInit.inits;
  // remove the initializer wrapper if present
  inits = inits.filter(init => init !== shadowImplicitInitializer);
  // Take the actual initializers and move them to the configuration
  Stamp.compose.configuration.explicitInit.inits = inits.concat(Stamp.compose.initializers);

  // Removing actual initializers, place there our only initializer
  Stamp.compose.initializers = [shadowImplicitInitializer];

  return Stamp;
}

// Usage:
const RegularStamp = explicitInitCompose.init(fn1).init(fn2); // regular stamp
const SingleInitStamp = explicitInitCompose.explicitInit(
  function (opts, {inits}) { // accessing the initializers
    init[0].apply(this, arguments); // if needed
  }
);

// All sort of composition is possible
const MishMash = SingleInitStamp.compose(RegularStamp).compose(
  SingleInitStamp.explicitInit(function (opts, {inits}) { console.log(inits); })
);
compose(explicitInitCompose(), MishMash);
// etc

^ totally valid code. Not tested. Could be simplified (shortened) because I'm in a rush.

@koresar
Copy link
Member Author

koresar commented Aug 1, 2016

Another way could be editing the initializers array while first initializer is running:

const ExplicitInit = compose({
  staticProperties: {
    explicitInit: function(theOnlyInitializer) {
      // Adding the new API
      return compose.apply(this, { configuration: {theOnlyInitializer} });
    }
  },
  initializers: [function shadowImplicitInitializer(opts, specialOpts) {
    const {stamp} = specialOpts;
    // One more argument to the special options
    specialOpts.inits = stamp.compose.initializers.slice(1);
    // Remove all the initializers from the array object
    while(stamp.compose.initializers.length) stamp.compose.initializers.pop();
    // Call the initializer supplied by the user (TODO: check if it was provided)
    return stamp.configuration.theOnlyInitializer.apply(this, opts, specialOpts);
  }]
});

// Usage:
const SomeRandomStamp = compose(/* whatever */);
const SingleInitStamp = ExplicitInit.compose(SomeRandomStamp).explicitInit(
  function (opts, {inits}) {
    init[0].apply(this, arguments); // if needed
  }
);

This is a simpler implementation. The only caveat with this approach is that the ExplicitInit should be the very left of the compose stamps. I.e. its initializer should be executed the first.

@boneskull
Copy link

Thanks @koresar. Correct me if I'm wrong, but if you were to use "explicit" inits, you wouldn't run into the original problem, right?

Worth metioning is that init() keeps an Array of these functions; Stampit doesn't really expect the user to interact with the Array. But if it did, by exposing it via explicitInit(), we'd quickly see that an Array is going to be unwieldy, especially if no deduping took place. How do we know which function(s) in that Array to run?

If we were to somehow allow the user to give each init function a name, then the data structure will quickly get ugly. We'd need to consider what if the user didn't supply a name, duplicate names, cost of searching, etc., etc.

Ultimately, I'm not really sure that we need to do any of that, unless I'm missing something. What I'm saying is:

  • Leave .init() as-is.
  • Provide, say, .initOnce().
  • When you implement Stamp1, you use initOnce() for your expensive work; it stores the function in initOnceFunc.
  • Stampit's compose() function will ignore this value. This is key.

Given that when you're composing Stamp5, you have a reference to Stamp1 anyway, you can just call it:

import {Stamp1, Stamp2, Stamp3, Stamp4} from './stamps';
import {compose} from 'stampit';

const Stamp5 = compose(Stamp1, Stamp2, Stamp3, Stamp4)
  // might want to use initOnce() just in case, but let's throw caution to the wind
  .init(function () {
    Stamp1.initOnceFunc.apply(this, arguments);
    // maybe Stamp3 had one we wanted too?
    Stamp3.initOnceFunc.apply(this, arguments);
  });
  // also, all functions registered via init() in Stamp1, Stamp2, Stamp3 and Stamp4 
  // are called as usual. 

So, I don't understand why we'd need access to the inits Array if the explicit initialization function(s) we want to execute are already accessible in the scope. If Stamp1 wasn't available, you wouldn't have used it to compose Stamp5 anyway.

Again, a very rough API, but I wanted to be clear about what I meant--and this is just a wild idea, unless you think it's a good one, then I'm totally serious about it. 😄

@koresar
Copy link
Member Author

koresar commented Aug 1, 2016

Correct me if I'm wrong, but if you were to use "explicit" inits, you wouldn't run into the original problem, right?

Not right. The "compose" function would still concat initalizers without deduping.
However, I'm not sure what you are talking about. :)))

  • Provide, say, .initOnce().

Stamps are great because YOU CAN EXTEND THEM WITH ANY API YOU WANT.
For example, you can make them to have the .initOnce() as you suggest. And I even provided two different implementation of that .initOnce() idea.
I'm reluctant to introduce features which people can implement separately. Less is more.

Given that when you're composing Stamp5, you have a reference to Stamp1 anyway, you can just call it:

In your code example it is a regular function composition. I'd recommend to use functions for that instead of stamps. Otherwise, it's a heavy overcomplication.

Again, a very rough API, but I wanted to be clear about what I meant--and this is just a wild idea, unless you think it's a good one, then I'm totally serious about it. :)

Thanks for sharing with us!

I'd resume: stamp specification does not need to be extended in order to implement your wild idea. The feature can be implement as a composable behaviour.

@boneskull
Copy link

I think I got sidetracked a bit from my original point by talking about an API when we're supposed to be talking about a specification. I was kind of unsure why you wrote all that code...

Anyway, I believe what you're saying way up above is that we want to dedupe inits because:

  • There's no way to dedupe inits.
  • Nobody seems to need dupe inits.
  • If we dedupe by default, then someone who needs dupe inits can extend a stamp with this functionality.

That's fine, but I'm trying to get to why we have this problem in the first place. An attempt to solve it was "let's stop introducing dupe inits", so deduping is no longer necessary. I offered a way to do this without breaking how init() currently works, which was admittedly confusing. Am I making sense so far..?

@koresar
Copy link
Member Author

koresar commented Aug 1, 2016

Yeah. You're making total sense. 👍

Have I listed occasions when I actually needed deduped initializers? I think I did so somewhere above.

Another reason to dedupe could be performance concerns. But that's secondary.

@danielkcz
Copy link
Contributor

Well I must I got bit lost in here what we are actually trying to solve by all this. I mean trying took for different approach instead of deduplicating initiatilizers out of the box, that's obvious. However things like initOnce surely just adds confusion uncessarilly.

That's fine, but I'm trying to get to why we have this problem in the first place.

Sorry if I am wrong, but have you written some complex stamps on your own? As you have stated here, you are deliberately avoiding to compose stamp multiple times over. Perhaps if you start avoiding it, you will discover the need for deduping. I think it's nice feature of stamps, for OOP newcomer it looks like long wanted multiple inheritance.

@ericelliott
Copy link
Contributor

I still think if you need to dedupe, that's a code smell. Could you be using shared modules or dependency injection instead of trying to turn everything into a stamp?

Stamps aren't magical hammers that turn everything into a nail.

@danielkcz
Copy link
Contributor

@ericelliott I still see it as a main feature of stamps that you don't need be afraid to do that. I certainly don't want to assume that some high order stamp is composing what I need in some lower levels. I would rather explicitly compose it to be certain it's there.

On the other hand, I do not consider myself such a beginner with stamps now and to be honest I don't really have any example where running initializer multiple times could be harmful except performance reasons.

Btw, just out of curiousty, it's already almost 3 months when you've said:

I can think of many use-cases where you might want the same initializer to run more than once.

Still have no time to get at least one example together? ;)

@boneskull
Copy link

@FredyC

Sorry if I am wrong, but have you written some complex stamps on your own? As you have stated here, you are deliberately avoiding to compose stamp multiple times over. Perhaps if you start avoiding it, you will discover the need for deduping. I think it's nice feature of stamps, for OOP newcomer it looks like long wanted multiple inheritance.

I think you meant to say "if you stop avoiding it". Of course, if I didn't avoid it, given the way Stamps currently work, I'd need to dedupe. But given that I'd need to dedupe, I'm not sure why I'd do it, except...

I would rather explicitly compose it to be certain it's there.

So, this would be the "why"--but I'd consider this practice to be overly defensive (opinion).

I still think if you need to dedupe, that's a code smell.

I'm in agreement with the above, regardless of whether there's a use case for duplicate initializers.

@ericelliott
Copy link
Contributor

ericelliott commented Aug 1, 2016

I still see it as a main feature of stamps that you don't need be afraid to do that. I certainly don't want to assume that some high order stamp is composing what I need in some lower levels.

This isn't how composition is supposed to work. There should not be implicit dependencies between stamps. Ideally, stamps know absolutely nothing about each other, and when they do need to know about features of other stamps, those features are shared by common modules or dependency injected so that all your stamp dependencies are explicit.

Dependencies should always be explicit rather than implicit.

If you have implicit dependencies and/or duplicated init functions, I still argue that's a code smell. Fix the design, not the library.

I'm OK with deduping init by default, but I've never personally had this problem, and I've been using stamps longer than any of you. =)

@koresar
Copy link
Member Author

koresar commented Aug 2, 2016

I still think if you need to dedupe, that's a code smell.

@ericelliott could you please carefully read this and tell me what you think of the ArgumentChecker stamp?

UPD: I'll same you some time.

Without deduping this would create 3 identical initializers:

const SlackApi = compose(ArgumentChecker)
.checkArguments({team: 'string'}) // 1
.checkArguments({accessToken: 'string'}) // 2
.checkArguments({apiVersion: 'number'}) //3
.compose({initializers: [
  function ({team, accessToken, apiVersion}) {
    console.log(`${team}, ${accessToken}, v${apiVersion}`);
  }
]});

With deduping the above code adds only one argument checking initializer.

@danielkcz
Copy link
Contributor

danielkcz commented Aug 2, 2016

Dependencies should always be explicit rather than implicit.

I am not sure what exactly you call dependencies. For example the stamp that wraps EventEmitter.

export default const EventEmittable = stampit({
    initializers: function initEventEmitter() {
        Reflect.apply(EventEmitter, this, []);
    },
    methods: ['emit', 'on', 'once', 'removeListener', 'removeAllListeners'].reduce((methods, methodName) => {
        methods[methodName] = EventEmitter.prototype[methodName];
        return methods;
    }, {}),
});

This one is than composed explicitly in various other stamps to get those methods on it. Do you call that a code smell? Obviously it's not a nice to have a initializer of that called multiple times. Are you suggesting to create instance of that EventEmittable and pass it in options instead? That would mean you would have to manually expose those methods everywhere. It's probably possible that way, but you know, DRY isn't that stupid idea.

@ericelliott
Copy link
Contributor

@koresar ArgumentChecker is a perfect example of something that should not be a stamp. Each stamp should be responsible for knowing and checking its own arguments. Instead of making ArgumentChecker a stamp, you could very easily import a shared ArgumentChecker module into each component that needs it and pass the arguments and map.

Bonus: You'll colocate the required arguments/validation info with the stamp that actually uses them.

EventEmitter is also a great example of something that other stamps should not be aware of. If other stamps need to use the emitter API, you can explicitly pass it into the stamp using dependency injection.

Are you suggesting to create instance of that EventEmittable and pass it in options instead?

Yes. If it is problematic to do so, perhaps we could come up with a simplified API for making explicit dependency injection easier.

@ericelliott
Copy link
Contributor

Everybody please read "Mixins Considered Harmful" -- Dan Abramov's warnings about mixins apply to stamps.

Stamps are not a panacea. You should not try to use them for every little thing.

@ericelliott
Copy link
Contributor

ericelliott commented Aug 2, 2016

Note that I have no problem with doing stuff like this:

import EventEmitter from 'stamp-event-emitter';
import { compose, init } from 'stamp-utils';

const myStamp = compose(EventEmitter, init(options => {
  // use EventEmitter here
}));

The distinction here is that in this file, the dependency on EventEmitter is clear and explicit. However, if you exported myStamp independently of the event emitter, but it had dependencies on it, and then you later composed them together, that's where I have a problem. This use case is fine.

So... If you then compose this OK use case with another stamp that also uses EventEmitter, a dedupe option does make sense, which is why I'm fine with dedupe behavior.

What you need to watch out for is implicit dependency between modules. Your module should have an import statement or a dependency passed in for every stamp you make use of in your exported stamp.

Make sense?

@danielkcz
Copy link
Contributor

danielkcz commented Aug 2, 2016

Totally makes sense and that's the way how I am doing it. Whenever I am using some api of another stamp, I am explicity importing it and composing there. That's why I was bit confused by this comment of yours...

I still think if you need to dedupe, that's a code smell. Could you be using shared modules or dependency injection instead of trying to turn everything into a stamp?

So with explicit dependencies the dedupe is no longer code smell I assume?

Anyway regarding mixins ... it's bit unclear where mixins end and "right way" begins. Even if I explicitly state that this stamp composes EventEmitter and then I use addListener method, it's still isn't clearly stated that this method comes from that stamp. Especially if there much more stamps composed together, it requires some cognitive effort to realize where that method comes from (this is an easy use case).

@ericelliott
Copy link
Contributor

ericelliott commented Aug 3, 2016

So with explicit dependencies the dedupe is no longer code smell I assume?

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.

Even if I explicitly state that this stamp composes EventEmitter and then I use addListener method, it's still isn't clearly stated that this method comes from that stamp.

Let static analysis tools worry about that. =)

Especially if there much more stamps composed together

Again, if you're composing lots of stamps together, there's a good chance that maybe you're building a stamp that's too big.. doing too many things.

Keep in mind the single responsibility principle, the principle of high cohesion, and "The Single Biggest Mistake Programmers Make Every Day".

@ericelliott
Copy link
Contributor

Just because you can merge lots of stamps into one giant god stamp doesn't mean you should.

@danielkcz
Copy link
Contributor

danielkcz commented Aug 4, 2016

Hmm, I guess that makes sense, but it also makes one question true purpose of stamps. I mean if I went with small things, I could just write bunch of factory functions without anything extra to it. Stamps are essentially just adding bit of sugar syntax on specifying methods, properties, statics.

@ericelliott
Copy link
Contributor

I mean if I went with small things, I could just write bunch of factory functions without anything extra to it. Stamps are essentially just adding bit of sugar syntax on specifying methods, properties, statics.

Yes, indeed. I use them as sugar when I need to assign prototypes for fast property access (prototype property access triggers v8's fast object mode), as well as use private variables, privileged methods, and when I need to mix in a common APIs, such as event emitters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants