Underscore: Underscore does not follow SemVer

Created on 14 Jun 2014  ·  32Comments  ·  Source: jashkenas/underscore

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.

duplicate

Most helpful comment

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 :frowning:

All 32 comments

To quote https://github.com/jashkenas/backbone/issues/2888#issuecomment-29076249:

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 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?

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.

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 :frowning:

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.

+1 for semver

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.

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?"

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.

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.

@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.

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.

:thumbsup: 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.

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.

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

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.

@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.

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.

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.

Agreed with @dilvie, for example https://github.com/jashkenas/underscore/compare/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

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 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.

@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.

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.

@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.

@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.)

@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.

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.

@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.

:+1:

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.

@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.

@jdalton, class act. :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

githublyp picture githublyp  ·  3Comments

marcalj picture marcalj  ·  5Comments

acl0056 picture acl0056  ·  5Comments

afranioce picture afranioce  ·  8Comments

arypbatista picture arypbatista  ·  3Comments