Why was Records & Tuples proposal withdrawn in JavaScript?

Published on

Records & Tuples proposal withdrawn

As many people in JavaScript community know, recently the Records & Tuples proposal was withdrawn. Many people got really upset, some even to the point of blaming the EcmaScript standard committee. Here I want to discuss the possible reasons why this feature was discounted.

What was Records & Tuples proposal about?

Let's first understand what are Records & Tuples before going to deeper technical details.

Records & Tuples are 2 new object-like primitive types. Their 2 main features are:

This is how records and tuples are declared and basically work:

// Tuples are declared similarly to regular arrays, they just have # at the beginning const tuple1 = #[1, 2, 4]; const tuple2 = #[1, 2, 3]; const tuple3 = #[1, 2, 3]; // Records are declared similarly to regular objects, they just have # at the beginning const record1 = #{ a: 1, b: tuple2, }; const record2 = #{ a: 1, b: tuple3, }; const record3 = #{ a: 1, b: tuple2, c: #{ a: tuple2 } }; console.log(typeof tuple1); // "tuple" console.log(typeof record1); // "record" console.log(tuple1 === tuple2); // false, their structures don't match console.log(tuple2 === tuple3); // true, their structures match console.log(record1 === record3); // false, their structures don't match console.log(record1 === record2); // true, their structures match const map = new Map(); map.set(record1, "record1"); // records and tuples can be even used for map keys console.log(map.get(record2)); // "record1", since record1 equals record2 console.log(map.get(record3)); // undefined, since no key with an identical structure was found in the map console.log(tuple1.length); // 3, will work like normal array // Will iterate like on normal array, will print 1, 2, 4 for (const x of tuple1) { console.log(x); } try { // will cause an error on any modification attempt tuple1[1] = 5; } catch (e) { console.error(e); } try { // will cause an error on any modification attempt record2['k'] = 5; } catch (e) { console.error(e); } try { const record4 = #{ a: 1, b: tuple2, // will cause an error since the nested structures must also be immutable primitive values c: { a: tuple2 } }; } catch (e) { console.error(e); }

You can experiment in this playground. Unfortunately the typeof keyword doesn't work correctly, because it's just a polyfill and JavaScript itself doesn't support records and tuples.

Records and tuples can also be made from regular objects:

const record = Record({ a: 4 }); const tuple = Tuple.from([1, 2, 3]); console.log(record, tuple); // #{ a: 4 }, #[1, 2, 3]

There are tons of other features, for more details check the proposal page.

The reasons for the withdrawal

Judging from these examples alone, records & tuples already seem to be an awesome feature, especially for state management. So why was this feature withdrawn?

Adding new fundamental types

The proposal adds 2 fundamental primitive types: record and tuple. Adding new fundamental types is not the same thing as adding a new class or API. Fundamental types affect how the heart of the JavaScript engines works, because JavaScript is a dynamic language, it has to constantly check and handle different operations and coercions with and between different types. New fundamental types will add even more complexity and, possibly, performance overheads (even for code not using records and tuples) to already complicated JavaScript engines.

Deep comparison

Deep comparison also adds complexity in order to work efficiently. One possible optimization is to use structural sharing for the new objects as much as possible. This will attempt to use existing records / tuples as much as possible, which will limit the memory usage and potentially speed up the creation / comparison. Another possible optimization is to use hash values attached to the tuples / records, so this will help to quickly determine if 2 tuples/records are not equal in many cases. However, the consensus is that deep comparison is not guaranteed to work faster than in linear time. Optimizing it without interning looked hard, so implementers were unwilling to commit.

Consistency of equality semantics

Without value-based ===, records and tuples break the long-standing rule that ===, SameValue (same as === except NaN === NaN and 0 !== -0), and SameValueZero (same as === except NaN === NaN) coincide for most types. Keeping that rule meant paying the complexity cost. Breaking it meant teaching the fifth equality relation in JS. Neither option got traction. Also removing value based equality makes records and tuples almost pointless 😒.

Composites might be a better alternative?

There were too many problems and uncertainties with records and tuples proposal in order to progress, thus the feature was eventually withdrawn. There is a new alternative proposal for Composites. Personally, I'm not very impressed with the current implementation yet. It still introduces some inconsistencies such as being compared structurally in Map / Set, but being compared by reference in WeakMap / WeakSet:

const pos1 = Composite({ x: 1, y: 4 }); const pos2 = Composite({ x: 1, y: 4 }); Composite.equal(pos1, pos2); // true const positions = new Set(); // the standard ES Set const weakPositions = new WeakSet(); // the standard ES WeakSet positions.add(pos1); weakPositions.add(pos2); positions.has(pos2); // true, OK, makes sense (at least we wanted this), // but it's handled in a special way instead of the normal reference identity despite it's an object weakPositions.has(pos2); // false, old behavior but now inconsistent with Set

Honestly, I don't know, maybe this is a necessary evil to have structural keys. Anyways, let's see where does this go.





Read previous