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

Underscore does not follow SemVer #1684

Closed
wavebeem opened this issue Jun 13, 2014 · 32 comments
Closed

Underscore does not follow SemVer #1684

wavebeem opened this issue Jun 13, 2014 · 32 comments

Comments

@wavebeem
Copy link

It would be extremely useful if Underscore followed Semantic Versioning. It currently does not, as things like breaking bindAll and removing unzip have occurred without major version bumps.

This would allow library consumers to upgrade bugfixes and performance improvements and gain additional functions without fear of code breaking.

@akre54
Copy link
Collaborator

akre54 commented Jun 13, 2014

To quote jashkenas/backbone#2888 (comment):

Thanks, but strictly following "semantic" versioning wouldn't work out very well for Backbone. Given that the project is almost all surface area, and very little internals, almost any given change (patch, pull request) to Backbone breaks backwards-compatibility in some small way ... even if only for the folks relying on previously undefined behavior.

The rest of the comment is worth a read too, even if you disagree.

@akre54
Copy link
Collaborator

akre54 commented Jun 13, 2014

@jdalton
Copy link
Contributor

jdalton commented Jun 15, 2014

@akre54 I'm curious what your thoughts are on the fact that others projects like Lo-Dash (Underscore alternative) and ExosJS (Backbone alternative) can follow semver?

Since those drop-in replacements can follow semver doesn't that kind of throw a wrench in the excuse pushed by Underscore/Backbone core?

@akre54
Copy link
Collaborator

akre54 commented Jun 15, 2014

Couple things.

Lo-Dash, for the most part, sticks to the cowpaths paved by Underscore, so it has the advantage of holding off until an Underscore release to push a version bump for feature parity. The differences from Underscore tend to be more in the enhancements category and less in significant breaking changes.

Lo-Dash adds much more guard code for bc at the expense of code clutter while Underscore strives for terseness. It's easier to get away with a few lines of extra code when the library is larger to begin with (Lo-Dash clocks in at 8.7k lines vs 1.4k for Underscore.) There's also much more internal mucking that can be done in Lo-Dash since so much of the logic is internal.

It's also written disproportionately by one contributor (you), whereas Underscore's changes tend to come from a much more diverse set of contributors, meaning new features can come at any time, and sometimes big feature changes and backward-incompatible changes come at inopportune moments to a release schedule.

Anecdotally, I get the feeling Underscore also has the release-schedule disadvantage of being used in many more beginner projects than Lo-Dash (by its very nature, Lo-Dash tends to appeal to devs who need its power). A more advanced dev is going to be more comfortable dealing with the fallout from breaking changes, and may understand their reasoning a bit more.

Lastly, as the co-author and lead contributor of Exoskeleton, I'll be the first to tell you we haven't been following semver either. We also have the advantages of Lo-Dash as described above.

@jdalton
Copy link
Contributor

jdalton commented Jun 15, 2014

Lo-Dash, for the most part, sticks to the cowpaths paved by Underscore, so it has the advantage of holding off until an Underscore release to push a version bump for feature parity. The differences from Underscore tend to be more in the enhancements category and less in significant breaking changes.

What does that have to do with Underscore's version strategy? Lo-Dash bumps versions following semver which is why it's at v2.x going on v3.x.

Lo-Dash adds much more guard code for bc at the expense of code clutter while Underscore strives for terseness.

What does guard code or being terse have to do with version strategy?

It's easier to get away with a few lines of extra code when the library is larger to begin with (Lo-Dash clocks in at 8.7k lines vs 1.4k for Underscore.)

Lo-Dash has inline docs, LOC isn't relevant to versioning.

There's also much more internal mucking that can be done in Lo-Dash since so much of the logic is internal.

Can't the same be said for Underscore?

It's also written disproportionately by one contributor (you), whereas Underscore's changes tend to come from a much more diverse set of contributors, meaning new features can come at any time, and sometimes big feature changes and backward-incompatible changes come at inopportune moments to a release schedule.

That's why Underscore has maintainers to accept/reject or push off until a future release.
Again, not relevant to versioning.

Anecdotally, I get the feeling Underscore also has the disadvantage of being used in many more beginner projects than Lo-Dash (by its very nature, Lo-Dash tends to appeal to devs who need its power).

I disagree, Lo-Dash has thousands of dependents and all can't be experts with it.

A more advanced dev is going to be more comfortable dealing with the fallout from breaking changes, and may understand their reasoning a bit more.

Because Lo-Dash follows semver devs don't have to deal with fallout until they jump to a major version update. Unlike Underscore, Lo-Dash won't change from under their feet because they used a ~ for the package's version range.

Lastly, as the co-author and lead contributor of Exoskeleton, I'll be the first to tell you we haven't been following semver either.

Then you should remove that from its marketing.

I don't think there's any reason why Underscore couldn't follow semver.
Not following it is a disservice to your users 😦

@ericelliott
Copy link

If you want to be included in npm or bower, it's not up for debate. You must use semver. You're making an implicit promise to follow semver, and if you don't, that breaks people's code without warning, and that's frowned on by pretty much everybody.

We're talking about a choice that breaks production code. It's not cool to play a sloppy game with other people's time and money.

That means your version numbers get bigger faster. So what? It's a number. That's far better than breaking somebody's production shopping cart.

@oriSomething
Copy link

+1 for semver

@raganwald
Copy link

Personally, I don't think it's "cool" to get personal in a bug report. Either it does or it doesn't follow semantic versioning. Here are the reasons--bang, bang, bang--why it would be an even better library if it did. Here are the reasons--bang, bang--why it's feasible for the maintainers to increment the version number in a manner congruent with semver.

After that, it's up to the maintainers. If you disagree with their stance, there are alternative libraries to use.. You can fork the project (as others have done). You can write a blog post getting as personal as you please.

But let's try to have civil disagreement.

@ericelliott
Copy link

I don't understand @raganwald. Who's getting personal?

I'm not attacking anybody. I'm pointing out that semver is part of the API contract you enter into when you publish a package to npm or bower.

If you break that contract, you break other people's code. That's not cool.

npm works really well because we all agree on a few rules that keep our modules working well with each other. If you break those rules, you break other modules that try to work with yours. You break production apps that rely on your code.

The question is not "should we use semver?" The question is, "do we want to be good citizens in this ecosystem?"

@akre54
Copy link
Collaborator

akre54 commented Jun 16, 2014

The key point is that on a project like Underscore, every change is breaking to someone. If we remove a null guard from _.extend (to use a random example) because it's causing a bug for someone, and it creates a bug for someone else, is that a patch? Is that a minor version? Major?

For Underscore and Backbone, I don't think it's unreasonable to pin your dependencies. Following semver isn't a requirement to publish to a packager.

@jdalton
Copy link
Contributor

jdalton commented Jun 16, 2014

The key point is that on a project like Underscore, every change is breaking to someone.

You're jumping to an extreme to justify your position. The reality is it's a balance. There's popular API, edge cases, and documented behavior. It's very possible to go for a stretch of time w/o bumping a major version by simply fixing bugs and adding functionality.

What it means is the maintainers have to think and plan. This means you may have to create a roadmap for features or changes that can't be tackled w/o breaking back-compat and that's fine.

Underscore has bumped patch releases and introduced major breaking changes. This is something the maintainers can totally control.

For Underscore and Backbone, I don't think it's unreasonable to pin your dependencies.

There should be a message/warning in the documentation for sure.

@ericelliott
Copy link

@akre54 Changing undocumented features or undocumented bugs may break code that relies on those, but users rely on undocumented features and bugs at their own peril.

Ignoring semver puts every user of your API in peril. People fix bugs in large open source projects that follow semver all the time, but it's really rare to see large version numbers in the npm ecosystem.

Why is that?

Because all good APIs follow the open/closed principle (open for extension, closed for breaking changes) as closely as they reasonably can, so that users can keep pace with the API, and changes don't break existing code.

@braddunbar
Copy link
Collaborator

To add to the anecdotes, my code has also been broken by Underscore bumps in the past. We've resorted to committing node_modules to prevent it because --save --save-exact doesn't cut it. If a second-level dependency relies on Underscore and uses a ^x.y.z version then your app is still broken.

As for version numbers indicating progress, I don't care. Using version 2.1.3 or 143.3.2 makes no difference to me. Library progress and maturity are not measured in version numbers. I just don't want a phone call on Sunday afternoon because someone updated a dependency that relies on underscore@^x.y.z.

@ericelliott
Copy link

👍 Yep. @braddunbar makes a point I haven't got to yet -- ignoring semver makes your package a dangerous, virulent package, because that breaking change could be lingering anywhere.

It's definitely an onerous requirement to force users to hunt down breaks in third party code that depends on your broken package.

IMO, real library progress and maturity is measured in dependability, and semver goes a long way toward meeting that goal.

@akre54
Copy link
Collaborator

akre54 commented Jun 16, 2014

There should be a message in the documentation for sure.

Definitely. I'm happy to add one if that's what we decide.

I'm not against a better release system (lord knows it's a pain to wait for months for your one little bugfix to get deployed), but I'm not sure "follow semver" is the one right answer.

@jridgewell
Copy link
Collaborator

Right now, I don't think it matters if it's the one right answer. It is the community accepted answer.

@ericelliott
Copy link

I don't think anybody said "follow semver" is the one right answer for a better release system. What we said is that it's the npm API contract, and that breaking that contract does damage.

@akre54
Copy link
Collaborator

akre54 commented Jun 16, 2014

@jdalton and @dilvie How would you propose we handle releases? What about accepting features? "Use semver" doesn't really tell us anything.

Should we push back features like _.matches a few months so that we can wait on bugfixes for the current code, or should we release it now and let people hammer out implementation issues?

I think semver just simply doesn't fully apply here.

@bahmutov
Copy link

Shameless self-promotion: use next-update https://github.com/bahmutov/next-update to test first if the dependencies can be updated without breaking your project. I constantly found breaking changes in different modules despite changed *.*.x, so now I do not trust any self-declared versions.

@ericelliott
Copy link

Do you think this project is a snowflake? Lo-Dash is basically Underscore++, and following semver is no problem for @jdalton.

Accepting new features:

Does it break the existing API contract?

Yes? - bump major version number.
No? - bump minor version number.

Accepting bug fixes / minor patches:

Does it break existing API contract?

Yes? - bump major version number.
No? - bump patch version number.

How you choose to schedule official releases is entirely up to you. Just make sure the versioning is semver compatible so you don't break other people's code.

@megawac
Copy link
Collaborator

megawac commented Jun 16, 2014

Agreed with @dilvie, for example 1.3.3...1.4.0 probably should of been a major bump as we dropped support for sparse arrays. Next version should be a major bump as we'll be adding a ton of new sugar via lookupIterator and drop native iterators.

If the documentation of a function has to change its clearly a major change

@ELLIOTTCABLE
Copy link

I got into a brief discussion about this at JSconf this year. I don't have the time to go into detail, here, but:

stop conflating human-facing “versioning,” with computer-facing (or really, build-system-facing) “API compatibility.” just stop.

Version: “This thing I like has new features, or something else I should be interested in when I have the time.” There's a new iPhone. There's a new OS X. There's a new Ember. There's a new Revised Report on the Algorithmic Language Scheme.

API compatibility: “This thing was updated to fix <other person's> problem, and this may/will break the way exercises that thing.” The iPhone's power-plug was replaced. OS X deprecated resource-forks. Ember deprecated a style of action names. Scheme is now case-sensitive.

The latter happens during, or alongside, the former; but the former is in no way necessarily reliant on, or even related to, the latter. They should be tracked, and (if we're really going to freaking specify a versioning system, jesus) specified, separately. (the concept of ‘semantic versioning’ popular in the J.S. community is particularly obtuse and terrible; but again, not something I want to go into in detail on somebody else's Issue comment-thread.)

tl;dr: They aren't screwing up your ecosystem by choosing their own path. (Especially when that path is terribly flawed.) I wish more projects would. Go use something else, if you're that bent out of shape over it.

@ericelliott
Copy link

@ELLIOTTCABLE - agreed about human facing vs computer facing ... As long as the human facing looks nothing like the computer facing so that conflating is less problematic.

Maybe call the next human facing release something like "snowflake" instead of n.n.n

Totally disagree with the last bit, though. When you pretend to follow an API contract and then break it, stuff breaks. It costs other people real time and real money. With a project as popular as underscore, there's potentially a lot of real damage done.

@akre54
Copy link
Collaborator

akre54 commented Jun 16, 2014

Does it break the existing API contract?

Look back to the very first arguments in this thread. "Everything in Underscore is basically surface area", ergo, everything, documented and not, is part of its API contract. Any one change is a change for everyone.

Lo-Dash, being "Underscore++" doesn't deal with nearly as many changes in features because it can follow safely behind Underscore in working out major features. Lo-Dash's improvements are mainly under the hood or in adding a few methods, not fundamentally rethinking its features.

@jdalton
Copy link
Contributor

jdalton commented Jun 16, 2014

@akre54

How would you propose we handle releases? What about accepting features? "Use semver" doesn't really tell us anything.

You can accept new API or even some enhancements to existing API. If you add new API or enhancements (that don't break compat) its a Minor version update.

Should we push back features like _.matches a few months so that we can wait on bugfixes for the current code, or should we release it now and let people hammer out implementation issues?

I think _.matches is pretty straight forward. There will always be bug fixes.

I think semver just simply doesn't fully apply here.

Sure it does. You're starting to creep into release cycle concerns, which is separate from semver, but I'll follow you there.

@ELLIOTTCABLE
Copy link

@dilvie: going to stay out of the rest of the argument, but throwing this in: really, really like your suggestion of going with the ‘version name’ convention. (=

For the human-facing part, I'm a big fan of less-style versioning:

> less --version
less 418
Copyright (C) 1984-2007 Mark Nudelman

less comes with NO WARRANTY, to the extent permitted by law.
For information about the terms of redistribution,
see the file named README in the less distribution.
Homepage: http://www.greenwoodsoftware.com/less

… couple that with a pretty name, to make it extra-clear that we're talking about “interestingness”-version, not API compatibility, and you've got a winning combo.

+1 for Underscore 42: “Silly Sheltie.”

(As for the build-system facing part, I've got some controversial views on automated, generative API-compatibility. Let's take this shit out of the hands of fallible maintainers, please, and attack it with static analysis or dynamic crawling.)

@jdalton
Copy link
Contributor

jdalton commented Jun 16, 2014

@akre54

Lo-Dash, being "Underscore++" doesn't deal with nearly as many changes in features because it can follow safely behind Underscore in working out major features.

What does that even mean? Lo-Dash deals with more changes and follows its own versioning separate from Underscore. Lo-Dash has changed so much it has had to offer an Underscore-compat build to continue its support of being a drop-in replacement. We have different features, methods, and different API compat concerns.

Lo-Dash's improvements are mainly under the hood or in adding a few methods, not fundamentally rethinking its features.

That's not the case either. Lo-Dash moves at a faster pace and runs up against back compat more frequently. This is why we're ~v2.x going on ~v3.x. Lo-Dash is Underscore-like. If Lo-Dash can follow semver so can Underscore. I've done it for ~2 years now. Your arguments simply fall flat in the face of reality.

I've been down this road before so I can help y'all get there too.
For starters, the next Underscore release would make a great 2.0.

@akre54
Copy link
Collaborator

akre54 commented Jun 16, 2014

I'm curious how everyone thinks "breaking change" should be defined. Every new feature change to Underscore is a breaking change for somebody else.

Let's take for example all the recent changes to _.each. Should we have bumped when we changed the return value of _.each to return the original list? Is this a bugfix? A new feature? A backwards-incompatible change? It returned undefined before, so it's unlikely it broke anyone's code.

Should we have bumped when we allowed the internal each helper to be reassigned externally? Could that have broke someone's code? No public API changed there.

Changing _.each to avoid sparse arrays and using for loops instead of native forEach is obviously breaking for some, but since sparse arrays are dead who really cares? Is it something we should push a major version over? Is this a bugfix?

I think we're overdue on a major version bump (a lot has happened in 219 commits). A 2.0 release and a solidification of our version policy would go a long way here.

@jdalton
Copy link
Contributor

jdalton commented Jun 16, 2014

@akre54

Every new feature change to Underscore is a breaking change for somebody else.

Not necessarily.

Let's take for example all the recent changes to _.each. Should we have bumped when we changed the return value of _.each to return the original list? Is this a bugfix?

It's not a bug fix, it's an enhancement. Is it non-breaking?-- It's probably a safe change because it's unlikely the return value of _.each was relied on and not something devs have reported as being a roadblock when switching to Lo-Dash. If in doubt side with breaking, or test the waters with an RC release. If the change was to allow exiting early from _.each I'd say it was a definite breaking change as devs run into that when using CoffeeScript.

Should we have bumped when we allowed the internal each helper to be reassigned externally? Could that have broke someone's code? No public API changed there.

I'd say that falls under undocumented implementation details. At the time the change didn't allow anything new because Underscore still branched for native methods. This change falls under the larger group of changes in post 1.6.0 so can land in a 2.0.

Changing _.each to avoid sparse arrays and using for loops instead of native forEach is obviously breaking for some, but since sparse arrays are dead who really cares? Is it something we should push a major version over? Is this a bugfix?

It can be seen as a bug fix but it's definitely a breaking change. This is one of the things devs run into when they switch to Lo-Dash. Because of how Underscore used to be, using native when available, it would mask sparse array use and devs would only encounter the issue if they tested in older browsers. However with this change devs will be alerted to their sparse array use sooner, in modern browsers, so there's a chance their previously working code will hit a snag.

I think we're overdue on a major version bump (a lot has happened in 219 commits). A 2.0 release and a solidification of our version policy would go a long way here.

👍

@ericelliott
Copy link

breaking change (plural breaking changes)

(computing) A change in one part of a software system that potentially causes other components to fail; occurs most often in shared libraries of code used by multiple applications
"Not possible to fix old entries without a breaking change, so remap old to new in import lib."

Some of this requires some thought and judgment. It may be true that all changes break somebody's code, but if all participants agree to use the open/closed principle as a guide for what constitutes breaking, it makes everybody's life easier.

So, adding any property or method to the API is generally not a breaking change (the API is open for extension, but closed to backwards incompatible changes).

Changes to function signatures require more thought.

Should we have bumped when we changed the return value of _.each to return the original list?

Was that a documented feature of the API that served some purpose? For example, some functions return undefined when the inputs passed in would not result in sensible output. That doesn't seem to be the case with each... So, probably not breaking.

Avoiding sparse arrays on the other hand has a larger potential to change return values that developers rely on, so clearly, that's a breaking change, and anybody who used sparse arrays cares.

Sparse arrays may not survive ES6, but they're not dead yet.

@jdalton
Copy link
Contributor

jdalton commented Jun 16, 2014

@akre54
Sometimes the answer to whether or not a change is breaking isn't straight forward. In those cases context, history, and data helps. It's fortunate that I've been able to use Lo-Dash as a testing ground for new features and see which changes trip up devs coming from Underscore. Underscore can in turn use that to help make informed decisions on the impact of changes.

At the very least following semver will help prevent major breaking changes from slipping into patch releases and encourage developers to think about the impact of their changes. That's a win for everyone.

@ericelliott
Copy link

@jdalton, class act. :)

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