Las matrices de JavaScript son una forma increíblemente flexible de modelar colecciones utilizando técnicas de programación funcional. Este artículo le presenta el uso de herramientas como forEach()
, map()
y reduce()
para matrices de estilo funcional.
Matrices de JavaScript tradicionales
Las matrices de JavaScript pueden contener tipos heterogéneos, cambiar de tamaño sobre la marcha e insertar o eliminar elementos fácilmente. Los métodos tradicionales como cortar, empalmar y empujar/pop hacen esto operando en la matriz misma, modificando la colección de una manera «destructiva»:
// Create an array with heterogeneous types:
let myArray = [10, "hello", true, { name: "Alice" }];
// Add to an array and change size on the fly:
myArray.push(42);
// Extract elements without modifying the original array:
let extracted = myArray.slice(1, 3);
Programación funcional con arrays.
Aunque las matrices de JavaScript son muy capaces desde el primer momento, el paradigma funcional mejora la claridad y la facilidad de mantenimiento del código de matriz. En general, la programación funcional busca utilizar funciones como operadores que pueden pasarse a matrices. Esto permite operar sobre la matriz como el cabezal de una cinta, en lugar de los tradicionales bucles imperativos que describen en detalle lo que va a ocurrir.
Veamos algunos ejemplos de cómo trabajar con matrices en el paradigma funcional.
para cada()
Array.forEach()
es nuestro primer ejemplo. Esto le permite pasar una función que realiza operaciones arbitrarias en los elementos de forma iterativa. Es una alternativa común a la tradicional. for
bucle:
myArray.forEach((element) => {
console.log("my element is: " + element);
})
En este ejemplo, simplemente enviamos cada elemento a la consola. El equivalente en un for
bucle sería:
for (let i = 0; i < myArray.length; i++) {
console.log("my element is: " + myArray[i]);
}
Notarás que hay menos piezas móviles en la versión funcional. En particular, eliminamos el iterador (i
), que es una variable extraña utilizada para expresar la lógica de la mecánica, en lugar de una parte de la intención real. Tenga en cuenta que no estoy sugiriendo for
los bucles no tienen lugar; A veces son la herramienta adecuada, pero a menudo, forEach()
es un enfoque más limpio.
La programación funcional como filosofía promueve la «inmutabilidad». Eso significa simplemente que le gusta evitar modificar variables. En cambio, la programación funcional prefiere tomar una variable existente y pasarla a través de una “canalización” (una función o funciones) que la transforma en una nueva variable, dejando la original como está.
forEach
A menudo se usa de esta manera, pero también se usa “destructivamente”, como se muestra aquí:
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function(number, index) {
if (number % 2 === 0) { // Check for even numbers
numbers.splice(index, 1); // Remove even numbers from the array
}
});
Es posible que este ejemplo no se considere la forma más pura de programación funcional, pero utiliza características funcionales clave como «funciones de primer orden». Cuando nos referimos a una función de primer orden, queremos decir que estamos usando una función como cualquier otra referencia, en este caso, pasándola como argumento. Lo largo y lo corto de esta historia es que las funciones pueden actuar como paquetes portátiles de funcionalidades que se transmiten para realizar trabajos de manera predecible.
Tenga en cuenta también que todavía hay muchos casos en los que una antigua for
El bucle es el mejor enfoque. Por ejemplo, cuando se itera con un número distinto de 1, se itera hacia atrás y cuando se manejan escenarios complejos que requieren múltiples iteradores.
matriz.map()
Las funciones que no son destructivas y evitan cualquier otro «efecto secundario» se denominan «funciones puras». Nosotros podemos usar forEach
de esta manera, pero el Array.map()
La función está diseñada específicamente para este propósito. No opera en la matriz en sí, sino que ejecuta el operador de función y devuelve el resultado como una nueva matriz:
const bands = [
{ name: "Led Zeppelin", year: 1968 },
{ name: "Pink Floyd", year: 1965 },
{ name: "Queen", year: 1970 },
{ name: "The Clash", year: 1976 },
{ name: "The Ramones", year: 1974 },
{ name: "R.E.M.", year: 1980 },
];
const bandNames = bands.map(band => {
return band.name;
});
// bandNames is an array that has just the string band names
Array.map()
Es un mecanismo muy poderoso para transformar matrices. Le brinda la posibilidad de hacer casi cualquier cosa con una matriz de forma limpia. En particular, evita la complejidad al cambiar la matriz original, donde otro código en otros lugares podría depender de ella de maneras desconocidas o inesperadas.
Por otro lado, es importante tener en cuenta que Array.map()
siempre hace una copia, lo que tiene implicaciones en el rendimiento. No querrás utilizarlo en matrices muy grandes. A veces, las consideraciones de memoria dictan que se utilice otro enfoque.
Cómo funciona esto es que todo lo que devuelva la función proporcionada se mantendrá en la nueva matriz. Entonces, podríamos usar la versión que regresa automáticamente de una función:
const bandNames = bands.map(band => band.name)
Este enfoque puede ser mucho más limpio para funciones cortas.
matriz.filtro()
Array.map()
genera una matriz con la misma longitud que la fuente. Si la función no devuelve algo, la matriz de salida se etiquetará undefined
en esa posición. Para crear una matriz con una longitud diferente, puede usar Array.filter()
. En ese caso, cuando el argumento funcional no devuelve nada, ese elemento se eliminará de la matriz de destino:
const bands = [
{ name: "Led Zeppelin", year: 1968 },
{ name: "Pink Floyd", year: 1965 },
{ name: "Queen", year: 1970 },
{ name: "The Clash", year: 1976 },
{ name: "The Ramones", year: 1974 },
{ name: "R.E.M.", year: 1980 },
];
const seventiesBands = bands.filter(band => {
if (band.year >= 1970 && band.year < 1980) {
return band;
}
});
// seventiesBands is an array holding only those bands satisfying the condition (band.year >= 1970 && band.year < 1980)
En este ejemplo, tomamos una serie de objetos que contienen bandas de rock y el año en que se formaron y luego usamos bands.filter()
para proporcionar una función que nos proporcione una nueva matriz que contenga sólo las bandas de la década de 1970.
matriz.reducir()
A veces, es necesario tomar una matriz completa y convertirla en un valor único. Para eso puedes usar Array.reduce
:
// same band array as source
const earliestBand = bands.reduce((earliestSoFar, band) => {
return band.year < earliestSoFar.year ? band : earliestSoFar;
}, { year: Infinity }); // Start with a band in the infinitely distant future
console.log(earliestBand.name); // outputs “Pink Floyd”
La función pasada a reduce()
tiene dos argumentos: el “acumulador” y el elemento actual. El acumulador es lo que finalmente se devolverá y mantiene su estado en cada iteración, lo que le permite «recopilar» todo en una única salida.
El reduce
La función es una herramienta muy útil cuando la necesitas. Como otro ejemplo rápido, supongamos que desea una cadena que contenga todos los nombres de las bandas en una cadena. Podrías hacer esto:
const allBandNames = bands.reduce((accumulator, band) => {
return accumulator + band.name + ", ";
}, ""); // Initial value is an empty string
Componer funciones
Las funciones integradas que has visto hasta ahora son fundamentales para la programación funcional (y su hermana mayor, programación reactiva). Ahora, consideremos la idea de vincular funciones para lograr alguna funcionalidad deseada.
Dos de las funciones de enlace más básicas e importantes son compose()
y chain()
. Muchas bibliotecas de utilidades y programación funcional los incluyen, pero también son fáciles de implementar. El siguiente ejemplo le ofrece una visión clara de cómo funcionan:
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x);
const chain = (...fns) => (xs) => xs.reduce((acc, x) => acc.concat(fns.reduceRight((v, f) => f(v), x)), []);
compose()
combina muchas funciones, de modo que la salida de cada función se introduce en la siguiente, de derecha a izquierda (según su orden tal como se pasa a la función). chain()
Hace lo mismo, pero de izquierda a derecha.
Estas funciones también le dan un vistazo a reduceRight()
la imagen especular de reduce()
, que ya has visto. El reduceRight()
La función le permite acumular retrocediendo a través de los argumentos funcionales.
El compose()
y chain()
Las funciones no son específicas de las matrices, pero se pueden usar con ellas. A continuación se muestra un ejemplo sencillo del uso compose()
con una matriz:
const numbers = [1, 4, 2, 8, 5, 7];
// Define reusable higher-order functions:
const findEvenNumbers = arr => arr.filter(n => n % 2 === 0);
const doubleNumbers = arr => arr.map(n => n * 2);
const sortNumbers = arr => arr.sort((a, b) => a - b);
// Compose functions to create complex transformations:
const processNumbers = compose(sortNumbers, doubleNumbers, findEvenNumbers);
const processedNumbers = processNumbers(numbers);
console.log(processedNumbers); // Output: [4, 8, 16]
Conclusión
Organizar funciones es fundamental para la programación tanto funcional como reactiva. Le permite reutilizar y combinar funciones en nuevas funciones. En esencia, puede definir funciones compuestas que se componen de las capacidades de otras más enfocadas. Esto es similar, conceptualmente, a cómo un programador orientado a objetos piensa acerca de componer aplicaciones a partir de objetos.
Debido a que las funciones expresan su trabajo de una manera tan minimalista (con la entrada y la salida como toda su superficie API), proporcionan un enfoque excepcionalmente limpio. Por supuesto, a medida que te vuelves más sofisticado, pierdes parte de esta claridad. Incluso el compose()
y chain()
Las funciones dejan atrás algo de la elegancia de las funciones simples.
En general, manejar funciones de matriz como hemos visto aquí, usando funciones integradas de JavaScript como map()
y filter()
es una excelente aplicación del poder de la programación funcional.
Copyright © 2024 IDG Communications, Inc.