-
Notifications
You must be signed in to change notification settings - Fork 7k
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
make moment mostly immutable #1754
Comments
My two cents is that we should either go for full-hog immutability or not at all. Having some methods be immutable ( |
I would also prefer everything immutable by default. Are the getters not immutable already? |
Here are the big issues I see with switching to immutability. Pseudo-ImmutabilityBecause of the nature of javascript, we will never be able to have true immutability. The best we can do is create a copy and then mutate and return that copy. We can carefully wrap all the public methods to make sure we are always copying and mutating rather than just mutating, but that doesn't prevent someone from doing I agree with @icambron, if we move to immutability, it should be a complete change. Any method that could possibly change a property on a moment would instead create a copy of the moment and make a change on the copy instead. Api Surface AreaThe unfortunate thing about switching to pseudo-immutability is that many new apis need to be created if we want to still support mutability. Before, switching from mutating a moment to cloning and mutating a moment was as simple as adding a This includes the ~20 setter methods, add/subtract, local/utc/zone/tz, startOf/endOf, lang, and any other methods used in 3rd party plugins. Memory ConcernsBecause we are now creating copies every time we want to change a value, the memory usage will increase. Of course, the new moments will be garbage collected, but the additional cost associated with this change is something to keep in mind. We would have to take great care to make sure we are not creating tons of disposable clones with methods that use other setters. To track what methods are being called, I used this little function wrapper. for (var method in moment.fn) {
moment.fn[method] = (function (fn, method) {
return function () {
console.log(method);
return fn.apply(this, arguments)
}
})(moment.fn[method], method)
} Now, when running a method, we can see how may other methods on the prototype are used. Not all of these methods would need to clone the moment, so I added comments on the ones that would require cloning. moment().isSame(moment(), 'year')
isSame
clone // clone
startOf // clone
month // clone
date // clone
year // clone
date // clone
hours // clone
minutes // clone
seconds // clone
milliseconds // clone
valueOf
local // clone
zone // clone
startOf // clone
month // clone
date // clone
year // clone
date // clone
hours // clone
minutes // clone
seconds // clone
milliseconds // clone
valueOf That is 21 copies that are created and then immediately discarded. Obviously we could optimize this by using some internal methods that are mutable and only expose the immutable versions, but it will significantly add to the internal complexity trying to keep a record of which moments still need cloning and which don't. Performance ConcernsCloning a moment is much slower than mutating a moment. I threw together a couple jsperf tests for this. http://jsperf.com/moment-cloning http://jsperf.com/moment-cloning-2 I think the second test is a much better representation of the performance losses when switching to pseudo-immutability. If we multiply those results by the 21 cloning instances noted above, the execution times are that much slower. I'm sure we could optimize the path for cloning a moment, but we would need to make it 50x faster in order to have comparable performance. I'm pretty sure this is impossible. SummarySwitching to immutability greatly increases the complexity of the internal and external apis and has major performance and memory concerns. I don't think those costs are worth the benefits that immutability would provide. |
I think the performance concerns listed here are missing the point: Generally speaking, an initial .clone() is needed to ensure correctness before performing mutation. We can't pretend like clone() is not needed with the current API. The main case here that is different is performing multiple sequential mutations. That case is addressed by creating a builder API such that all the mutations are performed as mutations on a single clone. Am I missing other common use cases? |
My original issue was about |
@gregwebs Sorry, I meant I disagree with @soswow; I think it needs to be consistent. In fact, I think that On @timrwood's concerns: That the JS objects won't be truly immutable doesn't bother me. The point is that the API provide an immutable contract. Of course the user can cheat by fiddling with the underscored properties, and cheating is generally possible even languages where immutability is the main way of doing things. On surface area and about perf: I think we'll need to use the mutators internally to avoid using up all that CPU and memory [1], so we'll then we'll have to support them at some level. Then we might as well expose them externally, like One way to look at that is that the user has to clone in their code, so Moment might as well be doing for them. That does present a problem with chaining in perf-sensitive places, which could be battled by either a builder interface (per Greg's idea) or by letting the user just use mutators there. The builder adds a bunch complexity [2], so I think I just favor explicit mutator alternatives. I think the reality is that most of the time, Moment is not being used in perf-sensitive situations, so those situations don't have to be the most convenient API-wise. I'd rather have a nice immutable API with a perf hatch for when I need it. [1] The cool kids in FP land solve this with structural sharing, but that's probably impractical here. [2] Traditionally, people make builders that are separate objects, but that would be really verbose here, since you'd have to copy the whole setter API. Just spitballing, but one alternative is that |
what worked to me, was adding an extra parameter to the functions, a flag (i named self) to denote mutability, by default is in inmutable (return a copy or new object), and when i detect perfomance i set the flag to true this stand point solved a lot of conflicts, in my code i work with arrays of arrays, (like a table, array of rows) an basic example that could be here: |
moment.add = function (measure, ammount, self) { |
Thank you everybody for their 2 cents :) For the protocol, I agree on @icambron's last post on all points. There are two big questions left. The easier one is what should the new API be, two options: The builder api definitely looks sexier, but care should be taken that objects don't stay in build mode for too long, which adds another source of trouble for us and the users. Now the hard problem -- migrating to the new version. I see two options here: And now a crazy idea that came to me now Copy-on-write clonem = moment();
funcIDontTrust(m.clone()); // doesn't actually clone
function funcIDontTrust(m) {
m.year(2005); // perform the clone here
console.log(m);
} I'm not sure how much can be shaved off with this approach, given moment instances are pretty light. Also all mutators now have to perform a check. There are a few ways to implement with varying performance in different scenarios. The good news is that its backwards compatible and we'll save us and our users a great deal of effort. And I think this is more important than reinventing the wheel. |
I'm not sure what we are gaining here. Switching to immutablilty has a ton of associated costs and maybe I'm missing it, but The main benefits seem to be developer preference. Is this all so that developers don't have I don't think switching to immutability will make bugs any less frequent, it will just m = moment();
funcIDontTrust(m.clone()); // doesn't actually clone
function funcIDontTrust(m) {
m.year(2005); // perform the clone here
// m is still in 2014
// m.year(2005) created a clone but did not assign it to anything
// it should be `m = m.year(2005)`
console.log(m);
} Here is a pros/cons list between mutability and immutability. If I missed anything,
I do think immutability is useful, but I don't think it is a good fit in JavaScript. I think an immutable interface may make sense for a lanugage like Elm, where immutablity is expected, but for JavaScript, I think mutability is expected. Much of the apis for I think we are seeing a lot of people complaining about moments being mutable, but if we switch, I think we will have just as many people complaining. This has already happened with the After looking at a bunch of old issues on mutability, I guess I'm probably sounding like a broken record here. On both sides, it's the same points that have been brought up for the past few years. I just wanted to make sure an argument for keeping a mutable api was represented in the discussion. |
@timrwood those are good comparisons, but it is pretty clear you haven't taken the time to understand the immutable use case. We have already discussed why the performance comparisons you posted assume a poorly implemented API and do not make sense. The bug comparison is also invalid. Because momentjs supports a chaining API, one could expect it to be immutable. var newM = m.year(2005) // wrong, these are both the same! So both immutable and mutable have the same problem right now. You could avoid it with the current mutable version if you got rid of the chaining API. So the immutable API is preferable to mutable because you can safely pass a moment between functions. With the current mutable moments if I pass a moment between function I have 2 options
With the immutable API we don't have to remember to call clone() every time. Instead, we have to remember to call the API function that lets us avoid cloning, but this is only a performance optimization, not an issue of correctness. |
That's an unfair statement. My argument is that I see the benefits, but do not think they outweigh the costs.
Is this not the use case for immutability? If there is more to it that I haven't taken the time to understand, please let me know, but this seems to be the only argument for the past few years. |
@timrwood yes, that is the entire case. But I don't see a sign from you acknowledging that your case against immutability (horrible performance, promotes a different type of bug not present in the mutable API) is not valid. |
i don't know if freeze() could help |
i think we should stick with the ecmascript 5 point of view, and maybe add a function that deep freeze the current object, or a global flag that automatically creates frezee objects http://blogorama.nerdworks.in/preventextensionssealandfreeze/ |
maybe a extra parameter in the constructor to create a freeze object, because a freeze object can't be unfreeze |
@lfnavess I thought about @timrwood I don't think I made my example clear. On the line Creating a half-assed version of I didn't get any any comments on how to deal with migrating the existing libraries/users of moment. I don't really like any of the options I listed above. |
Reverse-compatability is, IMO, the strongest argument against this. If we do this, we just have to accept that it's a big breaking change. If we don't want to accept that, we shouldn't do it. It's worth mentioning re:perf that there are also some huge advantages you can gain from immutability; it's not a monotonic perf hit. For example, you can cache things at the object level, because they'll never change. I also think we should be able to optimize the living crap out of Edit, arg, no - plugins add fields too (e.g. here). |
given the strong desire for backwards compatibility and yet to keep thing light-weight, I think the best solution is to fork the javascript sources (either in the sources of this project or start an entirely new project). There is room for more than 1 time library for the internet. |
Also: re: @ichernev's idea on structural sharing, one possibility is to use prototype inheritance instead of wrapping a shared state object. |
We at WhoopInc have been lurking in this discussion for quite a while. Since the discussion here seems to be going in circles, I took some time this weekend to explore what an immutable version of moment with a builder API might look like. (I have no intention of submitting a PR against moment unless invited to do so, since I'm deliberately making more strident API changes than I would expect to ever see implemented in moment itself.) Here's the result: https://github.com/WhoopInc/frozen-moment I'm just a few hours in, so everything is really rough around the edges, but based on test results I think most of the core moment functionality is working. I'll continue to track this conversation, and I'd welcome feedback specific to our fork in our repo's issues. I'll try to publish some updated docs on that repo tonight, but basically I just split out all the setter and mutation methods into a separate builder object. So using the API might look like |
FWIW, when I started using moment I had assumed it followed FP principles and would return the same value every time I call a function: var now = moment();
var yesterday = now.subtract(1, 'days');
var dayBeforeYesterday = now.subtract(2, 'days'); Of course, I didn't get the results I expected. This caught me off guard as a new user. Consider this pseudocode, which demonstrates how I would have expected the code to behave:
But instead it ended up working like this, which feels strange to me:
For now, even though it's quite tedious, I just carefully var now = moment();
var yesterday = now.clone().subtract(1, 'days');
var dayBeforeYesterday = now.clone().subtract(2, 'days'); IMO, Javascript is prone to subtle errors and I feel that FP principles help minimize those errors. I sympathize that this is a hard decision to make. I appreciate your work. Moment.js is amazing. |
+1 for 100% immutability. |
It's honestly kind of frustrating having it not be immutable. |
+1 for 100% immutability in version 3 |
There definitely should be an immutable API. As a user of other date libraries (notably, .NET |
+1 for immutability |
+1 for 100% immutability |
If the decision is made for immutability, I'd be willing to give of my time to make it happen. Perhaps pairing over a video call. I'd like to contribute more to open source but need to learn the ropes. |
Executive summary of RFC with our primary questions to be answered: https://maggiepint.com/2016/07/16/immutability-its-happening/ |
@maggiepint I tried to post a comment on that article, but for some reason it was swallowed. Here's what I wrote: My biggest concern with these changes is that they respond to a disproportionately vocal and expressive part of the community - avoiding the quiet bulwark of ordinary developers who aren't even aware this discussion is taking place. GitHub's threads are not a microcosm of the wider development community. This site suffers the same participation bias as many forums on the web, and is biased towards a certain kind of developer: intellectually engaged with the theory of computing; interested in ideas rather than applications; and dare I even say socially inclined to rallying around popular trends. These developers are naturally attracted to philosophical cause célèbres like immutability and functional programming, and have closer access to you than any other group. They will be the loudest voice in the room, and they will clamour for this change - but what of the wider world? The wider world wants Stuff That Works. It wants to know that the library it currently uses will continue receiving updates - bugfixes at least, but ideally small incremental improvements that make their lives better. But it does not say so, because it does not actively seek out these forums, and because it takes a thick skin to speak out against the trend. If you are intent on this change, I think you will need to make a very clear case to these people why this change will make their lives better; why they should upgrade; why they needn't worry their legacy projects won't receive indirect bugfixes; and essentially - what things they will now be able to do that they could not presently. I also think you should also be very careful to validate the importance of this change in real terms. I wonder if you should release this change as a plugin or wrapper, and monitor its uptake carefully over several months before merging it into trunk. If immutability has been over-represented as a niche concern, you will have found a way to satisfy these vocal users without changing the course of the library as a whole. |
As a first time user I ended here after some time lost with startOf endOf. These methods are surprisingly mutating! +1 for full immutability |
Maybe another solution is to make it painfully obvious that objects in momentjs are mutable. The documentation does mention it in the cloning section, but it is NOT salient enough. Another solution,If you want mutability use Object Oriented concepts and create object creation using the NEW keyword vs the moment() factory pattern. |
Everybody i have witnessed being new to moment falls into the trap of "moments" being mutated. The current behaviour is not intuitive and this is a long time overdue. Regarding... |
I like @AdamHess' idea about choosing whether you're looking for OOP or immutability. My two cents about why we get confused is this: return values. If |
I think there is a low likelihood of hitting memory use issues for more developers (except for special use cases). Moment's mutability has already bitten me though. Dates should be treated as values like a string or number. +1 for immutable by default |
This same issue exists for PHP, by the way. Do you want to be like PHP?! 😆 In PHP, they solve it by providing If we don't change the default behavior to be immutable, we should at least consider a first-class alternate API like |
I vote for immutability too, I'm willing to help with implementing if that's the plan. I already ran into mutability issues when doing addition and substraction, what makes it more confusing even, is that these methods do return the instance because of chaining. |
By the way how about cloning durations? What's the best way of doing that, couldn't find any that did not feel hacky. |
@ngerritsen I believe the best available option is Re: implementation, #3548 is still an active PR. Hopefully there isn't much code-level work left, but it never hurts to have more eyes to validate big changes. We also need to work on documentation and etc before we can do a major version bump, which will be necessary in order to release a change like this. If you'd like to help with any of this list, I'm sure we'd appreciate it. 😀 |
Just spent an hour trying to identify a subtle bug caused by .startOf() mutating the original date... Thanks for the hard work, momentjs did a great job showing how to build an excellent date lib for JS, but I'm switching to date-fns because of the very subtle nature of this kind of bugs and because after my introduction to some general FP concepts (mostly thanks to React, Redux and ELM) I started to appreciate the general benefits of immutability. For what it's worth, lodash has already followed a more FP approach with lodash/fp. I'd suggest having a look at the way lodash/fp has been implemented because they wrap their existing functions instead of having to complete rewrite everything. Lodash guys are also very very concerned about performance. I also agree with @mull, the real problem is due to the chaining API for me, which IMO has some big design flaws not just in this case but more generally (e.g. jQuery). I'd be better to me if method mutating dates would return undefined (at least this is the general rule I apply to the code I write) |
While moment.frozen namespace is in the works, I'd recommend --as previous poster suggested-- to just use date-fns. |
Just fixed another bug due to mutable moments 🎉 |
Tangentially related, it would be awesome if 3.0 could move to ES6 classes: let mom1 = new Moment();
let mom2 = Moment.parse('2019-03-01T14:55');
// etc Such a move could also guide the immutability discussion. I'd say all methods should be immutable with one exception. A method called |
I just started using Moment, and was horrified that this library was not entirely immutable from the start. I am using moment-immutable until this gets fixed. |
@alancnet moment will probably never be changed. It's too much of a change and there is enough push back from users who learned to embrace the current behaviour. Check out their new project: luxon |
For those who'd like to transition from momentjs to a modern approach, please check out https://github.com/you-dont-need/You-Dont-Need-Momentjs |
Very surprised it is mutable! How far is luxon to being a replacement? One of the reasons I'm using Moment.js is timezone support - it is a must for my project. |
@alamothe That question is clearly answered on the website: https://moment.github.io/luxon/docs/manual/moment.html |
Closing this. As others have pointed out, use Luxon if you want a mostly immutable API. Thanks. |
There has been a lot of discussions about this. Here's the proposal:
The following mutable methods will become immutable in 3.0.0:
utc
,local
,zone
,add
,subtract
,startOf
,endOf
,lang
, also induration
:add
,subtract
, andlang
.As a start, all methods will be duplicated with
methodNameMute
variants. We also need immutable variants namedmethodNameImmute
. From 3.0 the plain old methods will start using the immutable option by default.What is debatable is:
lang
be made immutableget
/set
) be made immutable tooThe good part is we can make immutable versions of the methods today and decide later on what to do. If we switch it would also mean the
2.x
branch would be around for quite some time after3.x
is out.@icambron @timrwood @gregwebs @yang @lfnavess @soswow @langalex
The text was updated successfully, but these errors were encountered: