Skip to content

Upgrading to Immutable v3

leebyron edited this page Nov 5, 2014 · 5 revisions

Immutable v3.0.0 is here, and there are a number of new concepts, renamed methods, and other breaking changes. If you've already been using Immutable v2.x you should be aware of the major changes before upgrading your codebase.

Guiding Philosophy

Immutable aims to be Idiomatic JavaScript. More than that, it tries to mirror existing related ES6 specifications. It expands on these specs while attempting to maintain the spirit of these specs.

For example, the APIs of Immutable.Map and Immutable.Set should look very similar to the existing ES6 Map and Set.

This philosophy helped guide the changes to Immutable v3.

Renamed concepts

Some existing concepts in v2 have been renamed in v3:

Iterable (from Sequence)

Where we used to refer to objects with iterators and collection methods as Sequences, we now refer to them as Iterables. JavaScript Arrays and Objects which we used to refer as "seqable" we now refer to as "iterable-like".

This is both more aligned to the vernacular used in ES6, and avoids confusion with the lazy Seq.

List (from Vector)

List is the same indexed, ordered, dense homogeneous collection previously known as Vector.

Based on the spirit of ES6's Map and Set named by the abstract type of collection rather than the specific implemented data structure (e.g., they are not named HashMap or TreeSet); Vector is the name of a specific data structure, whereas List is the type of collection we're describing.

size (from length)

In order to better match the property defined on ES6 Map and Set, Immutable v3 has renamed length to size.

Because length is pretty annoying to code-mod, a warning is logged if you attempt to access length directly. This warning can be disabled with Immutable.Iterable.noLengthWarning = true;. The warning will be removed in a future version.

Three kinds of Iterable.

In v2, we had Sequence and IndexedSequence. Iterable now comes in three specific flavors, describing the behavior of the "key" property in enumeration as well as providing relevant additional methods:

KeyedIterable

Map and OrderedMap extend from this.

Returns true when passed to Iterable.isKeyed(maybeKeyed) or Iterable.isAssociative(maybeAssociative).

Keyed iterables have keys with any value associated directly with each value. If you scramble a keyed iterable, each key and value pair will remain together.

IndexedIterable

List and Stack extend from this.

Returns true when passed to Iterable.isIndexed(maybedIndexed) or Iterable.isAssociative(maybedAssociative).

Indexed iterables have numeric keys which always incrementally count up from 0 as they are enumerated and are not directly-associated to their corresponding value. If you scramble an indexed iterable, the resulting iterable's keys would still count up from 0.

SetIterable

Set extends from this.

Returns false when passed to either Iterable.isKeyed(maybeKeyed), Iterable.isIndexed(maybedIndexed) or Iterable.isAssociative(maybedAssociative).

Set iterables have no keys at all. When enumerated, the second argument is also the value.

Eager operations

Perhaps the biggest conceptual change is that all collection operations (filter, map, take, skip, slice, etc.) are no longer Lazy by default. While laziness is powerful, it can also be surprising. This pitfall has been a constant point of confusion.

In Immutable v2.x.x, a common pattern is to follow a collection operation with a .toXXX() method to convert back into the original type.

// Immutable v2
myMap.filter(somePredicate)
// Seq { ... }
myMap.filter(somePredicate).toMap()
// Map { ... }

In Immutable v3, operations like this are eager, so the explicit conversion is no longer necessary.

// Immutable v3
myMap.filter(somePredicate)
// Map { ... }

Lazy operations are still possible, and in cases where more than one collection method are used together, or the end result should be of a different type, it can result in a performance improvement by removing intermediate allocations.

Seq

Lazy operations on Iterables are described by a Seq. In order to perform lazy operations on a collection, you must first convert it to a Seq with .toSeq(). This is a very inexpensive O(1) operation.

// Immutable v2
myMap.filter(somePred).sort(someComp).toOrderedMap()
// OrderedMap { ... }

In v3, this might produce the same result, however intermediate Immutable Maps are being produced after filter(somePred) and sort(someComp). In fact, since the iteration order of a Map is undefined, sort(someComp) may not produce anything sorted, as the conversion back to Map could disrupt the sorted order.

// Immutable v3
myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()
// OrderedMap { ... }

Here, calling toSeq() before the collection methods ensures that each returns another lazy Seq. The chain of operations will not be executed until the conversion to toOrderedMap() forces them.

Conversion between Iterable types.

Seq comes in the same three flavors as Iterable: KeyedSeq, IndexedSeq, and SetSeq. All Iterables have the methods toKeyedSeq(), toIndexedSeq() and toSetSeq() which allow for conversion between types.

Example: Want a Map of value-to-index in a List for quick lookup?

var lookupMap = myList.toKeyedSeq().flip().toMap();

Constructors

Immutable v2 allowed you to explicitly create collections from iterables with CollectionType.from(iterableLike), or attempt to intelligently do so by calling CollectionType(x, y, z) directly, which could lead to ambiguity when only one argument is provided.

In an effort to better mirror ES6 collection constructors, Immutable v3 collections no longer have ambiguous constructors, nor do they have from() or empty() methods. Indexed and Set constructors do provide of(...values).

// Immutable v2
Vector([1,2,3]);
// Vector [ 1, 2, 3 ]

Vector(1,2,3);
// Vector [ 1, 2, 3 ]

Vector.from([1,2,3]);
// Vector [ 1, 2, 3 ]

Vector.empty();
// Vector []

In v3, these same constructions look like:

// Immutable v3
List([1,2,3]);
// List [ 1, 2, 3 ]

List.of(1,2,3);
// List [ 1, 2, 3 ]

List([1,2,3]);
// List [ 1, 2, 3 ]

List();
// List []

These new strict constructors are a prerequisite to enable eager collection operations, and make them easier to use along-side transducer libraries.

Cursors in contrib/

Cursors are no longer part of the core Immutable library, however the existing implementation lives on as the first addition to the contrib/ directory.

Also, the API for constructing Cursors has changed:

// Immutable v2
var myCursor = myMap.cursor(key, onChange);
var fooCursor = myCursor.get('foo');

The contrib Cursors are constructed directly:

// Immutable v3
var Cursor = require('immutable/contrib/cursor');

var myCursor = Cursor.from(myMap, key, onChange);
var fooCursor = myCursor.get('foo');

The included Cursor implementation does not deserve first-class treatment. I hope to see this open up development of other state-management APIs.

The included implementation of Cursor has no internal dependencies, illustrating how you can extend Immutable in your own libraries.

Additional Changes

  • list.remove(i) is now equivalent to list.splice(i, 1), better mirroring the APIs of dense list collections found in other libraries.
  • XXX.isXXX() predicates exist on all major Iterable types.
    • Iterable.isIterable()
    • Seq.isSeq()
    • Map.isMap()
    • OrderedMap.isOrderedMap()
    • List.isList()
    • Stack.isStack()
    • Set.isSet()
  • groupBy() and countBy() return concrete Map.
  • Added: keyOf() and lastKeyOf() on KeyedIterable are similar to indexOf() and lastIndexOf() on IndexedIterable.
  • Record is closer in form to an Object than Map, and it's constructors have been made more strict.