Pensamiento de estilo Ramda: inmutabilidad y objetos

1. Primeros pasos
2. Combina las funciones
3. Uso parcial (curry)
4. Programación declarativa
5. Notación por excelencia
6. Inmutabilidad y objetos.
7. Inmutabilidad y matrices
8. lentes
9. Conclusión


Esta publicación es la sexta parte de una serie de artículos sobre programación funcional llamada Ramda Style Thinking.


En la quinta parte, hablamos sobre escribir funciones en el estilo de notación sin sentido, donde el argumento principal con los datos para nuestra función no se especifica explícitamente.


En ese momento, no podíamos reescribir todas nuestras funciones en un estilo sin bits, porque no teníamos algunas herramientas necesarias para esto. Es hora de estudiarlos.


Lectura de propiedades de objeto


Veamos nuevamente el ejemplo de la definición de personas que tienen derecho a votar, que examinamos en la quinta parte :


const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => person.age >= 18 const isCitizen = either(wasBornInCountry, wasNaturalized) const isEligibleToVote = both(isOver18, isCitizen) 

Como puede ver, creamos que isCitizen y isEligibleToVote , pero no podemos hacer esto con las tres primeras funciones.


Como aprendimos en la cuarta parte , podemos hacer que nuestras funciones sean más declarativas mediante el uso de equals y gte . Comencemos con esto:


 const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY) const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => gte(person.age, 18) 

Para que estas funciones no tengan sentido, necesitamos una forma de construir la función para que apliquemos la variable person al final de la expresión. El problema es que necesitamos acceder a las propiedades de la person , ahora sabemos la única forma de hacerlo, y es imprescindible.


apoyo


Afortunadamente, Ramda una vez más viene en nuestra ayuda. Proporciona una función prop para acceder a las propiedades de los objetos.


Usando prop , podemos reescribir person.birthCountry para prop('birthCountry', person) . Hagámoslo:


 const wasBornInCountry = person => equals(prop('birthCountry', person), OUR_COUNTRY) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18) 

Wow, ahora se ve mucho peor. Pero continuemos nuestra refactorización. Cambiemos el orden de los argumentos que pasamos a equals para que el prop sea ​​el último. equals funciona exactamente de la misma manera a la inversa, por lo que no romperemos nada:


 const wasBornInCountry = person => equals(OUR_COUNTRY, prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18) 

A continuación, usemos curry, la propiedad natural de equals y gte , para crear nuevas funciones a las que se aplicará el resultado de la llamada prop :


 const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(__, 18)(prop('age', person)) 

Todavía parece la peor opción, pero sigamos. Aprovechemos el curry nuevamente para todas las llamadas de prop :


 const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry')(person)) const wasNaturalized = person => Boolean(prop('naturalizationDate')(person)) const isOver18 = person => gte(__, 18)(prop('age')(person)) 

De nuevo, de alguna manera no muy. Pero ahora vemos un patrón familiar. Todas nuestras funciones tienen la misma imagen f(g(person)) , y como sabemos por la segunda parte , esto es equivalente a compose(f, g)(person) .


Apliquemos esta ventaja a nuestro código:


 const wasBornInCountry = person => compose(equals(OUR_COUNTRY), prop('birthCountry'))(person) const wasNaturalized = person => compose(Boolean, prop('naturalizationDate'))(person) const isOver18 = person => compose(gte(__, 18), prop('age'))(person) 

Ahora tenemos algo. Todas nuestras funciones se ven como person => f(person) . Y ya sabemos por la quinta parte que podemos hacer que estas funciones no tengan sentido.


 const wasBornInCountry = compose(equals(OUR_COUNTRY), prop('birthCountry')) const wasNaturalized = compose(Boolean, prop('naturalizationDate')) const isOver18 = compose(gte(__, 18), prop('age')) 

Cuando comenzamos, no era obvio que nuestros métodos hicieran dos cosas. Se volvieron hacia la propiedad del objeto y prepararon algunas operaciones con su valor. Esta refactorización en un estilo sin sentido lo hizo muy explícito.


Echemos un vistazo a algunas de las otras herramientas que Ramda proporciona para trabajar con objetos.


recoger


Cuando prop lee una propiedad de un objeto y devuelve su valor, pick lee muchas propiedades del objeto y devuelve un nuevo objeto solo con ellas.


Por ejemplo, si solo necesitamos los nombres y años de personas, podemos usar pick(['name','age'], person) .


tiene


Si solo queremos saber que nuestro objeto tiene una propiedad, sin leer su valor, podemos usar la función has para verificar sus propiedades, así como hasIn para verificar la cadena del prototipo: has('name', person) .


camino


Donde prop una propiedad de objeto, la ruta se adentra en objetos anidados. Por ejemplo, queremos extraer el código postal de una estructura más profunda: path(['address','zipCode'], person) .


Tenga en cuenta que el path más indulgente que el prop . path volverá undefined si algo en el camino (incluido el argumento original) es null o undefined , mientras que prop causará un error en tales situaciones.


propOr / pathOr


propOr y pathOr son similares a prop y path combinados con defaultTo . Le proporcionan la capacidad de especificar un valor predeterminado para una propiedad o ruta que no se puede encontrar en el objeto en estudio.


Por ejemplo, podemos proporcionar un marcador de posición cuando no sabemos el nombre de la persona: propOr('<Unnamed>, 'name', person) . Tenga en cuenta que a diferencia de prop , propOr no causará un error si la person es null o undefined ; en su lugar, devolverá el valor predeterminado.


claves / valores


keys devuelve una matriz que contiene todos los nombres de todas las propiedades conocidas del objeto. Los valores devolverán los valores de estas propiedades. Estas funciones pueden ser útiles cuando se combinan con las funciones de iteración para colecciones, que aprendimos en la primera parte .


Agregar, actualizar y eliminar propiedades


Ahora tenemos muchas herramientas para leer desde objetos en un estilo declarativo, pero ¿qué hay de hacer cambios?


Dado que la inmutabilidad es importante para nosotros, no queremos modificar objetos directamente. En cambio, queremos devolver nuevos objetos que han cambiado de la manera que queremos.


Una vez más, Ramda nos brinda muchos beneficios.


assoc / assocPath


Cuando programamos en un estilo imperativo, podemos establecer o cambiar el nombre de la persona a través del operador de asignación: person.name = 'New name' .


En nuestro mundo funcional e inmutable, podemos usar assoc en su lugar: const updatedPerson = assoc('name', 'newName', person) .


assoc devuelve un nuevo objeto con un valor de propiedad agregado o actualizado, dejando el objeto original sin cambios.


También tenemos a nuestra disposición assocPath para actualizar la propiedad adjunta: const updatedPerson = assocPath(['address', 'zipCode'], '97504', person) .


dissoc / dissocPath / omit


¿Qué pasa con la eliminación de propiedades? Imperativamente, podemos querer decir delete person.age . En Ramda, usaremos dissoc : `const updatedPerson = dissoc ('age', person)


dissocPath es casi lo mismo, pero funciona en estructuras de objetos más profundas: dissocPath(['address', 'zipCode'], person) .


Y también tenemos omitir , que puede eliminar varias propiedades a la vez: const updatedPerson = omit(['age', 'birthCountry'], person) .


Tenga en cuenta que pick y omit poco similares y se complementan muy bien. Son muy convenientes para incluir en la lista blanca (guardar solo un cierto conjunto de propiedades usando pick ) y listas negras (deshacerse de ciertas propiedades mediante el uso de omit ).


Transformación de objetos


Ahora sabemos lo suficiente como para trabajar con objetos en un estilo declarativo e inmutable. Escribamos una función de celebrateBirthday que actualice la edad de la persona en su cumpleaños.


 const nextAge = compose(inc, prop('age')) const celebrateBirthday = person => assoc('age', nextAge(person), person) 

Este es un patrón muy común. En lugar de actualizar la propiedad con un nuevo valor, realmente queremos cambiar el valor aplicando la función al valor anterior, como lo hicimos aquí.


No conozco una buena manera de escribir esto con menos duplicación y en un estilo menos riguroso, con esas herramientas que aprendimos anteriormente.


Ramda una vez más nos salva con la función evolucionar . evolve acepta un objeto y le permite especificar funciones de transformación para aquellas propiedades que queremos cambiar. Refractemos celebrateBirthday el celebrateBirthday sobre el uso de evolve :


 const celebrateBirthday = evolve({ age: inc }) 

Este código dice que convertiremos el objeto especificado (que no se muestra debido al estilo brutal) creando un nuevo objeto con las mismas propiedades y valores, pero la propiedad age se obtendrá aplicando inc al valor original de la propiedad age .


evolve puede transformar muchas propiedades a la vez, e incluso en múltiples niveles de anidamiento. La transformación del objeto puede tener la misma imagen que tendrá el objeto mutable, y evolve pasará recursivamente entre las estructuras, utilizando las funciones de transformación en la forma especificada.


Tenga en cuenta que evolve no agrega nuevas propiedades; si especifica una transformación para una propiedad que no ocurre en el objeto que se procesa, evolve simplemente la ignorará.


Descubrí que evolve convirtiendo rápidamente en un caballo de batalla en mis aplicaciones.


Fusionar objetos


A veces necesitas combinar dos objetos juntos. Un caso típico es cuando tiene una función que toma opciones con nombre y desea combinarlas con las opciones predeterminadas. Ramda proporciona una función de fusión para este propósito.


 function f(a, b, options = {}) { const defaultOptions = { value: 42, local: true } const finalOptions = merge(defaultOptions, options) } 

merge devuelve un nuevo objeto que contiene todas las propiedades y valores de ambos objetos. Si ambos objetos tienen la misma propiedad, se obtendrá el valor del segundo argumento.


La presencia de esta regla con un segundo argumento ganador hace que sea significativo usar la merge como una herramienta autónoma, pero menos significativo en situaciones de transporte. En este caso, a menudo necesita preparar una serie de transformaciones para un objeto, y una de esas transformaciones es la unión de algunos valores de propiedad nuevos. En este caso, querrá que gane el primer argumento en lugar del segundo.


Intentar usar merge(newValues) en la tubería no dará lo que nos gustaría obtener.


Para esta situación, generalmente creo mi propia utilidad llamada reverseMerge . Se puede escribir como const reverseMerge = flip(merge) . La llamada invertida intercambia los dos primeros argumentos de la función que le corresponde.


merge realiza una fusión de superficie. Si los objetos, cuando se combinan, tienen una propiedad cuyo valor es un subobjeto, entonces estos subobjetos no se fusionan. Ramda actualmente no tiene una habilidad de fusión profunda (El artículo original que estoy traduciendo ya tiene información desactualizada sobre este tema. Hoy Ramda tiene funciones como mergeDeepLeft , mergeDeepRight para fusionar objetos recursivamente profundos y otros métodos para fusionar ).


Tenga en cuenta que la merge solo acepta dos argumentos. Si desea combinar muchos objetos en uno, puede usar mergeAll , que toma una matriz de objetos para combinar.


Conclusión


Hoy tenemos un maravilloso conjunto de herramientas para trabajar con objetos en un estilo declarativo e inmutable. Ahora podemos leer, agregar, actualizar, eliminar y transformar propiedades en objetos sin cambiar los objetos originales. Y podemos hacer todas estas cosas en un estilo que facilite combinar funciones entre sí.


Siguiente


Ahora podemos trabajar con objetos en un estilo inmutable, pero ¿qué pasa con las matrices? "Inmunidad y matrices" nos dirá qué hacer con ellos.

Source: https://habr.com/ru/post/es414337/


All Articles