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

Immutable API #3548

Closed
wants to merge 17 commits into from
Closed

Immutable API #3548

wants to merge 17 commits into from

Conversation

butterflyhug
Copy link
Contributor

@butterflyhug butterflyhug commented Oct 30, 2016

This is an implementation of moment/moment-rfcs#2, which was written to address #1754. This is obviously a breaking API change that would require a new major version number (e.g. "3.0").

The only major change from the RFC draft's discussion is that moment.updateOffset() and moment.duration()._bubble() will now return moment and duration objects, respectively. Otherwise it's pretty much impossible for external code to customize the behavior of these hooks.

Feedback welcome! 😃

@ichernev
Copy link
Contributor

That is amazing. I'll review it very thoroughly on Tuesday utc. Also we need to make sure to backport whatever is released before 3.0 (if we decide to release). Or we can ship this as 3.0-rc1 and keep the two branches for a few months.

@butterflyhug
Copy link
Contributor Author

Thanks! I'm adding a few comments now about things that I think are worth extra scrutiny, but I'd love to get a comprehensive review.

proto.localeData = localeData;

// Deprecations
import { deprecate } from '../utils/deprecate';

proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', toISOString);
proto.lang = lang;
proto.lang = deprecate('duration.lang() is deprecated. Use locale() or localeData() instead.', wrap(Duration, lang));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lang method has been deprecated in the docs for several releases now, so I went ahead and added the warning here. But technically we said we'd get rid of all the deprecated stuff in our next major release, so maybe we should just delete old compatibility shims like this...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kill with fire 🔥 🔥 🔥

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd remove everything that has been deprecated besides good old 'moment construction falls back to JS date'. Unfortunately I think we'll break the whole internet if we remove that functionality.

export function Locale(config) {
if (config != null) {
this.set(config);
set.call(this, config);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This got reconfigured because the prototype's copy of set is wrapped for immutability, which is decidedly unhelpful inside a constructor.

Note that we hadn't explicitly discussed making Locale objects immutable, but it seemed like the obvious thing to do.

@@ -18,16 +18,13 @@ export function locale (key) {
}
}

export var lang = deprecate(
'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deprecation wrapper got moved to the prototype definition, the same way we handle all the other deprecated APIs.

@@ -0,0 +1,11 @@
export default function wrap(Type, fn, dontCloneWithNoArgs) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious to know if other folks think this function is sensible. It provides a conveniently concise and reusable API, but I fear the implementation might be a little hard for us humans to parse.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's clear enough to me. I'm not exactly sure how one would go about doing what you are doing besides this, while still mostly preserving existing code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put exactly how this works in the contributing file. Also needs to be a standard part of code review to make sure this gets used if it needs to be.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With regard to moment-timezone - it calls update on the moment, and there's no need to be cloning the moment to do that - that's just a perf killer. Can we track updateInProgress in this function and just mutate if that's the case? I think that's fine to do because JavaScript is single threaded (thank god!).

@@ -43,7 +43,7 @@ export function stringSet (units, value) {
units = normalizeObjectUnits(units);
var prioritized = getPrioritizedUnits(units);
for (var i = 0; i < prioritized.length; i++) {
this[prioritized[i].unit](units[prioritized[i].unit]);
prioritized[i].getSet.call(this, prioritized[i].value);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change prompted all of the other getter/setter upheaval in this commit. The old line of code didn't work as-is, because the prototype's getter/setter functions will now return new objects instead of mutating the old ones. A simpler change would've been:

        var ret = this;
        for (var i = 0; i < prioritized.length; i++) {
            ret = ret[prioritized[i].unit](units[prioritized[i].unit]);
        }

But that would involve generating a bunch of unnecessary moments. At the time I wrote this commit, I was worried about the resulting performance impact. I became somewhat less concerned about keeping copies to a minimum in later commits, after I discovered that the updateOffset API requires some "extra" copying no matter what.

Let me know if you'd like to see any changes in the way I've balanced performance vs readability throughout this PR.

value: unitsObj[u],
priority: priorities[u].priority,
getSet: priorities[u].getSet
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the getter/setter functions get registered here, in addition to registering them directly on the Moment prototype as before, so that we can access their unwrapped implementations in the generic .set() API (aka "stringSet", where I left the other comment above).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this code was a bit too "advanced" :)


if (keepLocalTime) {
this.subtract(getDateOffset(this), 'm');
ret = ret.subtract(getDateOffset(ret), 'm');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, I obviously wasn't quite as worried about creating intermediate copies when I updated this function...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see an issue about intermediate copies. In general, with immutable API, how do you avoid them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we'd done our original plan of wrapping mutable internals, then we wouldn't have intermediate copies within the library implementation. But this comment is outdated -- we decided to work toward immutable internals instead, to make future development more understandable.

updateInProgress = true;
hooks.updateOffset(this);
updateInProgress = false;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this code to createFromConfig, which seems to cover all cases of creating Moments from non-Moment input. In doing so, I'm assuming that it's safe to clone any Moment without running updateOffset. That seems like a reasonably safe assumption, but I'd like to highlight it in case there's anything I'm not aware of.

When I kept the updateOffset call here, I had a subtle bug with the way moments got cloned when the parseZone algorithm calls the wrapped copy of utcOffset from our prototype. It occurs to me now that I probably could've fixed that by (1) re-ordering operations within parseZone, and/or (2) using the unwrapped implementation behind utcOffset instead of simply calling the prototype's copy. I'd be happy to try replacing this with one or both of those solutions if you're concerned about this particular edit -- but if this edit seems safe, I think it's probably faster and easier to reason about.

(Note that this edit avoids the messiness of trying to make an immutable updateOffset work inside the constructor:

updateInProgress = true;
var updated = hooks.updateOffset(this);
copyConfig(this, updated);
this._d = updated._d;
updateInProgress = false;

Which is ugly and annoying.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I follow you here (maybe this comment is outdated). I'm left with the impression that there is no "wrap" any more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, most of this comment is outdated. The first paragraph is still true -- I've still got this call to updateOffset in create/from-anything.js -- but not for any particularly great reason anymore, so I should probably just move it back to the constructor.

// _bubble returns a new Duration because immutability,
// but here in the constructor we need to update `this` with the new values
var bubbled = this._bubble();
copyDuration(this, bubbled);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yuck. I don't see a way around this, though, unless we un-publish the _bubble method so that it can remain an unwrapped mutator.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaand I got my commits out of order while rebasing things. The implementation of copyDuration appears in the next commit. (In retrospect, I should've just squashed these two commits into one.)

@maggiepint
Copy link
Member

Not sure how everyone else feels, but my inclination with this PR is to divide up the tests between me, @icambron , @mj1856, and @ichernev. Each of us can take a set to review. You're then responsible for reviewing the entire code path down from the test. This will cause the most commonly used code paths to be reviewed several times, and the less commonly used to be reviewed once.
I think we also need to perf benchmark against the existing build to see if we have introduced any problem areas.
Finally, we should cut a build of this for a few interested individuals to use, before we do any kind of release. Better to catch with some dedicated beta testers.
Impressive as heck @butterflyhug!

@maggiepint
Copy link
Member

Also, we're going to probably have a few merge conflicts with existing PRs. Not sure on the strategy for resolving that, but to me it makes sense to start merging them to develop, even if we aren't planning on a release. I think poor Lucas is going to be stuck with resolving for a large part, though we all can.

@butterflyhug
Copy link
Contributor Author

So long as we don't do another ES6 rewrite in the meantime, I think merging changes from develop between now and 3.0 should be a doable (albeit large) task. After all, I've done it before.

@ichernev
Copy link
Contributor

ichernev commented Nov 3, 2016

OK, here are my notes:

  1. should we (do we) freeze moments/durations/locales to make sure they're not changed by something we've missed?
  2. can we add _clone method that clones instead of passing the type in wrap. It will be for internal use only
  3. in src/lib/create/from-anything.js why do we updateOffset (we weren't before)
  4. in src/lib/duration/constructor.js bubble -- if its internal and used only in constructor why can't it just modify stuff
  5. in src/lib/duration/prototype.js - why is abs not wrapped?
  6. about updateOffset madness -- we can just implement the new API and remove the old one Implement timezone interface #3134
  7. in src/lib/moment/start-end-of.js - why is startOf wrapped, when its implementation is immutable, same goes for endOf
  8. in src/lib/utils/day-of-week.js - getSetDayOfWeek is immutable, do we need wrap (others in file too)
  9. in src/lib/units/day-of-year.js - getSetDayOfYear -- no need to wrap
  10. we have a generic get/set for all builtin units (ms,s,h,d,M,y), so I think we can make it immutable, then remove the wrap on all methods. This half-way immutability might give us an immutable API but it is crazy to develop, never knowing which functions can mutate and which can't
  11. in src/lib/units/quarter.js -- the getSetQuarter can be made immutable (by relying on .month pseudo-immutability)
  12. how do we port the locales and the locales tests? How did you fix all tests now?

I'm a bit lost on something -- how come the plugin did work while not changing any code inside moment. Because it had both frozen and mutating API, and the inners where always using the mutating api, the frozen stuff was only for the user, using the mutating api + clone. Couldn't we just hide the mutating API from the user and only expose the frozen, which will be autogen with the plugin code? Just overall -- I was expecting a change that was basically the plugin code shipped with the core code. If we spend time to make it properly immutable we might as well do that and remove wrap from the remaining methods. The core mutating methods are not that much, most rely on other mutating methods.

@butterflyhug
Copy link
Contributor Author

Re @ichernev:

  1. I'm not freezing moments here, and I think we probably shouldn't add that. Object.freeze causes silent failures which I expect would be hugely problematic to debug here -- unless you're in strict mode, in which case it raises exceptions, but Moment APIs aren't supposed to raise exceptions. Also, Object.freeze is generally quite slow. If we care about cloning performance, then freezing seems like a non-starter.

    I suppose we could only do the freezing in (some or all) tests, but I think maybe we just write a new testcase to prove that everything still works if the user freezes the moment, and call it a day.

  2. Sure, will do.

  3. Because I removed updateOffset from the constructor (so that it doesn't run during internal cloning), and it still needs to happen somewhere when users call moment(). details

  4. You might disagree, but I feel like any prototype methods (even if private) should be immutable. Even if we slap everyone's wrist every time they do it, people will call prototype methods and then come looking to us for support, and I feel like our job is easier if we know that they can't run any of our mutating code.

    So, I'd be happy to keep this mutating if we pull it off the prototype; otherwise my instinct is to make it immutable. I suppose that means I should import the raw mutable implementation into the constructor and use that instead of the current property-copying hack, regardless of whether we keep it on the prototype or not.

  5. Oops, will fix.

6-11. Ooh, good point, I should use #3134 as a starting point. Without updateOffset in the mix, I should be able to keep everything internally mutable, more like the plugin. (During this work, I realized that my plugin presumably never worked properly with updateOffset / Moment Timezone.)

-12. I did all of these tests by hand, and I don't have a better system in place for porting locales and their tests. I'm tempted to try writing a script to do simple code rewriting (assigning the result of statements that have mutation methods back to the original variable name) and see how far that gets us, but I haven't done that sort of coding in JS before. Suggestions welcome here.

Re: "I'm a bit lost" -- because updateOffset, which shouldn't be an issue if I move this on top of #3134. I assume that consistent internal mutability is preferable to consistent internal immutability for performance reasons, even if it forces contributors to worry about more things (e.g. they can't directly call prototype APIs internally), but let me know if you disagree. I'm willing to push this in whichever direction we feel is best for the library long-term.

@maggiepint
Copy link
Member

One concern about moving this change on top of 3134 - code churn. It feels like that introduces a large number of changes at once. 3134 is a big deal.

If we go down that road, I would get 3134 shipped in moment 3.0, and follow about two or three months later with moment 4.0 being immutable. I'm okay with that if it's the right decision for the code, but I would not send those two things together.

@ichernev
Copy link
Contributor

ichernev commented Nov 7, 2016

@butterflyhug just verifying -- the whole reason for the internal mutability is updateOffset? Now that I think about it -- its hard now, because update offset is called on every mutator and might mutate again. But I don't buy the performance reason. Right now we clone the whole moment object on every mutation just because we might mutate it with updateOffset. This should be much more inefficient. If updateOffset was made to return a new moment it will only happen very rarely, and all mutators can just create one copy, correct from the start.

I'd ship the two changes together. I disagree with @maggiepint because, the updateOffset needs to match the immutability in 3.0. So we can't just ship one without the other. We could ship only new TZ interface, but I do not consider it a big change. Because it is kind of internal API to moment-timezone. If somebody was using it -- he can't in an immutable version, so porting your code to immutability should include that small nuance (updateOffset).

@butterflyhug
Copy link
Contributor Author

@ichernev Yes, I think we're basically saying the same thing in slightly different words.

With updateOffset, there are two possible end states:

  1. Convert all of our code (including internal APIs) to immutable APIs. This gets us a consistently immutable codebase at the cost of more cloning internally (potentially ~10 clones when calling certain public methods, like startOf).
  2. Keep the existing mutable APIs internally and wrap functions on the prototype to expose an immutable API to users. Whenever we need to call updateOffset, we copy the resulting moment's properties back into the original moment, thus preserving mutability internally while giving users an immutable updateOffset. This minimizes the algorithmic changes (although there'd still be a bit of code churn to use unwrapped APIs internally instead of calling mehods from the ptototype), at the cost of forcing contributors to think about and manage the mutable->immutable boundary.

Either way, updateOffset implementations won't be able to rely on adding new properties to the moment being updated anymore (e.g. these tests) -- but that's true for any of the immutable APIs so it's not really a surprise.

The current code in this PR is a middle ground between these two end states, because I was indecisive. You're right to flag the inconsistency as a major issue, so I should ideally bring the code into alignment with whichever path we'd prefer to maintain over the long term.

@butterflyhug
Copy link
Contributor Author

And if we're willing to remove updateOffset and just ship the TZ interface from #3134, then that makes it a bit cleaner to keep a mutable API internally while wrapping the prototype for immutability.

@ichernev
Copy link
Contributor

@butterflyhug why is the new api better for mut internals and immut api?

I think I like the first option more. The problem is some methods are slower but that's what people want.

The problem is, having the mutable methods around is very flexible, allowing us to do a lot of questionable magic, like auto-batch mutations (record mutations and batch exec them on read) or provide a mutable interface directly to speed things up.

Ideally we'd do one or both (your suggestions) and benchmark with existing code.

@butterflyhug
Copy link
Contributor Author

@ichernev I guess maybe it isn't any better. I thought #3134 had deprecated updateOffset with the new Timezone stuff, but upon further review I guess I remembered that wrong.

I'll aim to take another pass at this within the next week or two.

@ichernev
Copy link
Contributor

@butterflyhug I'm pretty sure if we introduce the new TZ api we need to get rid of updateOffset. Basically on every mutation we'll ask the library, and it will return the offset, which we need to store (but will probably be the same as the old one). It just has a cleaner API.

If we're to have half our methods mutable internally, half immutable then we need veery good guidelines on what and where needs to be mutable and try to decrease that to a minimum. And possibly a package (directory) inside lib for all mutable stuff. But again, I think this will cause huge butthurt in the future.

@butterflyhug
Copy link
Contributor Author

@ichernev Right, okay. That's what I initially thought. The Timezone interface would be better for mutable internals and immutable API. Moment-internal code would ask the Timezone for the correct offset and update the Moment instance when appropriate.

The problem with the updateOffset API is that we have external code (from Moment Timezone) that needs to edit a moment in the middle of the internal mutable code path. That's fundamentally at odds with building a clean internal=mutable / external=immutable boundary. Of course we can hack around the issue, but it's cleaner if we don't need to.

@maggiepint
Copy link
Member

Friendly reminder: the moment-timezone code to make the TZ interface work has not yet been written :-)

@ichernev
Copy link
Contributor

@maggiepint it hasn't. But I fail to find a better interface for updating the offset. So more or less it will be what the rfc says. If you have another idea that will magically solve the issue at hand I'm all ears.

moment#clone() is now a deprecated no-op, because immutability.
The bubble function now returns a new data object instead of
mutating the duration's existing data (which had always been empty
when we called _bubble anyway).
Also make the Duration#add and Duration#subtract implementations
internally immutable (so we don't need to wrap them for the
prototype).
Also ensure that we always test both the 11:00 hour and the
non-11:00 formatting variations, instead of simply testing
whichever hour it happens to be when we run the test.
@butterflyhug
Copy link
Contributor Author

Okay, I've updated this code and it should read a bit better now. For internal consistency, all prototype methods now have immutable implementations instead of using a Frozen Moment-style wrap function in the prototype definition. All existing tests are passing, and I've added a few additional tests for good measure.

I did not remove old deprecated functions here; that will be a quick and easy change for 3.0, but I think it belongs in a separate PR. This is also still using updateOffset instead of the new Timezone API. Otherwise I think I've addressed the bulk of the feedback on this PR -- let me know what you think!

I haven't written any new benchmarks to test things thoroughly, but based on existing resources, the performance impact seems to be relatively small:

  • Our existing benchmarks seem to be roughly the same speed as before. Obviously this is far from all-inclusive, but it's a promising result. Amusingly, the big outlier is that this PR's no-op clone is dramatically slower than before -- apparently our deprecation warnings are relatively slow.

  • Anecdotally, running the entire test suite is between 0.5 and 1 seconds slower (~4-8%) on my machine with this code than v2.18.1, although there's a lot of variance from one run to the next so this is a very rough estimate. I suspect this is a rough upper bound on the actual performance changes in our test suite (when running in Node), given that I've added a few more tests and deprecation warnings.

@butterflyhug
Copy link
Contributor Author

Oh, and this is rebased on top of last night's v2.18.1 release.

Copy link
Contributor

@ichernev ichernev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I was reviewing two things popped to my mind:

  • we need to provide an interface for users to check if the moment is mutable or not (because with 3.x and 2.x out in the wild it will be hard to tell)
  • we need to do somehow test that moment is actually immutable (that is -- all functions don't change anything). Even if it is now, we'll have trouble keeping it this way just by "looking at the code".

The one thing that still bothers me with the code itself is the way we make changes to internals, just after clone in a bunch of methods (places where we call new Moment(...)). In the places where we modify _d, couldn't we just take _d, clone it, modify it and then create a moment with it -- so modify before clone, not after. Now this is dangerous because could mess something up, when its spread to many places (12 right now). On the other hand we can ship this way and work our way toward all-mutability-in-constructor in future (not necessarily major) versions.

if (!keepLocalTime || mom._changeInProgress) {
mom = addSubtract(mom, createDuration(input - offset, 'm'), 1, false);
} else if (!mom._changeInProgress) {
mom._changeInProgress = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This _changeInProgress was there because of mutability. Now I don't think its necessary. Or maybe it is necessary, but we should put it on the new moment, so it want fall into a cycle. Maybe that is related to the other piece of cycle-break-check in the constructor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, as written this is a bug! I agree that we probably don't need it anymore; I'll try removing it. Good catch!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually need to implement the the new moment-timezone that handles both 2.x an 3.x moments.


if (keepLocalTime) {
this.subtract(getDateOffset(this), 'm');
ret = ret.subtract(getDateOffset(ret), 'm');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see an issue about intermediate copies. In general, with immutable API, how do you avoid them?

updateInProgress = true;
hooks.updateOffset(this);
updateInProgress = false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I follow you here (maybe this comment is outdated). I'm left with the impression that there is no "wrap" any more.

newLocaleData = getLocale(key);
if (newLocaleData != null) {
this._locale = newLocaleData;
clone._locale = newLocaleData;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get a bit uneasy when reading this. Sure, its a new clone, but I can see a bug slipping through if one is not very careful. Would it be possible to cram all the "mutability" inside the constructor, so it gets all arguments and produces a frozen (maybe not literally) moment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll take a stab at that.

// the following switch intentionally omits break keywords
// to utilize falling through the cases.
switch (units) {
case 'year':
this.month(0);
m = m.month(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say this particular function could use a lower level API to avoid unnecessary cloning. Because resetting all units lower than a given unit is the most expected operation dealing with multiple units and it would be nice if it was fast. Of course we can ship this way, and fix (improve) later.

value: unitsObj[u],
priority: priorities[u].priority,
getSet: priorities[u].getSet
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this code was a bit too "advanced" :)

@@ -196,8 +203,8 @@ export function hasAlignedHourOffset (input) {

export function isDaylightSavingTime () {
return (
this.utcOffset() > this.clone().month(0).utcOffset() ||
this.utcOffset() > this.clone().month(5).utcOffset()
this.utcOffset() > new Moment(this).month(0).utcOffset() ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed? Can't we just .month(X).utfOffset()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, yes, I think this is a stale change. I'll simplify this as you suggest.

@ichernev
Copy link
Contributor

One more thing before I forget -- we might need allow some APIs for plugins to hook into the constructor, because if they need to keep a piece of data with moment, now that its immutable it needs to be in the constructor, but we don't have any hooks for that. Or maybe plugins don't need to keep data in moment, or maybe the data should be only lazily computed (and keep moment at least observably immutable).

@butterflyhug
Copy link
Contributor Author

I think it's probably best for plugins to lazily compute values. It's possible for someone to create a moment instance before loading all their plugins, so ideally plugins will need to handle the case where they didn't exist when the constructor ran anyway. I suspect they're more likely to do it correctly if we make that the normal case. 😉

Also, I'm sure we'll still be lazily computing a few things in the core library unless we do a rigorous code audit specifically aimed at moving all lazy computations into the constructor. As long as nobody ever updates existing values (and all new values are derived solely from the moment's existing data), I think it'll be fine.

That said: we/I should probably write a guide to help plugin authors to update their code, and this would be a good issue to highlight in that doc.

@butterflyhug
Copy link
Contributor Author

Also: the proposal for distinguishing immutable from mutable moments was to add a version property to the instance prototypes, to supplement the existing moment.version global. I'll try to take a pass at adding that and addressing your other comments later today.

Thanks for the thorough review, @ichernev!

@ichernev
Copy link
Contributor

ichernev commented May 2, 2017

I'd rather add moment.isImmutable property. With version you have to split and compare and so on.

I didn't think too much but we should be able to hack some code that verifies all moment properties did not change after each function (for tests only). And we need to consider lazily computed props and not fail if those change.

I added moment.isImmutable and fixed unnecessary cloning.
@muescha
Copy link

muescha commented Aug 27, 2019

just note for someone who come over this PR:

the referenced #1754 is closed in favour of Luxon with is immutable

Closing this. As others have pointed out, use Luxon if you want a mostly immutable API. Thanks.

@icambron
Copy link
Member

Yeah, I think we can safely close this.

@icambron icambron closed this Aug 28, 2019
@muescha
Copy link

muescha commented Aug 29, 2019

Maybe we add a note to the moment readme to point to LUXON?

The not-immutable was the first pain point with the momentjs for me so it would be nice to know that exist a new improved tool for it

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

Successfully merging this pull request may close these issues.

None yet

6 participants