Auteurs:
Champions:
Conseillers
Étape: 2
Record
et Tuple
Cette proposition introduit deux nouvelles structures de données profondément immuables à JavaScript:
Record
, une structure d'objet profondément immuable #{ x: 1, y: 2 }
Tuple
, une structure en forme de tableau profondément immuable #[1, 2, 3, 4]
Les enregistrements et les tuples ne peuvent contenir que des primitives et d'autres enregistrements et tuples. Vous pouvez considérer les enregistrements et les tuples comme des "primitives composées". En étant complètement basé sur les primitives, et non sur les objets, les enregistrements et les tuples, sont profondément immuables.
Les enregistrements et les tuples prennent en charge les idiomes confortables pour la construction, la manipulation et l'utilisation, similaire à travailler avec des objets et des tableaux. Ils sont comparés profondément par leur contenu, plutôt que par leur identité.
Les moteurs JavaScript peuvent effectuer certaines optimisations sur la construction, la manipulation et la comparaison des enregistrements et des tuples, analogue à la façon dont les chaînes sont souvent mises en œuvre dans les moteurs JS. (Il faut comprendre que ces optimisations ne sont pas garanties.)
Les enregistrements et les tuples visent à être utilisables et compris avec des sur-ensembles de typesystèmes externes tels que TypeScript ou Flow.
Aujourd'hui, les bibliothèques d'utilisateurs implémentent des concepts similaires, tels que Immutable.js. Une proposition antérieure a également été tentée mais abandonnée en raison de la complexité de la proposition et du manque de cas d'utilisation suffisants.
Cette nouvelle proposition est toujours inspirée par cette proposition précédente, mais introduit des changements importants: les enregistrements et les tuples sont désormais profondément immuables. Cette propriété est fondamentalement basée sur l'observation selon laquelle, dans les grands projets, le risque de mélange des structures de données immuables et mutables augmente à mesure que la quantité de données stockées et transmises augmente également afin que vous soyez plus probablement géré . Cela peut introduire des bogues difficiles à trouver.
En tant que structure de données intégrée et profondément immuable, cette proposition offre également quelques avantages d'utilisation par rapport aux bibliothèques des terres d'utilisateur:
IMMER est une approche notable des structures de données immuables et prescrit un schéma de manipulation par le biais des producteurs et des réducteurs. Il ne fournit cependant pas de types de données immuables, car il génère des objets congelés. Ce même motif peut être adapté aux structures définies dans cette proposition en plus des objets congelés.
L'égalité profonde telle que définie dans les bibliothèques d'utilisateurs peut varier considérablement, en partie en raison de références possibles aux objets mutables. En dessinant une ligne dure sur uniquement les primitives, les enregistrements et les tuples en profondeur et les récursiteurs à travers toute la structure, cette proposition définit la sémantique simple et unifiée pour les comparaisons.
Record
const proposal = # {
id : 1234 ,
title : "Record & Tuple proposal" ,
contents : `...` ,
// tuples are primitive types so you can put them in records:
keywords : # [ "ecma" , "tc39" , "proposal" , "record" , "tuple" ] ,
} ;
// Accessing keys like you would with objects!
console . log ( proposal . title ) ; // Record & Tuple proposal
console . log ( proposal . keywords [ 1 ] ) ; // tc39
// Spread like objects!
const proposal2 = # {
... proposal ,
title : "Stage 2: Record & Tuple" ,
} ;
console . log ( proposal2 . title ) ; // Stage 2: Record & Tuple
console . log ( proposal2 . keywords [ 1 ] ) ; // tc39
// Object functions work on Records:
console . log ( Object . keys ( proposal ) ) ; // ["contents", "id", "keywords", "title"]
Ouvert dans l'aire de jeux
Les fonctions peuvent gérer les enregistrements et les objets de la même manière:
const ship1 = # { x : 1 , y : 2 } ;
// ship2 is an ordinary object:
const ship2 = { x : - 1 , y : 3 } ;
function move ( start , deltaX , deltaY ) {
// we always return a record after moving
return # {
x : start . x + deltaX ,
y : start . y + deltaY ,
} ;
}
const ship1Moved = move ( ship1 , 1 , 0 ) ;
// passing an ordinary object to move() still works:
const ship2Moved = move ( ship2 , 3 , - 1 ) ;
console . log ( ship1Moved === ship2Moved ) ; // true
// ship1 and ship2 have the same coordinates after moving
Ouvert dans l'aire de jeux
Voir plus d'exemples ici.
Tuple
const measures = # [ 42 , 12 , 67 , "measure error: foo happened" ] ;
// Accessing indices like you would with arrays!
console . log ( measures [ 0 ] ) ; // 42
console . log ( measures [ 3 ] ) ; // measure error: foo happened
// Slice and spread like arrays!
const correctedMeasures = # [
... measures . slice ( 0 , measures . length - 1 ) ,
- 1
] ;
console . log ( correctedMeasures [ 0 ] ) ; // 42
console . log ( correctedMeasures [ 3 ] ) ; // -1
// or use the .with() shorthand for the same result:
const correctedMeasures2 = measures . with ( 3 , - 1 ) ;
console . log ( correctedMeasures2 [ 0 ] ) ; // 42
console . log ( correctedMeasures2 [ 3 ] ) ; // -1
// Tuples support methods similar to Arrays
console . log ( correctedMeasures2 . map ( x => x + 1 ) ) ; // #[43, 13, 68, 0]
Ouvert dans l'aire de jeux
De même qu'avec les enregistrements, nous pouvons traiter les tuples comme un tableau:
const ship1 = # [ 1 , 2 ] ;
// ship2 is an array:
const ship2 = [ - 1 , 3 ] ;
function move ( start , deltaX , deltaY ) {
// we always return a tuple after moving
return # [
start [ 0 ] + deltaX ,
start [ 1 ] + deltaY ,
] ;
}
const ship1Moved = move ( ship1 , 1 , 0 ) ;
// passing an array to move() still works:
const ship2Moved = move ( ship2 , 3 , - 1 ) ;
console . log ( ship1Moved === ship2Moved ) ; // true
// ship1 and ship2 have the same coordinates after moving
Ouvert dans l'aire de jeux
Voir plus d'exemples ici.
Comme indiqué avant l'enregistrement et le tuple sont profondément immuables: tenter d'insérer un objet en eux se traduira par une énergie type:
const instance = new MyClass ( ) ;
const constContainer = # {
instance : instance
} ;
// TypeError: Record literals may only contain primitives, Records and Tuples
const tuple = # [ 1 , 2 , 3 ] ;
tuple . map ( x => new MyClass ( x ) ) ;
// TypeError: Callback to Tuple.prototype.map may only return primitives, Records or Tuples
// The following should work:
Array . from ( tuple ) . map ( x => new MyClass ( x ) )
Cela définit les nouveaux morceaux de syntaxe ajoutés à la langue avec cette proposition.
Nous définissons une expression d'enregistrement ou de tuple en utilisant le modificateur #
devant des expressions d'objet ou de tableau autrement normales.
# { }
# { a : 1 , b : 2 }
# { a : 1 , b : # [ 2 , 3 , # { c : 4 } ] }
# [ ]
# [ 1 , 2 ]
# [ 1 , 2 , # { a : 3 } ]
Les trous sont empêchés dans la syntaxe, contrairement aux tableaux, qui permettent des trous. Voir le numéro 84 pour plus de discussions.
const x = # [ , ] ; // SyntaxError, holes are disallowed by syntax
L'utilisation de l'identifiant __proto__
comme propriété est empêchée dans la syntaxe. Voir le numéro n ° 46 pour plus de discussions.
const x = # { __proto__ : foo } ; // SyntaxError, __proto__ identifier prevented by syntax
const y = # { [ "__proto__" ] : foo } ; // valid, creates a record with a "__proto__" property.
Les méthodes concises sont interdites dans la syntaxe enregistrée.
# { method ( ) { } } // SyntaxError
Les enregistrements ne peuvent avoir que des touches de chaîne, pas des clés de symbole, en raison des problèmes décrits dans le n ° 15. La création d'un enregistrement avec une clé de symbole est une TypeError
.
const record = # { [ Symbol ( ) ] : # { } } ;
// TypeError: Record may only have string as keys
Les enregistrements et les tuples ne peuvent contenir que les primitives et autres enregistrements et tuples. Tenter de créer un Record
ou Tuple
qui contient un Object
( null
n'est pas un objet) ou une Function
lance un TypeError
.
const obj = { } ;
const record = # { prop : obj } ; // TypeError: Record may only contain primitive values
L'égalité des enregistrements et des tuples fonctionne comme celle des autres types primitifs JS comme les valeurs booléennes et de chaîne, en comparant par contenu, pas sur l'identité:
assert ( # { a : 1 } === # { a : 1 } ) ;
assert ( # [ 1 , 2 ] === # [ 1 , 2 ] ) ;
Ceci est distinct de la façon dont l'égalité fonctionne pour les objets JS: la comparaison des objets observera que chaque objet est distinct:
assert ( { a : 1 } !== { a : 1 } ) ;
assert ( Object ( # { a : 1 } ) !== Object ( # { a : 1 } ) ) ;
assert ( Object ( # [ 1 , 2 ] ) !== Object ( # [ 1 , 2 ] ) ) ;
L'ordre d'insertion des clés d'enregistrement n'affecte pas l'égalité des enregistrements, car il n'y a aucun moyen d'observer l'ordre original des clés, car ils sont implicitement triés:
assert ( # { a : 1 , b : 2 } === # { b : 2 , a : 1 } ) ;
Object . keys ( # { a : 1 , b : 2 } ) // ["a", "b"]
Object . keys ( # { b : 2 , a : 1 } ) // ["a", "b"]
Si leur structure et leur contenu sont profondément identiques, alors les valeurs Record
et Tuple
considérées comme égales selon toutes les opérations d'égalité: Object.is
, ==
, ===
, et l'algorithme interne SameValueZero (utilisé pour comparer les clés des cartes et des ensembles) . Ils diffèrent en termes de traitement -0
traité:
Object.is
traite -0
et 0
comme inégal==
, ===
et SameValueZero Treat -0
avec 0
comme égal Notez que ==
et ===
sont plus directs sur les autres types de valeurs imbriquées dans les enregistrements et les tuples - retourner true
si et seulement si le contenu est identique (à l'exception de 0
/ -0
). Cette directivité a des implications pour NaN
ainsi que les comparaisons entre les types. Voir des exemples ci-dessous.
Voir une discussion plus approfondie dans le n ° 65.
assert ( # { a : 1 } === # { a : 1 } ) ;
assert ( # [ 1 ] === # [ 1 ] ) ;
assert ( # { a : - 0 } === # { a : + 0 } ) ;
assert ( # [ - 0 ] === # [ + 0 ] ) ;
assert ( # { a : NaN } === # { a : NaN } ) ;
assert ( # [ NaN ] === # [ NaN ] ) ;
assert ( # { a : - 0 } == # { a : + 0 } ) ;
assert ( # [ - 0 ] == # [ + 0 ] ) ;
assert ( # { a : NaN } == # { a : NaN } ) ;
assert ( # [ NaN ] == # [ NaN ] ) ;
assert ( # [ 1 ] != # [ "1" ] ) ;
assert ( ! Object . is ( # { a : - 0 } , # { a : + 0 } ) ) ;
assert ( ! Object . is ( # [ - 0 ] , # [ + 0 ] ) ) ;
assert ( Object . is ( # { a : NaN } , # { a : NaN } ) ) ;
assert ( Object . is ( # [ NaN ] , # [ NaN ] ) ) ;
// Map keys are compared with the SameValueZero algorithm
assert ( new Map ( ) . set ( # { a : 1 } , true ) . get ( # { a : 1 } ) ) ;
assert ( new Map ( ) . set ( # [ 1 ] , true ) . get ( # [ 1 ] ) ) ;
assert ( new Map ( ) . set ( # [ - 0 ] , true ) . get ( # [ 0 ] ) ) ;
Record
et Tuple
En général, vous pouvez traiter des enregistrements comme des objets. Par exemple, l'espace de noms Object
et l'opérateur in
Work avec Records
.
const keysArr = Object . keys ( # { a : 1 , b : 2 } ) ; // returns the array ["a", "b"]
assert ( keysArr [ 0 ] === "a" ) ;
assert ( keysArr [ 1 ] === "b" ) ;
assert ( keysArr !== # [ "a" , "b" ] ) ;
assert ( "a" in # { a : 1 , b : 2 } ) ;
Record
et Tuple
Les développeurs JS n'auront généralement pas à penser aux objets d'enregistrement Record
et Tuple
, mais ils sont un élément clé de la façon dont les enregistrements et les tuples fonctionnent "sous le capot" dans la spécification JavaScript.
Accéder à un enregistrement ou à un tuple via .
ou []
suit la sémantique GetValue
typique, qui se convertit implicitement en une instance du type de wrapper correspondant. Vous pouvez également faire la conversion explicitement via Object()
:
Object(record)
crée un objet de wrapper d'enregistrementObject(tuple)
crée un objet de wrapper Tuple (On pourrait imaginer que new Record
ou new Tuple
pourrait créer ces emballages, comme le font new Number
et new String
, mais les enregistrements et les tuples suivent la nouvelle convention définie par symbole et bigint, ce qui fait lancer ces cas, car ce n'est pas le chemin que nous voulons Encouragez les programmeurs à prendre.)
Les objets d'enregistrement d'enregistrement et de tuple ont toutes leurs propres propriétés avec les attributs writable: false, enumerable: true, configurable: false
. L'objet wrapper n'est pas extensible. Tous assemblés, ils se comportent comme des objets gelés. Ceci est différent des objets de wrapper existants en JavaScript, mais il est nécessaire de donner les types d'erreurs que vous attendez des manipulations ordinaires sur les enregistrements et les tuples.
Une instance d' Record
a les mêmes clés et valeurs que la valeur record
sous-jacente. Le __proto__
de chacun de ces objets de wrapper d'enregistrement est null
(discussion: # 71).
Une instance de Tuple
a des clés qui sont des entiers correspondant à chaque index dans la valeur tuple
sous-jacente. La valeur pour chacune de ces clés est la valeur correspondante dans le tuple
d'origine. De plus, il existe une clé length
non dénuable. Dans l'ensemble, ces propriétés correspondent à celles de l'objet de wrapper String
. C'est-à-dire, Object.getOwnPropertyDescriptors(Object(#["a", "b"]))
et Object.getOwnPropertyDescriptors(Object("ab"))
chacun renvoie un objet qui ressemble à ceci:
{
"0" : {
"value" : " a " ,
"writable" : false ,
"enumerable" : true ,
"configurable" : false
},
"1" : {
"value" : " b " ,
"writable" : false ,
"enumerable" : true ,
"configurable" : false
},
"length" : {
"value" : 2 ,
"writable" : false ,
"enumerable" : false ,
"configurable" : false
}
}
Le __proto__
des objets de wrapper Tuple est Tuple.prototype
. Notez que si vous travaillez sur différents objets globaux JavaScript ("Realms"), le Tuple.prototype
est sélectionné en fonction du royaume actuel lorsque la conversion d'objet est effectuée, de la même manière que le .prototype
des autres primitives se comporte - c'est non attaché à la valeur du tuple lui-même. Tuple.prototype
contient diverses méthodes, analogues aux tableaux.
Pour l'intégrité, l'indexation numérique hors limites sur les tuples renvoie undefined
, plutôt que de transférer à travers la chaîne prototype, comme avec les tapedArrays. La recherche de clés de propriété non numériques se transmet à Tuple.prototype
, ce qui est important pour trouver leurs méthodes de type tableau.
Record
et Tuple
Les valeurs Tuple
ont une fonctionnalité largement analogue au Array
. De même, les valeurs Record
sont prises en charge par différentes méthodes statiques Object
.
assert . deepEqual ( Object . keys ( # { a : 1 , b : 2 } ) , [ "a" , "b" ] ) ;
assert ( # [ 1 , 2 , 3 ] . map ( x => x * 2 ) , # [ 2 , 4 , 6 ] ) ;
Voir l'annexe pour en savoir plus sur les espaces de noms Record
et Tuple
.
Vous pouvez convertir des structures à l'aide Record()
, Tuple()
(avec l'opérateur d'étalement), Record.fromEntries()
ou Tuple.from()
:
const record = Record ( { a : 1 , b : 2 , c : 3 } ) ;
const record2 = Record . fromEntries ( [ [ "a" , 1 ] , # [ "b" , 2 ] , { 0 : 'c' , 1 : 3 } ] ) ; // note that any iterable of entries will work
const tuple = Tuple ( ... [ 1 , 2 , 3 ] ) ;
const tuple2 = Tuple . from ( [ 1 , 2 , 3 ] ) ; // note that an iterable will also work
assert ( record === # { a : 1 , b : 2 , c : 3 } ) ;
assert ( tuple === # [ 1 , 2 , 3 ] ) ;
Record ( { a : { } } ) ; // TypeError: Can't convert Object with a non-const value to Record
Tuple . from ( [ { } , { } , { } ] ) ; // TypeError: Can't convert Iterable with a non-const value to Tuple
Notez que Record()
, Tuple()
Tuple.from()
Record.fromEntries()
. Les références d'objets imbriquées provoqueraient une émergente. Il appartient à l'appelant de convertir les structures intérieures de toutes les manières appropriées pour l'application.
Remarque : Le projet de proposition actuel ne contient pas de routines de conversion récursives, seulement des routines peu profondes. Voir discussion dans # 122
Comme les tableaux, les tuples sont itérables.
const tuple = # [ 1 , 2 ] ;
// output is:
// 1
// 2
for ( const o of tuple ) { console . log ( o ) ; }
De façon similaire aux objets, les enregistrements ne sont que dans la conjonction avec des API comme Object.entries
.
const record = # { a : 1 , b : 2 } ;
// TypeError: record is not iterable
for ( const o of record ) { console . log ( o ) ; }
// Object.entries can be used to iterate over Records, just like for Objects
// output is:
// a
// b
for ( const [ key , value ] of Object . entries ( record ) ) { console . log ( key ) }
JSON.stringify(record)
est équivalent à appeler JSON.stringify
sur l'objet résultant de la conversion récursive de l'enregistrement en un objet qui ne contient aucun enregistrement ni tuples.JSON.stringify(tuple)
équivaut à appeler JSON.stringify
sur le tableau résultant de la conversion récursive du tuple en un tableau qui ne contient aucun enregistrement ni tuples. JSON . stringify ( # { a : # [ 1 , 2 , 3 ] } ) ; // '{"a":[1,2,3]}'
JSON . stringify ( # [ true , # { a : # [ 1 , 2 , 3 ] } ] ) ; // '[true,{"a":[1,2,3]}]'
Veuillez consulter https://github.com/tc39/proposal-json-parseimmutable
Tuple.prototype
Tuple
prend en charge les méthodes d'instance similaires à la table avec quelques modifications:
Tuple.prototype.push
.Tuple.prototype.withAt
. L'annexe contient une description complète du prototype de Tuple
.
typeof
typeof
identifie les enregistrements et les tuples comme des types distincts:
assert ( typeof # { a : 1 } === "record" ) ;
assert ( typeof # [ 1 , 2 ] === "tuple" ) ;
Map
| Set
| WeakMap
| WeakSet
} Il est possible d'utiliser un Record
ou Tuple
comme clé dans une Map
et comme valeur dans un Set
. Lorsque vous utilisez un Record
ou Tuple
ici, ils sont comparés par valeur.
Il n'est pas possible d'utiliser un Record
ou Tuple
comme clé dans un WeakMap
ou une valeur dans un WeakSet
, car Records
et Tuple
ne sont pas Objects
, et leur durée de vie n'est pas observable.
const record1 = # { a : 1 , b : 2 } ;
const record2 = # { a : 1 , b : 2 } ;
const map = new Map ( ) ;
map . set ( record1 , true ) ;
assert ( map . get ( record2 ) ) ;
const record1 = # { a : 1 , b : 2 } ;
const record2 = # { a : 1 , b : 2 } ;
const set = new Set ( ) ;
set . add ( record1 ) ;
set . add ( record2 ) ;
assert ( set . size === 1 ) ;
const record = # { a : 1 , b : 2 } ;
const weakMap = new WeakMap ( ) ;
// TypeError: Can't use a Record as the key in a WeakMap
weakMap . set ( record , true ) ;
const record = # { a : 1 , b : 2 } ;
const weakSet = new WeakSet ( ) ;
// TypeError: Can't add a Record to a WeakSet
weakSet . add ( record ) ;
L'un des principaux avantages de la proposition de dossiers et de tuples est qu'ils sont comparés par leur contenu, et non leur identité. Dans le même temps, ===
dans JavaScript sur les objets a une sémantique très claire et cohérente: comparer les objets par identité. Faire des enregistrements et des primitifs permettant une comparaison en fonction de leurs valeurs.
À un niveau élevé, la distinction objet / primitive aide à former une ligne dure entre le monde profondément immuable, sans contexte et sans identité et le monde des objets mutables au-dessus. Cette division de catégorie rend la conception et le modèle mental plus claire.
Une alternative à la mise en œuvre de l'enregistrement et du tuple en tant que primitives serait d'utiliser la surcharge de l'opérateur pour obtenir un résultat similaire, en implémentant un opérateur d'égalité abstrait surchargé ( ==
) qui compare profondément les objets. Bien que cela soit possible, il ne satisfait pas le cas d'utilisation complet, car la surcharge de l'opérateur ne fournit pas de remplacement pour l'opérateur ===
. Nous voulons que l'opérateur strict d'égalité ( ===
) soit une vérification fiable de "l'identité" pour les objets et la "valeur observable" (modulo -0 / + 0 / nan) pour les types primitifs.
Une autre option consiste à effectuer ce que l'on appelle des internats : nous suivons les objets d'enregistrement ou de tuples à l'échelle mondiale et si nous essayons d'en créer un nouveau qui se trouve être identique à un objet d'enregistrement existant, nous référons maintenant cet enregistrement existant au lieu d'en créer un nouveau. C'est essentiellement ce que fait le polyfill. Nous assimilons maintenant la valeur et l'identité. Cette approche crée des problèmes une fois que nous étendons ce comportement dans plusieurs contextes JavaScript et ne donnerait pas d'immuabilité profonde par nature et il est particulièrement lent, ce qui ferait de l'utilisation d'enregistrer et de tuple un choix négatif.
L'enregistrement et le Tuple sont construits pour interagir avec les objets et les tableaux: vous pouvez les lire exactement de la même manière que vous le feriez avec les objets et les tableaux. Le principal changement réside dans l'immuabilité profonde et la comparaison par valeur au lieu de l'identité.
Les développeurs utilisés pour manipuler des objets de manière immuable (comme la transformation des morceaux de Redux) pourront continuer à faire les mêmes manipulations qu'ils faisaient sur des objets et des tableaux, cette fois, avec plus de garanties.
Nous allons faire des recherches empiriques par le biais d'entretiens et d'enquêtes pour déterminer si cela fonctionne comme nous le pensons.
.get()
/ .set()
comme Immutable.js?Si nous voulons garder l'accès à l'enregistrement et au tuple similaire aux objets et aux tableaux comme décrit dans la section précédente, nous ne pouvons pas compter sur des méthodes pour effectuer cet accès. Cela nous obligerait à brancher le code lorsque vous essayez de créer une fonction "générique" capable de prendre des objets / tableaux / enregistrements / tuples.
Voici un exemple de fonction qui prend en charge les enregistrements immuables.js et les objets ordinaires:
const ProfileRecord = Immutable . Record ( {
name : "Anonymous" ,
githubHandle : null ,
} ) ;
const profileObject = {
name : "Rick Button" ,
githubHandle : "rickbutton" ,
} ;
const profileRecord = ProfileRecord ( {
name : "Robin Ricard" ,
githubHandle : "rricard" ,
} ) ;
function getGithubUrl ( profile ) {
if ( Immutable . Record . isRecord ( profile ) ) {
return `https://github.com/ ${
profile . get ( "githubHandle" )
} ` ;
}
return `https://github.com/ ${
profile . githubHandle
} ` ;
}
console . log ( getGithubUrl ( profileObject ) ) // https://github.com/rickbutton
console . log ( getGithubUrl ( profileRecord ) ) // https://github.com/rricard
Ceci est sujet aux erreurs car les deux branches pourraient facilement se synchroniser au fil du temps ...
Voici comment nous rédigerions cette fonction en prenant des enregistrements de cette proposition et des objets ordinaires:
const profileObject = {
name : "Rick Button" ,
githubHandle : "rickbutton" ,
} ;
const profileRecord = # {
name : "Robin Ricard" ,
githubHandle : "rricard" ,
} ;
function getGithubUrl ( profile ) {
return `https://github.com/ ${
profile . githubHandle
} ` ;
}
console . log ( getGithubUrl ( profileObject ) ) // https://github.com/rickbutton
console . log ( getGithubUrl ( profileRecord ) ) // https://github.com/rricard
Cette fonction prend en charge les objets et les enregistrements dans un seul chemin de code et ne forçant pas le consommateur à choisir les structures de données à utiliser.
Pourquoi devons-nous soutenir les deux en même temps de toute façon? Il s'agit principalement d'éviter une division écosystème. Disons que nous utilisons Immutable.js pour faire notre gestion de l'État, mais nous devons nourrir notre état à quelques bibliothèques externes qui ne le soutiennent pas:
state . jobResult = Immutable . fromJS (
ExternalLib . processJob (
state . jobDescription . toJS ( )
)
) ;
toJS()
et fromJS()
peuvent finir par être des opérations très coûteuses en fonction de la taille des sous-structures. Une scission d'écosystème signifie des conversions qui, à leur tour, signifient des problèmes de performances possibles.
La syntaxe proposée améliore considérablement l'ergonomie de l'utilisation Record
et Tuple
dans le code. Par exemple:
// with the proposed syntax
const record = # {
a : # {
foo : "string" ,
} ,
b : # {
bar : 123 ,
} ,
c : # {
baz : # {
hello : # [
1 ,
2 ,
3 ,
] ,
} ,
} ,
} ;
// with only the Record/Tuple globals
const record = Record ( {
a : Record ( {
foo : "string" ,
} ) ,
b : Record ( {
bar : 123 ,
} ) ,
c : Record ( {
baz : Record ( {
hello : Tuple (
1 ,
2 ,
3 ,
) ,
} ) ,
} ) ,
} ) ;
La syntaxe proposée est destinée à être plus simple et plus facile à comprendre, car elle est intentionnellement similaire à la syntaxe pour les littéraux d'objet et de tableau. Cela profite de la familiarité existante de l'utilisateur avec les objets et les tableaux. De plus, le deuxième exemple introduit des littéraux d'objets temporaires supplémentaires, ce qui ajoute à la complexité de l'expression.
L'utilisation d'un mot-clé comme préfixe de la syntaxe littérale d'objet / tableau standard présente des problèmes autour de la compatibilité vers l'arrière. De plus, la réutilisation des mots clés existants peut introduire l'ambiguïté.
ECMAScript définit un ensemble de mots clés réservés qui peuvent être utilisés pour les extensions futures de la langue. La définition d'un nouveau mot-clé qui n'est pas déjà réservé est théoriquement possible, mais nécessite un effort important pour valider que le nouveau mot-clé ne casse probablement pas la compatibilité en arrière.
L'utilisation d'un mot-clé réservé facilite ce processus, mais ce n'est pas une solution parfaite car il n'y a pas de mots clés réservés qui correspondent à "l'intention" de la fonction, autre que const
. Le mot-clé const
est également délicat, car il décrit un concept similaire (Imutabilité de référence variable) alors que cette proposition a l'intention d'ajouter de nouvelles structures de données immuables. Bien que l'immuabilité soit le fil conducteur entre ces deux caractéristiques, il y a eu des commentaires de la communauté importants qui indiquent que l'utilisation const
dans les deux contextes n'est pas souhaitable.
Au lieu d'utiliser un mot-clé, {| |}
et [||]
ont été suggérés comme alternatives possibles. Actuellement, le groupe Champion se penche vers #[]
/ #{}
, mais la discussion est en cours dans # 10.
La définition de l'enregistrement et du tuple en tant que primitives composées force tout en enregistrement et en tuple pour ne pas être des objets. Cela est livré avec certains inconvénients (référencer les objets devient plus difficile mais est toujours possible) mais aussi plus de garanties pour éviter les erreurs de programmation courantes.
const object = {
a : {
foo : "bar" ,
} ,
} ;
Object . freeze ( object ) ;
func ( object ) ;
// func is able to mutate object’s keys even if object is frozen
Dans l'exemple ci-dessus, nous essayons de créer une garantie d'immuabilité avec Object.freeze
. Malheureusement, puisque nous n'avons pas congelé profondément l'objet, rien ne nous dit que object.a
n'a pas été touché. Avec le record et le tuple, cette contrainte est par nature et il ne fait aucun doute que la structure est intacte:
const record = # {
a : # {
foo : "bar" ,
} ,
} ;
func ( record ) ;
// runtime guarantees that record is entirely unchanged
assert ( record . a . foo === "bar" ) ;
Enfin, l'immuabilité profonde supprime la nécessité d'un schéma commun qui consiste à des objets en profondeur pour conserver les garanties:
const clonedObject = JSON . parse ( JSON . stringify ( object ) ) ;
func ( clonedObject ) ;
// now func can have side effects on clonedObject, object is untouched
// but at what cost?
assert ( object . a . foo === "bar" ) ;
En général, l'opérateur de diffusion fonctionne bien pour cela:
// Add a Record field
let rec = # { a : 1 , x : 5 }
# { ... rec , b : 2 } // #{ a: 1, b: 2, x: 5 }
// Change a Record field
# { ... rec , x : 6 } // #{ a: 1, x: 6 }
// Append to a Tuple
let tup = # [ 1 , 2 , 3 ] ;
# [ ... tup , 4 ] // #[1, 2, 3, 4]
// Prepend to a Tuple
# [ 0 , ... tup ] // #[0, 1, 2, 3]
// Prepend and append to a Tuple
# [ 0 , ... tup , 4 ] // #[0, 1, 2, 3, 4]
Et si vous modifiez quelque chose dans un tuple, le Tuple.prototype.with
avec la méthode fonctionne:
// Change a Tuple index
let tup = # [ 1 , 2 , 3 ] ;
tup . with ( 1 , 500 ) // #[1, 500, 3]
Certaines manipulations de «chemins profonds» peuvent être un peu gênants. Pour cela, les propriétés du chemin profond pour la proposition d'enregistrements ajoutent une syntaxe de sténographie supplémentaire pour enregistrer les littéraux.
Nous développons la proposition de propriétés de chemin Deep en tant que proposition de suivi distincte car nous ne le voyons pas comme un cœur de l'utilisation des enregistrements, qui fonctionnent bien indépendamment. C'est le type d'addition syntaxique qui fonctionnerait bien pour prototyper au fil du temps dans les transpiles, et où nous avons de nombreux points de décision qui n'ont pas à voir avec les enregistrements et les tuples (par exemple, comment cela fonctionne avec les objets).
Nous avons discuté avec les champions de collections Readonly, et les deux groupes conviennent que ce sont des compléments:
Aucun des deux n'est un sous-ensemble de l'autre en termes de fonctionnalité. Au mieux, ils sont parallèles, tout comme chaque proposition est parallèle à d'autres types de collection dans la langue.
Ainsi, les deux groupes de champions se sont résolus pour s'assurer que les propositions sont en parallèle les unes des autres . Par exemple, cette proposition ajoute un nouveau Tuple.prototype.withReversed
. L'idée serait de vérifier, pendant le processus de conception, si cette signature aurait également du sens pour les tableaux en lecture seule (si ceux-ci existent): nous avons extrait ces nouvelles méthodes au tableau de changement par proposition de copie, afin que nous puissions discuter d'une API qui construit un modèle mental cohérent et partagé.
Dans les projets de proposition actuels, il n'y a pas de types de chevauchement pour le même type de données, mais les deux propositions pourraient se développer dans ces directions à l'avenir, et nous essayons de penser à ces choses à l'avance. Qui sait, un jour TC39 pourrait décider d'ajouter des types de records et de records primitifs, comme les versions profondément immuables de Set and Map! Et ceux-ci seraient en parallèle avec les types de collections en lecture.
TC39 discute depuis longtemps des "types de valeur", qui constitueraient une sorte de déclaration de classe pour un type primitif, depuis plusieurs années, de temps en temps. Une version antérieure de cette proposition a même fait une tentative. Cette proposition essaie de commencer simple et minimal, ne fournissant que les structures de base. L'espoir est qu'il pourrait fournir le modèle de données pour une future proposition de cours.
Cette proposition est vaguement liée à un ensemble plus large de propositions, y compris la surcharge de l'opérateur et les littéraux numériques étendus: tous conspirent pour fournir un moyen pour les types définis par l'utilisateur de faire de même que BigInt. Cependant, l'idée est d'ajouter ces fonctionnalités si nous déterminons qu'ils sont motivées indépendamment.
Si nous avions des types primitifs / valeurs définis par l'utilisateur, il pourrait être logique de les utiliser dans des fonctionnalités intégrées, telles que CSS typé OM ou la proposition temporelle. Cependant, c'est loin à l'avenir, si jamais cela se produit; Pour l'instant, il fonctionne bien pour utiliser des objets pour ces types de fonctionnalités.
Bien que les deux types d'enregistrements se rapportent aux objets et que les deux types de tuples se rapportent aux tableaux, c'est là que la similitude se termine.
Les enregistrements dans TypeScript sont un type d'utilité générique pour représenter un objet prenant un type de clé correspondant à un type de valeur. Ils représentent toujours des objets.
De même, les tuples dans TypeScript sont une notation pour exprimer des types dans un tableau de taille limitée (en commençant par TypeScript 4.0, ils ont une forme variatique). Les tuples en dactylographie sont un moyen d'exprimer des tableaux avec des types hétérogènes. Les tuples ECMAScript peuvent correspondre facilement à des tableaux TS ou à des tuples TS car ils peuvent contenir un nombre indéfini de valeurs du même type ou contenir un nombre limité de valeurs avec différents types.
Les enregistrements TS ou les tuples sont des caractéristiques orthogonales des enregistrements et des tuples ECMAScript et les deux pourraient être exprimés en même temps:
const record : Readonly < Record < string , number > > = # {
foo : 1 ,
bar : 2 ,
} ;
const tuple : readonly [ number , string ] = # [ 1 , "foo" ] ;
Cette proposition ne fait aucune garantie de performance et ne nécessite pas d'optimisations spécifiques dans les implémentations. Sur la base des commentaires des implémenteurs, il est prévu qu'ils implémenteront des opérations communes via des algorithmes "temps linéaire". Cependant, cette proposition n'empêche pas certaines optimisations classiques pour les structures de données purement fonctionnelles, y compris, mais sans s'y limiter:
Ces optimisations sont analogues à la façon dont les moteurs JavaScript modernes gèrent la concaténation des cordes, avec divers types de chaînes internes différents. La validité de ces optimisations repose sur l'inobservabilité de l'identité des enregistrements et des tuples. On ne s'attend pas à ce que tous les moteurs agissent de manière identique en ce qui concerne ces optimisations, mais plutôt, ils prendront chacun des décisions sur les heuristiques particulières à utiliser. Avant l'étape 4 de cette proposition, nous prévoyons de publier un guide pour les meilleures pratiques pour l'utilisation optimisable des enregistrements et les tuples optimisables du moteur transversal, sur la base de l'expérience de mise en œuvre que nous aurons à ce moment-là.
Une nouvelle structure de données de type primitive composée profondément immuable, proposée dans ce document, qui est analogue à l'objet. #{ a: 1, b: 2 }
Une nouvelle structure de données de type primitive composée profondément immuable, proposée dans ce document, qui est analogue au tableau. #[1, 2, 3, 4]
Des valeurs qui agissent comme d'autres primitives JavaScript, mais sont composées d'autres valeurs constituantes. Ce document propose les deux premiers types primitifs composés: Record
et Tuple
.
String
, Number
, Boolean
, undefined
, null
, Symbol
et BigInt
Des choses qui sont composées ou des types primitifs simples. Toutes les primitives en JavaScript partagent certaines propriétés:
Une structure de données qui n'accepte pas les opérations qui les modifient en interne, mais qui ont plutôt des opérations qui renvoient une nouvelle valeur qui est le résultat de l'application de cette opération.
Dans cette proposition, Record
et Tuple
sont des structures de données profondément immuables.
L'opérateur ===
est défini avec l'algorithme de comparaison d'égalité strict. L'égalité stricte fait référence à cette notion particulière d'égalité.
Le partage structurel est une technique utilisée pour limiter l'empreinte mémoire des structures de données immuables. En un mot, lors de l'application d'une opération pour dériver une nouvelle version d'une structure immuable, le partage structurel tentera de maintenir la majeure partie de la structure interne intacte et utilisée par les versions anciennes et dérivées de cette structure. Cela limite considérablement le montant à copier pour dériver la nouvelle structure.