Autores:
Campeones:
Asesores
Etapa: 2
Record
y biblioteca estándar Tuple
Esta propuesta introduce dos nuevas estructuras de datos profundamente inmutables a JavaScript:
Record
, una estructura de objeto profundamente inmutable #{ x: 1, y: 2 }
Tuple
, una estructura de matriz profundamente inmutable #[1, 2, 3, 4]
Los registros y las tuplas solo pueden contener primitivas y otros registros y tuplas. Podrías pensar en los registros y tuplas como "primitivas compuestas". Al estar completamente basado en primitivas, no objetos, registros y tuplas son profundamente inmutables.
Los registros y tuples admiten modismos cómodos para la construcción, manipulación y uso, similar al trabajo con objetos y matrices. Se comparan profundamente por su contenido, en lugar de por su identidad.
Los motores JavaScript pueden realizar ciertas optimizaciones en la construcción, manipulación y comparación de registros y tuplas, análogos a la forma en que las cadenas a menudo se implementan en los motores JS. (Debe entenderse que estas optimizaciones no están garantizadas).
Los registros y las tuplas apuntan a ser utilizables y entendidos con superconjuntos de sistemas de tipos externos, como TypeScript o Flow.
Hoy, las bibliotecas de usuarios implementan conceptos similares, como Immutable.js. También se ha intentado una propuesta anterior, pero se ha abandonado debido a la complejidad de la propuesta y la falta de casos de uso suficientes.
Esta nueva propuesta todavía se inspira en esta propuesta anterior, pero presenta algunos cambios significativos: los registros y las tuplas ahora son profundamente inmutables. Esta propiedad se basa fundamentalmente en la observación de que, en grandes proyectos, el riesgo de mezclar estructuras de datos inmutables y mutables crece a medida que la cantidad de datos que se almacenan y pasan también crece, por lo que es más probable que maneje grandes estructuras de registros y tuples. . Esto puede introducir errores difíciles de encontrar.
Como una estructura de datos incorporada y profundamente inmutable, esta propuesta también ofrece algunas ventajas de usabilidad en comparación con las bibliotecas de Userland:
Immer es un enfoque notable para las estructuras de datos inmutables, y prescribe un patrón para la manipulación a través de productores y reductores. Sin embargo, no está proporcionando tipos de datos inmutables, ya que genera objetos congelados. Este mismo patrón se puede adaptar a las estructuras definidas en esta propuesta además de objetos congelados.
La igualdad profunda como se define en las bibliotecas de usuarios puede variar significativamente, en parte debido a las posibles referencias a objetos mutables. Al dibujar una línea dura sobre solo que contiene primitivas, registros y tuplas profundamente, y recurrir a través de toda la estructura, esta propuesta define semántica simple y unificada para las comparaciones.
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"]
Abierto en el patio de recreo
Las funciones pueden manejar registros y objetos de la misma manera:
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
Abierto en el patio de recreo
Vea más ejemplos aquí.
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]
Abierto en el patio de recreo
Del mismo modo que con los registros, podemos tratar las tuplas como una matriz:
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
Abierto en el patio de recreo
Vea más ejemplos aquí.
Como se indica antes, Registro y Tuple son profundamente inmutables: intentar insertar un objeto en ellos dará como resultado un TypeError:
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 ) )
Esto define las nuevas piezas de sintaxis que se agregan al lenguaje con esta propuesta.
Definimos un registro o expresión de tupla utilizando el modificador #
frente a las expresiones de objeto o matriz normales.
# { }
# { a : 1 , b : 2 }
# { a : 1 , b : # [ 2 , 3 , # { c : 4 } ] }
# [ ]
# [ 1 , 2 ]
# [ 1 , 2 , # { a : 3 } ]
Los agujeros se evitan en la sintaxis, a diferencia de las matrices, que permiten agujeros. Vea el número 84 para más discusión.
const x = # [ , ] ; // SyntaxError, holes are disallowed by syntax
El uso del identificador __proto__
como propiedad se evita en la sintaxis. Vea el número 46 para más discusión.
const x = # { __proto__ : foo } ; // SyntaxError, __proto__ identifier prevented by syntax
const y = # { [ "__proto__" ] : foo } ; // valid, creates a record with a "__proto__" property.
Los métodos concisos se no permiten en la sintaxis registrada.
# { method ( ) { } } // SyntaxError
Los registros solo pueden tener teclas de cadena, no teclas de símbolos, debido a los problemas descritos en el #15. Crear un registro con una tecla de símbolo es un TypeError
.
const record = # { [ Symbol ( ) ] : # { } } ;
// TypeError: Record may only have string as keys
Los registros y tuplas solo pueden contener primitivas y otros registros y tuplas. Intentar crear un Record
o Tuple
que contenga un Object
( null
no es un objeto) o una Function
arroja un TypeError
.
const obj = { } ;
const record = # { prop : obj } ; // TypeError: Record may only contain primitive values
La igualdad de registros y tuplas funciona como la de otros tipos primitivos JS como los valores booleanos y de cadena, comparando por contenido, no de identidad:
assert ( # { a : 1 } === # { a : 1 } ) ;
assert ( # [ 1 , 2 ] === # [ 1 , 2 ] ) ;
Esto es distinto de cómo funciona la igualdad para los objetos JS: la comparación de objetos observará que cada objeto es distinto:
assert ( { a : 1 } !== { a : 1 } ) ;
assert ( Object ( # { a : 1 } ) !== Object ( # { a : 1 } ) ) ;
assert ( Object ( # [ 1 , 2 ] ) !== Object ( # [ 1 , 2 ] ) ) ;
El orden de inserción de claves de registro no afecta la igualdad de los registros, porque no hay forma de observar el orden original de las claves, ya que están implícitamente ordenados:
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 su estructura y contenido son profundamente idénticos, entonces los valores Record
y Tuple
considerados iguales de acuerdo con todas las operaciones de igualdad: Object.is
, ==
, ===
, y el algoritmo interno de Valuezero (utilizado para comparar claves de mapas y conjuntos) . Difieren en términos de cómo se trata -0
:
Object.is
trata -0
y 0
como desiguales==
, ===
Y SameValuezero Treat -0
con 0
como igual Tenga en cuenta que ==
y ===
son más directos sobre otros tipos de valores anidados en registros y tuplas, lo que representa true
si y solo si el contenido es idéntico (con la excepción de 0
/ -0
). Esta franqueza tiene implicaciones para NaN
, así como las comparaciones entre los tipos. Ver ejemplos a continuación.
Ver más discusión en el #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
y Tuple
En general, puede tratar registros como objetos. Por ejemplo, el espacio de nombres Object
y el operador in
el operador funcionan con 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
y envoltura Tuple
Los desarrolladores de JS generalmente no tendrán que pensar en los objetos Record
y envoltura Tuple
, pero son una parte clave de cómo funcionan los registros y las tuplas "debajo del capó" en la especificación de JavaScript.
Acceso de un registro o tupla a través de .
o []
sigue la semántica típica GetValue
, que se convierte implícitamente en una instancia del tipo de envoltorio correspondiente. También puede hacer la conversión explícitamente a través Object()
:
Object(record)
crea un objeto de envoltura de registroObject(tuple)
crea un objeto de envoltura de tuple (Uno podría imaginar que new Record
o new Tuple
podrían crear estos envoltorios, como new Number
y new String
. Anime a los programadores a tomar).
Los objetos de registro y envoltorio de tupla tienen todas sus propias propiedades con los atributos writable: false, enumerable: true, configurable: false
. El objeto de envoltura no es extensible. Todos juntos, se comportan como objetos congelados. Esto es diferente de los objetos de contenedor existentes en JavaScript, pero es necesario dar los tipos de errores que esperaría de las manipulaciones ordinarias en registros y tuplas.
Una instancia de Record
tiene las mismas claves y valores que el valor record
subyacente. El __proto__
de cada uno de estos objetos de envoltura de registros es null
(discusión: #71).
Una instancia de Tuple
tiene claves que son enteros correspondientes a cada índice en el valor tuple
subyacente. El valor para cada una de estas claves es el valor correspondiente en la tuple
original. Además, hay una tecla length
no enumerable. En general, estas propiedades coinciden con las del objeto String
Wrapper. Es decir, Object.getOwnPropertyDescriptors(Object(#["a", "b"]))
y Object.getOwnPropertyDescriptors(Object("ab"))
Devuelve un objeto que se ve así:
{
"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
}
}
El __proto__
de los objetos de envoltura de tuple es Tuple.prototype
. Tenga en cuenta que, si está trabajando en diferentes objetos globales de JavaScript ("reinos"), el Tuple.prototype
se selecciona en función del ámbito actual cuando se realiza la conversión de objetos, de manera similar a cómo se comporta el .prototype
de otras primitivas: es no adjunto al valor de la tupla en sí. Tuple.prototype
tiene varios métodos, análogos a las matrices.
Para la integridad, la indexación numérica fuera de los límites en tuplas devuelve undefined
, en lugar de reenviar a través de la cadena prototipo, como con los typedarrays. La búsqueda de claves de propiedad no numérica hacia adelante hasta Tuple.prototype
, lo que es importante encontrar sus métodos similares a la matriz.
Record
y biblioteca estándar Tuple
Los valores Tuple
tienen funcionalidad ampliamente análoga a Array
. Del mismo modo, los valores Record
son compatibles con diferentes métodos estatales Object
.
assert . deepEqual ( Object . keys ( # { a : 1 , b : 2 } ) , [ "a" , "b" ] ) ;
assert ( # [ 1 , 2 , 3 ] . map ( x => x * 2 ) , # [ 2 , 4 , 6 ] ) ;
Vea el apéndice para obtener más información sobre los espacios de nombres Record
y Tuple
.
Puede convertir estructuras usando Record()
, Tuple()
(con el operador spread), Record.fromEntries()
o 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
Tenga en cuenta que Record()
, Tuple()
, Record.fromEntries()
y Tuple.from()
esperan colecciones que consisten en registros, tuplas u otras primitivas (como números, cadenas, etc.). Las referencias de objetos anidados causarían un TipoError. Depende de la persona que llama convertir las estructuras internas de cualquier manera apropiada para la aplicación.
NOTA : El borrador actual de la propuesta no contiene rutinas de conversión recursiva, solo que son poco profundas. Ver discusión en #122
Al igual que las matrices, las tuplas son iterables.
const tuple = # [ 1 , 2 ] ;
// output is:
// 1
// 2
for ( const o of tuple ) { console . log ( o ) ; }
De manera similar a los objetos, los registros solo son iterables en conjunto con API como 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)
es equivalente a llamar JSON.stringify
en el objeto resultante de convertir recursivamente el registro a un objeto que no contiene registros ni tuplas.JSON.stringify(tuple)
es equivalente a llamar a JSON.stringify
en la matriz resultante de convertir recursivamente la tupla en una matriz que no contiene registros ni tuplas. JSON . stringify ( # { a : # [ 1 , 2 , 3 ] } ) ; // '{"a":[1,2,3]}'
JSON . stringify ( # [ true , # { a : # [ 1 , 2 , 3 ] } ] ) ; // '[true,{"a":[1,2,3]}]'
Consulte https://github.com/tc39/proposal-json-parseMutable
Tuple.prototype
Tuple
admite métodos de instancia similares a la matriz con algunos cambios:
Tuple.prototype.push
Method.Tuple.prototype.withAt
. El apéndice contiene una descripción completa del prototipo de Tuple
.
typeof
typeof
identifica registros y tuplas como tipos distintos:
assert ( typeof # { a : 1 } === "record" ) ;
assert ( typeof # [ 1 , 2 ] === "tuple" ) ;
Map
| Set
| WeakMap
| WeakSet
} Es posible usar un Record
o Tuple
como clave en un Map
, y como un valor en un Set
. Al usar un Record
o Tuple
aquí, se comparan por valor.
No es posible usar un Record
o Tuple
como clave en un WeakMap
o como un valor en un WeakSet
, porque Records
y Tuple
no son Objects
, y su vida útil no es 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 ) ;
Un beneficio central de la propuesta de registros y tuplas es que son comparados por su contenido, no por su identidad. Al mismo tiempo, ===
en JavaScript en objetos tiene una semántica muy clara y consistente: comparar los objetos por identidad. Hacer registros y primitivas de tuplas permite la comparación basada en sus valores.
En un alto nivel, el objeto/distinción primitiva ayuda a formar una línea dura entre el mundo profundamente inmutable, libre de contexto y sin identidad y el mundo de los objetos mutables por encima de él. Esta división de categoría deja el diseño y el modelo mental más claro.
Una alternativa a la implementación del registro y la tupla como primitivas sería utilizar la sobrecarga del operador para lograr un resultado similar, mediante la implementación de un operador de igualdad abstracta ( ==
) que compara profundamente los objetos. Si bien esto es posible, no satisface el caso de uso completo, porque la sobrecarga del operador no proporciona una anulación para el operador ===
. Queremos que el operador de igualdad estricto ( ===
) sea una verificación confiable de "identidad" para objetos y "valor observable" (modulo -0/+0/nan) para tipos primitivos.
Otra opción es realizar lo que se llama Interning : rastreamos los objetos de registro o tuple globalmente y si intentamos crear uno nuevo que sea idéntico a un objeto de registro existente, ahora hacemos referencia a este registro existente en lugar de crear uno nuevo. Esto es esencialmente lo que hace el polyfill. Ahora estamos equiparando el valor y la identidad. Este enfoque crea problemas una vez que ampliamos ese comportamiento en múltiples contextos de JavaScript y no daría una inmutabilidad profunda por naturaleza y es particularmente lento lo que haría que el uso de Registro y Tuple sea una elección de rendimiento negativa.
Record & Tuple está construido para interoperar bien con objetos y matrices: puede leerlos exactamente de la misma manera que lo haría con objetos y matrices. El cambio principal radica en la profunda inmutabilidad y la comparación por valor en lugar de la identidad.
Los desarrolladores solían manipular objetos de manera inmutable (como transformar piezas de estado Redux) podrán continuar haciendo las mismas manipulaciones que solían hacer en objetos y matrices, esta vez, con más garantías.
Vamos a hacer una investigación empírica a través de entrevistas y encuestas para averiguar si esto está funcionando como creemos que lo hace.
.get()
/ .set()
como Immutable.js?Si queremos mantener el acceso a Registro y Tuple de manera similar a los objetos y matrices como se describe en la sección anterior, no podemos confiar en los métodos para realizar ese acceso. Hacerlo requeriría que ramificemos el código al intentar crear una función "genérica" capaz de tomar objetos/matrices/registros/tuplas.
Aquí hay una función de ejemplo que tiene soporte para registros inmutables.js y objetos ordinarios:
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
Esto es propenso a errores, ya que ambas ramas podrían salir de sincronización fácilmente con el tiempo ...
Así es como escribiríamos esa función que toma registros de esta propuesta y objetos ordinarios:
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
Esta función admite objetos y registros en una sola ruta de código, además de no obligar al consumidor a elegir qué estructuras de datos usar.
¿Por qué necesitamos apoyar a ambos al mismo tiempo de todos modos? Esto es principalmente para evitar una división del ecosistema. Digamos que estamos usando Immutable.js para hacer nuestra gestión estatal, pero necesitamos alimentar a nuestro estado a algunas bibliotecas externas que no lo admiten:
state . jobResult = Immutable . fromJS (
ExternalLib . processJob (
state . jobDescription . toJS ( )
)
) ;
Tanto toJS()
como fromJS()
pueden terminar siendo operaciones muy costosas dependiendo del tamaño de las subestructuras. Una división del ecosistema significa conversiones que, a su vez, significa posibles problemas de rendimiento.
La sintaxis propuesta mejora significativamente la ergonomía del uso Record
y Tuple
en el código. Por ejemplo:
// 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 sintaxis propuesta está destinada a ser más simple y más fácil de entender, porque es intencionalmente similar a la sintaxis para los literales de objetos y matrices. Esto aprovecha la familiaridad existente del usuario con objetos y matrices. Además, el segundo ejemplo introduce literales de objetos temporales adicionales, lo que se suma a la complejidad de la expresión.
El uso de una palabra clave como prefijo para la sintaxis literal de objeto/matriz estándar presenta problemas en torno a la compatibilidad hacia atrás. Además, la reutilización de palabras clave existentes puede introducir la ambigüedad.
ECMAScript define un conjunto de palabras clave reservadas que pueden usarse para futuras extensiones al idioma. Definir una nueva palabra clave que aún no está reservada es teóricamente posible, pero requiere un esfuerzo significativo para validar que la nueva palabra clave probablemente no romperá la compatibilidad con respecto.
El uso de una palabra clave reservada facilita este proceso, pero no es una solución perfecta porque no hay palabras clave reservadas que coincidan con la "intención" de la función, aparte de const
. La palabra clave const
también es complicada, porque describe un concepto similar (inmutabilidad de referencia variable), mientras que esta propuesta tiene la intención de agregar nuevas estructuras de datos inmutables. Si bien la inmutabilidad es el hilo común entre estas dos características, ha habido comentarios significativos de la comunidad que indica que el uso de const
en ambos contextos no es deseable.
En lugar de usar una palabra clave, {| |}
y [||]
se han sugerido como posibles alternativas. Actualmente, el grupo de campeones se inclina hacia #[]
/ #{}
, pero la discusión está en curso en el #10.
La definición de registro y tupla como primitivas compuestas obliga a todo en el registro y la tupla a no ser objetos. Esto viene con algunos inconvenientes (hacer referencia a los objetos se vuelve más difícil pero aún es posible) pero también más garantías para evitar errores de programación comunes.
const object = {
a : {
foo : "bar" ,
} ,
} ;
Object . freeze ( object ) ;
func ( object ) ;
// func is able to mutate object’s keys even if object is frozen
En el ejemplo anterior, intentamos crear una garantía de inmutabilidad con Object.freeze
. Desafortunadamente, dado que no congelamos el objeto profundamente, nada nos dice ese object.a
. Con el registro y la tupla, esa restricción es por naturaleza y no hay duda de que la estructura no está tocada:
const record = # {
a : # {
foo : "bar" ,
} ,
} ;
func ( record ) ;
// runtime guarantees that record is entirely unchanged
assert ( record . a . foo === "bar" ) ;
Finalmente, la inmutabilidad profunda suprime la necesidad de un patrón común que consiste en objetos de clonación profunda para mantener garantías:
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 general, el operador spread funciona bien para esto:
// 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]
Y si estás cambiando algo en una tupla, el método de Tuple.prototype.with
funciona:
// Change a Tuple index
let tup = # [ 1 , 2 , 3 ] ;
tup . with ( 1 , 500 ) // #[1, 500, 3]
Algunas manipulaciones de "caminos profundos" pueden ser un poco incómodos. Para eso, las propiedades de la ruta profunda para la propuesta de registros agrega una sintaxis de taquigrafía adicional para grabar literales.
Estamos desarrollando la propuesta de propiedades de la ruta profunda como una propuesta de seguimiento separada porque no la vemos como un núcleo para usar registros, que funcionan bien de forma independiente. Es el tipo de adición sintáctica que funcionaría bien para prototipos con el tiempo en los transpiladores, y donde tenemos muchos puntos de decisión que no tienen que ver con los registros y las tuplas (por ejemplo, cómo funciona con los objetos).
Hemos hablado con los campeones de Readonly Collections, y ambos grupos están de acuerdo en que estos son complementos:
Ninguno de los dos es un subconjunto del otro en términos de funcionalidad. En el mejor de los casos, son paralelos, al igual que cada propuesta es paralela a otros tipos de colección en el idioma.
Por lo tanto, los dos grupos campeones han resuelto asegurarse de que las propuestas estén en paralelo con respecto a la otra . Por ejemplo, esta propuesta agrega un nuevo método Tuple.prototype.withReversed
. La idea sería verificar, durante el proceso de diseño, si esta firma también tendría sentido para las matrices de solo lectura (si existen): extrajimos estos nuevos métodos para la matriz de cambio por propuesta de copia, para que podamos discutir una API que construye un modelo mental consistente y compartido.
En los borradores de propuestas actuales, no hay tipos de superposición para el mismo tipo de datos, pero ambas propuestas podrían crecer en estas direcciones en el futuro, y estamos tratando de pensar estas cosas con anticipación. Quién sabe, algún día TC39 podría decidir agregar mapa de discos primitivo y tipos de récords, ¡como las versiones profundamente inmutables de Set and Map! Y estos estarían paralelos a los tipos de colecciones de lectura.
TC39 ha discutido durante mucho tiempo los "tipos de valor", que sería algún tipo de declaración de clase para un tipo primitivo, durante varios años, de vez en cuando. Una versión anterior de esta propuesta incluso hizo un intento. Esta propuesta intenta comenzar simple y mínima, proporcionando solo las estructuras centrales. La esperanza es que pueda proporcionar el modelo de datos para una propuesta futura para clases.
Esta propuesta está relacionado libremente con un conjunto más amplio de propuestas, que incluyen sobrecarga de operadores y literales numéricos extendidos: todos conspiran para proporcionar una forma para que los tipos definidos por el usuario hagan lo mismo que Bigint. Sin embargo, la idea es agregar estas características si determinamos que están motivados independientemente.
Si tuviéramos tipos de primitivos/valor definidos por el usuario, entonces podría tener sentido usarlos en características incorporadas, como CSS escribió OM o la propuesta temporal. Sin embargo, esto está lejos en el futuro, si alguna vez sucede; Por ahora, funciona bien para usar objetos para este tipo de características.
Aunque ambos tipos de registros se relacionan con los objetos, y ambos tipos de tuplas se relacionan con las matrices, ahí es dónde termina la similitud.
Los registros en TypeScript son un tipo de utilidad genérica para representar un objeto que toma un tipo de clave que coincide con un tipo de valor. Todavía representan objetos.
Del mismo modo, las tuplas en TypeScript son una notación para expresar tipos en una matriz de un tamaño limitado (comenzando con TypeScript 4.0 tienen una forma variádica). Las tuplas en TypeScript son una forma de expresar matrices con tipos heterogéneos. Las tuplas de Ecmascript pueden corresponder a las matrices TS o las tuplas TS fácilmente, ya que pueden contener un número indefinido de valores del mismo tipo o contener un número limitado de valores con diferentes tipos.
Los registros de TS o las tuplas son características ortogonales para los registros y tuplas de Ecmascript y ambos podrían expresarse al mismo tiempo:
const record : Readonly < Record < string , number > > = # {
foo : 1 ,
bar : 2 ,
} ;
const tuple : readonly [ number , string ] = # [ 1 , "foo" ] ;
Esta propuesta no hace garantías de rendimiento y no requiere optimizaciones específicas en las implementaciones. Según los comentarios de los implementadores, se espera que implementen operaciones comunes a través de algoritmos de "tiempo lineal". Sin embargo, esta propuesta no evita algunas optimizaciones clásicas para estructuras de datos puramente funcionales, incluidas, entre otros,:
Estas optimizaciones son análogas a la forma en que los motores modernos JavaScript manejan la concatenación de cadenas, con varios tipos internos diferentes de cadenas. La validez de estas optimizaciones se basa en la falta de observabilidad de la identidad de los registros y tuplas. No se espera que todos los motores actúen de manera idéntica con respecto a estas optimizaciones, sino que tomarán decisiones sobre qué heurísticas particulares usar. Antes de la Etapa 4 de esta propuesta, planeamos publicar una guía para las mejores prácticas para el uso optimizable de registros y tuplas optimizables entre motores, basada en la experiencia de implementación que tendremos en ese momento.
Una nueva estructura de datos de tipo primitivo compuesto, profundamente inmutable, propuesta, propuesta en este documento, que es análogo al objeto. #{ a: 1, b: 2 }
Una nueva estructura de datos de tipo primitivo compuesto, profundamente inmutable, propuesta, propuesta en este documento, que es análoga a la matriz. #[1, 2, 3, 4]
Valores que actúan como otras primitivas de JavaScript, pero están compuestos por otros valores constituyentes. Este documento propone los dos primeros tipos primitivos compuestos: Record
y Tuple
.
String
, Number
, Boolean
, undefined
, null
, Symbol
y BigInt
Cosas que son compuestas o tipos primitivos simples. Todas las primitivas en JavaScript comparten ciertas propiedades:
Una estructura de datos que no acepta operaciones que lo cambien internamente, sino que tiene operaciones que devuelven un nuevo valor que es el resultado de aplicar esa operación a él.
En esta propuesta, Record
y Tuple
son estructuras de datos profundamente inmutables.
El operador ===
se define con el algoritmo de comparación de igualdad estricto. La igualdad estricta se refiere a esta noción particular de igualdad.
El intercambio estructural es una técnica utilizada para limitar la huella de memoria de las estructuras de datos inmutables. En pocas palabras, al aplicar una operación para obtener una nueva versión de una estructura inmutable, el intercambio estructural intentará mantener intacta la mayor parte de la estructura interna y utilizada por las versiones antiguas y derivadas de esa estructura. Esto limita en gran medida la cantidad para copiar para derivar la nueva estructura.