Should we adopt ImmutableJS as a core design pattern for Dojo 2?
ImmutableJS provides a library for dealing with immutable data.
Immutable data cannot be changed once created, leading to much simpler application development, no defensive copying, and enabling advanced memoization and change detection techniques with simple logic. Persistent data presents a mutative API which does not update the data in-place, but instead always yields new updated data.
Immutable also provides a lazy Seq, allowing efficient chaining of collection methods like map and filter without creating intermediate representations. Create some Seq with Range and Repeat.
Part of the challenge with an immutable system is that it isn't possible to realize it in a pure manner. Eventually, any non-trivial application must mutate some sort of state in order to be useful (consider the need to mutate the state of the video buffer that is required to render content to the screen). In light of that, the question is not about whether an application can be purely immutable, but rather how to determine which aspects should be mutable or not.
Gary Bernhardt have an interesting talk called "boundaries" where he poses the idea of an architecture that is composed of immutable, functional cores that contain state and business logic surrounded by imperative shells that interact with the mutable parts of the environment. He builds a compelling argument that such as system promises to be easier to test and maintain since it separates the areas with a lot of execution paths into the functional core from aspects of the application that require a large number of dependencies, which are pushed into the imperative shell.
Following this principle could give us an answer to the first question posed. We would integrate ImmutableJS into a functional core of the architecture that is not allowed to mutate. This would be where we would store the state and logic of each sub-system. These sub-systems would be wrapped by mutable layers that would allow them to interact with one another in a stateless manner (via message passing). These mutable layers would also be responsible for responding the the changing environment that the application lives in (e.g. events being fired, text being entered, server data being received) and would respond to these mutations be requesting new states from the functional core which would then be used to create the new shape of the application in response to the mutation.
As an example, let's take routing in a single page application. At its core, the Dojo1 router almost implements this system already. In general RouterBase only mutates its state when new routes are registered, which could easily be converted to adopt an immutable model. The only other part of the router that does not align with this architecture lies in the startup() method. It registers a listener for the 'dojo/hashchange' topic that is triggered in response to an external mutation (the change of the browser hash). It would be a trivial thing to extract this into a "Navigator" class that would exist in the imperative shell and, when trigger, create a new router based on the new hash-state in the application. The router could still hold all of the registered callbacks and call them as required or, since they would likely cause mutations in other sub-systems, could be returned via a filtering operation to the imperative shell which could execute them.
The advantage of this separation is that the router would become trivial to test since all it would be doing would be creating copies of itself or filtering registered route handlers based on its own state. Similarly, the navigator would be equally trivial since it would only listen for topics to be published and, when they were, it would create a new router, ask it for the correct callbacks, and then call them.
I think we should clarify something. ImmutableJS isn't an application framework. It a toolset, which would be used appropriately in an application framework. It is one way of actually dealing with change in an application. Instead of having to "watch" things for change, you enforce you can't change them and you simply discard them when you need to replace them.
Adopting it would mean that we would have tools to leverage those appropriately and make a statement that we generally prefer immutability and object diffing instead of dirty polling to deal with changes.
Based on what I have heard elsewhere, I cannot tell whether this decision has already been made, but I will offer my two cents anyway.
After having been bitten hard in the past by changing state, I really like Immutable.js. Using an immutable data architecture would help make Dojo 2 apps easier to follow, and would perhaps even mitigate the need for complex diffing in a virtual DOM. Further, looking at the architecture proposed for widgets, an immutable domain model makes a lot of sense. The main arguments I can think of against using Immutable.js are:
Concerns #1 and #2 are admittedly straw man arguments to an extent, considering that Immutable.js is smart enough to minimize changes to the returned object, and since TypeScript itself makes it easy to know whether we are dealing with plain JS types or Immutable.js objects. #3 definitely is a concern if dstore and dgrid in their current implementations are to have a place in the Dojo 2 architecture. That said, it sounds as if dgrid is being modified for compatibility with Dojo 2, and there is a new (albeit empty) dojo/stores repo, so that may not be a problem.
@mwistrand I don't think the decision has been made. Right now, it is in the widgets proposal, but could be reversed. It is a fair amount of overhead to "enforce" something that in ways the API that surround it should support anyways.
It is also fairly chunky, even in its minified state at runtime. It can also thrash GC quite a lot (because of your point 1). I have gone back and forth on it in my prototyping widgets (which is why, when we get further with stores, I think it will be informative as to its long term value). It certainly does remove the "foot gun" that I think comes with not having to worry about if someone has a reference to something and changes it in a way you weren't aware of.
There are some ways of dealing with large mutation operations in Immutable.JS too that reduces the number of transitory "dead" objects you create, therefore reducing the impact of your point 1.
I revisited this when dealing with the implementation in dojo/widget, and while we tend to manage things like they are Immutable, which raised concerns in my mind if we would use it with a level of overhead for what was a limited benefit of enforcing immutability.
In looking at it though, not only does ImmutableJS provide the enforced immutability, it also provides a more uniform and feature rich API... For example List and Map provide .equals() which makes it easy to deep compare instances of these objects, where as Array and Map (the native equivalents) don't have this functional API and would require extensive branched logic to provide the same functionality.
cc/ @rishson @dylans
For now, we will continue to use ImmutableJS where appropriate. It maybe worth revisiting providing a more consistent API in dojo-core long term if we find we are really challenged with the overhead of ImmutableJS.
@matt-gadd has looked at building applications and our basic application, 22% of the code base is ImmutableJS:
So, let's re-open this issue and discuss further, because that brings it into question again, its usefulness to us.
Before, in parent widgets, we had two types, a Map and a List. We have now unified those interfaces and replaced it with OrderedMap. So for widgets, the only critical change to get rid of the dependency is find a way to provide a functional replacement for OrderedMap.
dojo/core#217 and dojo/core#218 propose adding List and OrderedMap which would then likely provide us with enough functionality to not require ImmutableJS for widgets.
The widget interfaces have been migrated to ES Map for now. The additional functionality can be addressed at some point in the future supported by use cases. So again, closing.