Original address: 2ality.com/2020/05/rec…

Original author: Dr. Axel Rauschmayer

Dr. Axel Rauschmayer recently wrote about two new JavaScript features that are still in Stage1: records and tuples.

Records and tuples are a new proposal (Record & Tuple, github.com/tc39/propos…) , suggest adding two compound primitive types to JavaScript:

  • A Record is an unmodifiable value comparison object
  • A Tuple is an unmodifiable array that compares values

What is comparison by value

Currently, JavaScript only compares by value (comparing content) when comparing raw values such as strings:

> 'abc'= = ='abc'
true
Copy the code

But when objects are compared, they are compared by identity, so objects are only strictly equal to themselves:

> {x: 1.y: 4} = = = {x: 1.y: 4}
false
> ['a'.'b'] = = = ['a'.'b']
false
Copy the code

The record and tuple proposal is designed to allow us to create values of composite types that are compared by value.

For example, you can create a record by prefacing an object literal with a pound sign (#). The record is a compound value compared by value and cannot be modified:

> # {x: 1.y: 4# {} = = =x: 1.y: 4}
true
Copy the code

If we prefix the array literal with a #, we create a tuple, which is an array that can be compared by value and cannot be modified:

> # ['a'.'b'] = = = # ['a'.'b']
true
Copy the code

Compound values that are compared by value are called compound primitive values or compound primitive types.

Records and tuples are primitive types

Using typeof, we can see that records and tuples are primitive types:

> typeof# {x: 1.y: 4}
'record'
> typeof# ['a'.'b']
'tuple'
Copy the code

The contents of records and tuples are limited

  • Record:
    • The key must be a string
    • Values must be original (including records and tuples)
  • Tuples:
    • Elements must be original values (including records and tuples)

Convert objects to records and tuples

> Record({x: 1.y: 4# {})x: 1.y: 4}
> Tuple.from(['a'.'b'# [])'a'.'b']
Copy the code

Note: These are shallow conversions. Record() and tupl.from () throw exceptions if any node in the value tree is not the original value.

Using the record

const record = #{x: 1.y: 4};

// Access properties
assert.equal(record.y, 4);

/ / deconstruction
const {x} = record;
assert.equal(x, 1);

/ / extensionassert.ok( #{... record,x: 3.z: 9# {} = = =x: 3.y: 4.z: 9});
Copy the code

Use a tuple

const tuple = #['a'.'b'];

// Access elements
assert.equal(tuple[1].'b');

// Destruct (tuples are iterable)
const [a] = tuple;
assert.equal(a, 'a');

/ / extension
assert.ok(
  #[...tuple, 'c'] = = = # ['a'.'b'.'c']);

/ / update
assert.ok(
  tuple.with(0.'x') = = = # ['x'.'b']);
Copy the code

Why are values compared by value not modifiable

Some data structures, such as hash maps and search trees, have slots where keys are saved based on their values. If the value of a key changes, the key usually has to be placed in a different slot. This is why it can be used as a key value in JavaScript:

  • Either by value and non-modifiable (original value)
  • Either compare by identity and be modifiable (objects)

The benefits of compounding the original values

Compounding raw values has the following benefits.

  • Depth comparison object, which is a built-in operation that can be invoked by such as ===.
  • Shared value: If the object is modifiable, a deep copy of it is required for safe sharing. For values that cannot be modified, they can be shared directly.
  • Non-destructive updates of data: If you change compound values, since everything is immutable, you create a copy that can be modified, and then you can safely reuse the parts that don’t need to be modified.
  • Used in data structures such as maps and sets: Because two compound raw values with the same content are considered strictly equal everywhere in the language (including the keys as maps and the elements as sets), mapping and aggregation become more useful.

Let’s demonstrate these benefits.

Example: Collections and maps become more useful

Deweighting by sets

With compound primitive values, even compound values (not atomic values like the original) can be de-duplicated:

> [...new Set# ([[3.4#], [3.4#], [5, -1#], [5, -1#]]]] [[3.4#], [5, -1]]
Copy the code

This is not possible with arrays:

> [...new Set([[3.4], [3.4], [5, -1], [5, -1[[]]]]3.4], [3.4], [5, -1], [5, -1]]
Copy the code

The composite key of the map

Since objects are compared by identity, using objects as keys in a (non-weak) map is of little use:

const m = new Map(a); m.set({x: 1.y: 4}, 1);
m.set({x: 1.y: 4}, 2);
assert.equal(m.size, 2)
Copy the code

This is different if you use compound raw values: the mapping created in line (A) below holds the mapping of addresses (records) to people’s names.

const persons = [
  #{
    name: 'Eddie'.address: # {street: '1313 Mockingbird Lane'.city: 'Mockingbird Heights'#,}}, {name: 'Dawn'.address: # {street: '1630 Revello Drive'.city: 'Sunnydale'#,}}, {name: 'Herman'.address: # {street: '1313 Mockingbird Lane'.city: 'Mockingbird Heights'#,}}, {name: 'Joyce'.address: # {street: '1630 Revello Drive'.city: 'Sunnydale',}},];const addressToNames = new Map(a);// (A)
for (const person of persons) {
  if(! addressToNames.has(person.address)) { addressToNames.set(person.address,new Set());
  }
  addressToNames.get(person.address).add(person.name);
}

assert.deepEqual(
  // Convert the Map to an Array with key-value pairs,
  // so that we can compare it via assert.deepEqual().
  [...addressToNames],
  [
    [
      #{
        street: '1313 Mockingbird Lane'.city: 'Mockingbird Heights',},new Set(['Eddie'.'Herman']], [# {street: '1630 Revello Drive'.city: 'Sunnydale',},new Set(['Dawn'.'Joyce']),]]);Copy the code

Example: Effectively equal depth

Process objects with compound attribute values

In the example below, we use the array method. Filter () (line (B)) extracts all entries whose addresses are equal to address (line (A)).

const persons = [
  #{
    name: 'Eddie'.address: # {street: '1313 Mockingbird Lane'.city: 'Mockingbird Heights'#,}}, {name: 'Dawn'.address: # {street: '1630 Revello Drive'.city: 'Sunnydale'#,}}, {name: 'Herman'.address: # {street: '1313 Mockingbird Lane'.city: 'Mockingbird Heights'#,}}, {name: 'Joyce'.address: # {street: '1630 Revello Drive'.city: 'Sunnydale',}},];const address = #{ // (A)
  street: '1630 Revello Drive'.city: 'Sunnydale'}; assert.deepEqual( persons.filter(p= > p.address === address), // (B)
  [
    #{
      name: 'Dawn'.address: # {street: '1630 Revello Drive'.city: 'Sunnydale'#,}}, {name: 'Joyce'.address: # {street: '1630 Revello Drive'.city: 'Sunnydale',}},]);Copy the code

Has the object changed?

When working with cached data, such as previousData in the example below, built-in depth equality allows us to effectively check whether the data has changed.

let previousData;
function displayData(data) {
  if (data === previousData) return;
  / /...
}

displayData(#['Hello'.'world']); / / show
displayData(#['Hello'.'world']); / / no show
Copy the code

test

Most testing frameworks support depth equality to check if a calculation is producing the desired result. For example, the Node.js built-in Assert module has a function called deepEqual(). With compound primitive values, we can directly assert:

function invert(color) {
  return# {red: 255 - color.red,
    green: 255 - color.green,
    blue: 255 - color.blue,
  };
}
assert.ok(
  invert(#{red: 255.green: 153.blue: 51= = = # {})red: 0.green: 102.blue: 204});
Copy the code

Advantages and disadvantages of the new syntax

One drawback of the new syntax is that the character # is already used in many places (such as private fields), and non-alphanumeric characters are somewhat mysterious. Consider the following example:

const della = #{
  name: 'Della'.children: #[
    #{
      name: 'Huey'# {},name: 'Dewey'# {},name: 'Louie',}]};Copy the code

The advantage is that this syntax is relatively concise. For a common structure, of course, the simpler the better. Furthermore, once you get used to the syntax, the mystery naturally diminishes.

In addition to the special literal syntax, you can also use factory functions:

const della = Record({
  name: 'Della'.children: Tuple([
    Record({
      name: 'Huey',
    }),
    Record({
      name: 'Dewey',
    }),
    Record({
      name: 'Louie',})])});Copy the code

If JavaScript supports Tagged Collection Literals (github.com/zkat/propos… , deprecated), this syntax could be improved:

constdella = Record! {name: 'Della'.children: Tuple! [ Record!{name: 'Huey', }, Record! {name: 'Dewey', }, Record! {name: 'Louie',}]};Copy the code

Alas, even with shorter names, the results look a bit messy:

const R = Record;
const T = Tuple;

constdella = R! {name: 'Della'.children: T! [ R!{name: 'Huey', }, R! {name: 'Dewey', }, R! {name: 'Louie',}]};Copy the code

JSON with records and tuples

  • Json.stringify () treats records as objects and tuples as arrays (recursive).
  • Json.parseimmutable is similar to json.parse (), but returns records instead of objects and tuples instead of arrays (recursively).

Future: Will instances of classes compare by value?

I actually prefer to use classes as data containers rather than objects and arrays. Because it can add names to objects. To that end, I hope that in the future there will be a class whose instances are immutable and comparable by value.

It would be even better if we could also deeply and nondestructively update data that contains objects generated by classes of value types.

Further reading

  • Share modifiable state issues and how to avoid them: exploringjs.com/deep-js/ch_…