El autor del material, cuya traducción publicamos hoy, dice que él, después de un largo tiempo dedicado a la programación orientada a objetos, pensó en la complejidad de los sistemas. Según
John Ousterhout , la complejidad es todo lo que dificulta la comprensión o la modificación del software. El autor de este artículo, después de completar una investigación, descubrió los conceptos de programación funcional como inmunidad y funciones puras. El uso de tales conceptos le permite crear funciones que no tienen efectos secundarios. El uso de estas funciones simplifica el soporte del sistema y le da al programador algunos otros
beneficios .

Aquí hablamos sobre programación funcional y algunos de sus principios importantes. Todo esto se ilustrará con muchos ejemplos de código JavaScript.
¿Qué es la programación funcional?
Puedes leer sobre qué es la programación funcional en
Wikipedia . Es decir, estamos hablando del hecho de que la programación funcional es un paradigma de programación en el que el proceso de cálculo se trata como el cálculo de los valores de las funciones en la comprensión matemática de este último. La programación funcional implica hacer el cálculo de los resultados de las funciones a partir de los datos de origen y los resultados de otras funciones, y no implica el almacenamiento explícito del estado del programa. En consecuencia, no implica la variabilidad de este estado.
Ahora, a modo de ejemplos, analizaremos algunas ideas de programación funcional.
Funciones puras
Las funciones puras son el primer concepto fundamental que debe estudiarse para comprender la esencia de la programación funcional.
¿Qué es una "función pura"? ¿Qué hace que una función sea "limpia"? Una función pura debe cumplir los siguientes requisitos:
- Siempre devuelve, al pasarle los mismos argumentos, el mismo resultado (tales funciones también se denominan deterministas).
- Tal función no tiene efectos secundarios.
Considere la primera propiedad de las funciones puras, es decir, el hecho de que, al pasarles los mismos argumentos, siempre devuelven el mismo resultado.
▍ Argumentos de función y valores de retorno
Imagine que necesitamos crear una función que calcule el área de un círculo. Una función que no es pura tomaría, como parámetro, el radio del círculo (
radius
), después de lo cual devolvería el valor del cálculo de la expresión
radius * radius * PI
:
const PI = 3.14; function calculateArea(radius) { return radius * radius * PI; } calculateArea(10);
¿Por qué esta función no se puede llamar pura? El hecho es que usa una constante global, que no se le pasa como argumento.
Ahora imagine que algunos matemáticos llegaron a la conclusión de que el valor de la constante
PI
debería ser el número
42
, por lo que se cambió el valor de esta constante.
Ahora, una función que no es pura, cuando se pasa el mismo valor de entrada, el número
10
, devolverá el valor
10 * 10 * 42 = 4200
. Resulta que usando aquí lo mismo que en el ejemplo anterior, el valor del parámetro de
radius
, la función devuelve un resultado diferente. Vamos a arreglar esto:
const PI = 3.14; function calculateArea(radius, pi) { return radius * radius * pi; } calculateArea(10, PI);
Ahora, al llamar a esta función, siempre le pasaremos el argumento
pi
. Como resultado, la función funcionará solo con lo que se le pasa cuando se llama, sin recurrir a entidades globales. Si analizamos el comportamiento de esta función, podemos llegar a las siguientes conclusiones:
- Si las funciones pasan el argumento de
radius
igual a 10
y el argumento pi
igual a 3.14
, siempre devolverá el mismo resultado: 314
. - Cuando se llama con un argumento de
radius
de 10
y pi
de 42
, siempre devolverá 4200
.
Lectura de archivos
Si nuestra función lee archivos, entonces no estará limpia. El hecho es que el contenido de los archivos puede cambiar.
function charactersCounter(text) { return `Character count: ${text.length}`; } function analyzeFile(filename) { let fileContent = open(filename); return charactersCounter(fileContent); }
Generación de números aleatorios
Cualquier función que se base en un generador de números aleatorios no puede ser pura.
function yearEndEvaluation() { if (Math.random() > 0.5) { return "You get a raise!"; } else { return "Better luck next year!"; } }
Ahora hablemos de los efectos secundarios.
▍ Efectos secundarios
Un ejemplo de un efecto secundario que puede ocurrir cuando se llama a una función es la modificación de variables globales o argumentos pasados a funciones por referencia.
Supongamos que necesitamos crear una función que tome un número entero e incremente ese número en 1. Esto es lo que podría parecer una implementación de una idea similar:
let counter = 1; function increaseCounter(value) { counter = value + 1; } increaseCounter(counter); console.log(counter);
Hay un
counter
variable global. Nuestra función, que no es pura, recibe este valor como argumento y lo sobrescribe, agregando uno a su valor anterior.
La variable global está cambiando, similar en programación funcional no es bienvenida.
En nuestro caso, el valor de la variable global se modifica. ¿Cómo hacer que la función raiseCounter
increaseCounter()
esté limpia en estas condiciones? De hecho, es muy simple:
let counter = 1; function increaseCounter(value) { return value + 1; } increaseCounter(counter);
Como puede ver, la función devuelve
2
, pero el valor del
counter
variable global no cambia. Aquí podemos concluir que la función devuelve el valor que se le pasó, aumentado en
1
, sin cambiar nada.
Si sigue las dos reglas anteriores para escribir funciones puras, esto facilitará la navegación en los programas creados con dichas funciones. Resulta que cada función estará aislada y no afectará las partes del programa externas a ella.
Las funciones puras son estables, consistentes y predecibles. Al recibir los mismos datos de entrada, tales funciones siempre devuelven el mismo resultado. Esto evita que el programador intente tener en cuenta la posibilidad de situaciones en las que la transferencia de funciones de los mismos parámetros conduce a resultados diferentes, ya que esto es simplemente imposible con funciones puras.
▍ Fortalezas de funciones puras
Entre los puntos fuertes de las funciones puras está el hecho de que el código escrito con ellas es más fácil de probar. En particular, no necesita crear ningún objeto auxiliar. Esto permite pruebas unitarias de funciones puras en varios contextos:
- Si el parámetro A se pasa a la función, se espera el valor de retorno de B.
- Si el parámetro C se pasa a la función, se espera el valor de retorno de D.
Como un ejemplo simple de esta idea, podemos dar una función que tome una matriz de números, y se espera que aumente en uno cada número de esta matriz, devolviendo una nueva matriz con los resultados:
let list = [1, 2, 3, 4, 5]; function incrementNumbers(list) { return list.map(number => number + 1); }
Aquí pasamos una matriz de números a la función, después de lo cual usamos el método de matriz
map()
, que nos permite modificar cada elemento de la matriz y forma una nueva matriz devuelta por la función. Llamamos a la función pasándola una matriz de
list
:
incrementNumbers(list);
De esta función se espera que, habiendo aceptado una matriz de la forma
[1, 2, 3, 4, 5]
, devolverá una nueva matriz
[2, 3, 4, 5, 6]
. Así es como funciona.
Inmunidad
La inmunidad de una determinada entidad puede describirse como el hecho de que no cambia con el tiempo, o como la imposibilidad de cambiar esta entidad.
Si intentan cambiar un objeto inmutable, esto no tendrá éxito. En su lugar, deberá crear un nuevo objeto que contenga los nuevos valores.
Por ejemplo, JavaScript a menudo usa el bucle
for
. En el curso de su trabajo, como se muestra a continuación, se utilizan variables mutables:
var values = [1, 2, 3, 4, 5]; var sumOfValues = 0; for (var i = 0; i < values.length; i++) { sumOfValues += values[i]; } sumOfValues
En cada iteración del bucle, el valor de la variable
i
y el valor de la variable global (puede considerarse el estado del programa)
sumOfValues
. ¿Cómo en tal situación mantener la inmutabilidad de las entidades? La respuesta está en usar la recursividad.
let list = [1, 2, 3, 4, 5]; let accumulator = 0; function sum(list, accumulator) { if (list.length == 0) { return accumulator; } return sum(list.slice(1), accumulator + list[0]); } sum(list, accumulator);
Hay una función
sum()
, que toma una matriz de números. Esta función se llama a sí misma hasta que la matriz esté vacía (este es el caso básico de nuestro
algoritmo recursivo ). En cada "iteración" de este tipo, agregamos el valor de uno de los elementos de la matriz al parámetro de la función de
accumulator
, sin afectar el
accumulator
variable global. En este caso, la
list
variables globales y el
accumulator
permanecen sin cambios; los mismos valores se almacenan en ellos antes y después de la llamada a la función.
Cabe señalar que para implementar dicho algoritmo, puede utilizar el método de
reduce
matriz. Hablaremos de esto a continuación.
En la programación, la tarea se generaliza cuando es necesario, basándose en una determinada plantilla de un objeto, crear su representación final. Imagine que tenemos una cadena que debe convertirse en una vista adecuada para su uso como parte de la URL que conduce a un determinado recurso.
Si resolvemos este problema usando Ruby y los principios de OOP, primero crearemos una clase, digamos, llamándola
UrlSlugify
, ¡y luego crearemos un método para esta clase
slugify!
que se usa para convertir la cadena.
class UrlSlugify attr_reader :text def initialize(text) @text = text end def slugify! text.downcase! text.strip! text.gsub!(' ', '-') end end UrlSlugify.new(' I will be a url slug ').slugify! # "i-will-be-a-url-slug"
Hemos implementado el algoritmo, y esto es maravilloso. Aquí vemos un enfoque imperativo para la programación, cuando nosotros, procesando la línea, pintamos cada paso de su transformación. Es decir, primero reducimos sus caracteres a minúsculas, luego eliminamos espacios innecesarios y finalmente cambiamos los espacios restantes en el tablero.
Sin embargo, durante esta transformación, se produce una mutación del estado del programa.
Puede hacer frente al problema de la mutación componiendo funciones o encadenando llamadas a funciones. En otras palabras, el resultado devuelto por la función se utilizará como entrada para la siguiente función y, por lo tanto, para todas las funciones de una cadena. En este caso, la cadena original no cambiará.
let string = " I will be a url slug "; function slugify(string) { return string.toLowerCase() .trim() .split(" ") .join("-"); } slugify(string);
Aquí usamos las siguientes funciones, representadas en JavaScript por cadenas estándar y métodos de matriz:
toLowerCase
: Convierte caracteres de cadena en toLowerCase
.trim
: elimina espacios en blanco desde el principio y el final de una línea.split
: divide una cadena en partes, colocando palabras separadas por espacios en una matriz.join
: forma una cadena con palabras separadas por un guión basado en una matriz con palabras.
Estas cuatro funciones le permiten crear una función para convertir una cadena que no cambie esta cadena en sí misma.
Transparencia de enlace
Cree una función
square()
que devuelva el resultado de multiplicar un número por el mismo número:
function square(n) { return n * n; }
Esta es una función pura que siempre, para el mismo valor de entrada, devolverá el mismo valor de salida.
square(2);
Por ejemplo, no importa cuántos números
2
se le pasen, esta función siempre devolverá el número
4
. Como resultado, resulta que una llamada de la forma
square(2)
puede ser reemplazada por el número
4
. Esto significa que nuestra función tiene la propiedad de la transparencia referencial.
En general, podemos decir que si una función devuelve invariablemente el mismo resultado para los mismos valores de entrada que se le pasan, tiene transparencia referencial.
▍ Funciones puras + datos inmutables = transparencia referencial
Usando la idea presentada en el título de esta sección, puede memorizar funciones. Supongamos que tenemos una función como esta:
function sum(a, b) { return a + b; }
Lo llamamos así:
sum(3, sum(5, 8));
La
sum(5, 8)
llamadas
sum(5, 8)
siempre da
13
. Por lo tanto, la llamada anterior se puede reescribir de la siguiente manera:
sum(3, 13);
Esta expresión, a su vez, siempre da
16
. Como resultado, puede ser reemplazado por una constante numérica y
memorizado .
Funciones como objetos de primera clase
La idea de percibir las funciones como objetos de la primera clase es que tales funciones pueden considerarse como valores y trabajar con ellas como datos. Se pueden distinguir las siguientes características de las funciones:
- Las referencias a funciones se pueden almacenar en constantes y variables y, a través de ellas, acceder a funciones.
- Las funciones se pueden pasar a otras funciones como parámetros.
- Las funciones pueden ser devueltas desde otras funciones.
Es decir, se trata de considerar las funciones como valores y tratarlas como datos. Con este enfoque, puede combinar varias funciones en el proceso de creación de nuevas funciones que implementen nuevas características.
Imagine que tenemos una función que agrega dos valores numéricos, luego los multiplica por
2
y devuelve lo que resultó:
function doubleSum(a, b) { return (a + b) * 2; }
Ahora escribimos una función que resta el segundo del primer valor numérico que se le pasa, multiplica lo que sucedió por
2
y devuelve el valor calculado:
function doubleSubtraction(a, b) { return (a - b) * 2; }
Estas funciones tienen una lógica similar, solo difieren en qué tipo de operaciones realizan con los números que se les pasan. Si podemos considerar las funciones como valores y pasarlas como argumentos a otras funciones, esto significa que podemos crear una función que acepte y use otra función que describa las características de los cálculos. Estas consideraciones nos permiten llegar a las siguientes construcciones:
function sum(a, b) { return a + b; } function subtraction(a, b) { return a - b; } function doubleOperator(f, a, b) { return f(a, b) * 2; } doubleOperator(sum, 3, 1);
Como puede ver, ahora la función
doubleOperator()
tiene un parámetro
f
, y la función que representa se usa para procesar los parámetros
b
. Las funciones
sum()
y
substraction()
pasadas a la función
doubleOperator()
, de hecho, le permiten controlar el comportamiento de la función
doubleOperator()
, cambiándola de acuerdo con la lógica implementada en ellas.
Funciones de orden superior
Hablando de funciones de orden superior, nos referimos a funciones que se caracterizan por al menos una de las siguientes características:
- Una función toma otra función como argumento (puede haber varias de esas funciones).
- La función devuelve otra función como resultado de su trabajo.
Es posible que ya esté familiarizado con los métodos estándar de matriz JS
filter()
,
map()
y
reduce()
. Hablemos de ellos.
▍ Filtrar matrices y el método filter ()
Supongamos que tenemos una cierta colección de elementos que queremos filtrar por algún atributo de los elementos de esta colección y formar una nueva colección. La función
filter()
espera recibir algún criterio para evaluar los elementos, en base al cual determina si se incluye o no un elemento en la colección resultante. Este criterio se define por la función que se le pasa, que devuelve
true
si la función
filter()
debe incluir un elemento en la colección final, de lo contrario, devuelve
false
.
Imagine que tenemos una matriz de enteros y queremos filtrarla obteniendo una nueva matriz que contenga solo números pares de la matriz original.
Enfoque imperativo
Al aplicar un enfoque imperativo para resolver este problema usando JavaScript, necesitamos implementar la siguiente secuencia de acciones:
- Cree una matriz vacía para nuevos elementos (llamémoslo
evenNumbers
). - Iterar sobre la matriz original de enteros (llamémosle
numbers
). - Coloque los números pares que se encuentran en la matriz de
numbers
en la matriz de numbers
evenNumbers
.
Así es como se ve la implementación de este algoritmo:
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; var evenNumbers = []; for (var i = 0; i < numbers.length; i++) { if (numbers[i] % 2 == 0) { evenNumbers.push(numbers[i]); } } console.log(evenNumbers);
Además, podemos escribir una función (llamémosla
even()
), que, si el número es par, devuelve
true
, y si es impar, devuelve
false
, y luego pasarlo al método de matriz
filter()
, que, al verificarlo, cada elemento de la matriz , formará una nueva matriz que contenga solo números pares:
function even(number) { return number % 2 == 0; } let listOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; listOfNumbers.filter(even);
Aquí, por cierto, está la solución a un problema interesante con respecto al
filtrado de matrices , que completé mientras trabajaba en tareas de programación funcional en
Hacker Rank . Por la condición del problema, era necesario filtrar una matriz de enteros, mostrando solo aquellos elementos que son menores que un valor dado de
x
.
Una solución imprescindible para este problema en JavaScript podría verse así:
var filterArray = function(x, coll) { var resultArray = []; for (var i = 0; i < coll.length; i++) { if (coll[i] < x) { resultArray.push(coll[i]); } } return resultArray; } console.log(filterArray(3, [10, 9, 8, 2, 7, 5, 1, 3, 0]));
La esencia del enfoque imperativo es que describimos la secuencia de acciones realizadas por la función. Es decir, describimos la búsqueda de la matriz, comparando el elemento actual de la matriz con
x
y colocando este elemento en la matriz
resultArray
si pasa la prueba.
Enfoque declarativo
¿Cómo cambiar a un enfoque declarativo para resolver este problema y el uso correspondiente del método
filter()
, que es una función de orden superior? Por ejemplo, podría verse así:
function smaller(number) { return number < this; } function filterArray(x, listOfNumbers) { return listOfNumbers.filter(smaller, x); } let numbers = [10, 9, 8, 2, 7, 5, 1, 3, 0]; filterArray(3, numbers);
Es posible que encuentre inusual usar
this
en la función
smaller()
en este ejemplo, pero aquí no hay nada complicado. La
this
es el segundo argumento del método
filter()
. En nuestro ejemplo, este es el número
3
representado por el parámetro
x
de
filterArray()
. Este número está indicado por
this
.
Se puede utilizar el mismo enfoque si la matriz contiene entidades que tienen una estructura bastante compleja, por ejemplo, objetos. Supongamos que tenemos una matriz que almacena objetos que contienen los nombres de personas representadas por la propiedad de
name
e información sobre la edad de estas personas representadas por la propiedad de
age
. Así es como se ve una matriz:
let people = [ { name: "TK", age: 26 }, { name: "Kaio", age: 10 }, { name: "Kazumi", age: 30 } ];
Queremos filtrar esta matriz seleccionando solo aquellos objetos que son personas cuya edad ha excedido los
21
años. Aquí se explica cómo resolver este problema:
function olderThan21(person) { return person.age > 21; } function overAge(people) { return people.filter(olderThan21); } overAge(people);
Aquí tenemos una matriz con objetos que representan personas. Verificamos los elementos de esta matriz utilizando la función más
olderThan21()
. En este caso, al verificar, nos referimos a la propiedad de
age
de cada elemento, verificando si el valor de esta propiedad excede
21
. Pasamos esta función al método
filter()
, que filtra la matriz.
▍ Procesar elementos de matriz y el método map ()
El método
map()
se usa para convertir elementos de matriz. Aplica la función pasada a cada elemento de la matriz y luego crea una nueva matriz que consta de los elementos modificados.
Continuemos los experimentos con el conjunto de
people
que ya conoce. Ahora no vamos a filtrar esta matriz en función de la propiedad de
age
objetos de
age
. Necesitamos crear sobre una base una lista de líneas de la forma
TK is 26 years old
. En este enfoque, las líneas en las que se convierten los elementos se construirán de acuerdo con la plantilla
p.name is p.age years old
, donde
p.name
y
p.age
son los valores de las propiedades correspondientes de los elementos de la matriz de
people
.
Un enfoque imperativo para resolver este problema en JavaScript se ve así:
var people = [ { name: "TK", age: 26 }, { name: "Kaio", age: 10 }, { name: "Kazumi", age: 30 } ]; var peopleSentences = []; for (var i = 0; i < people.length; i++) { var sentence = people[i].name + " is " + people[i].age + " years old"; peopleSentences.push(sentence); } console.log(peopleSentences);
Si recurre a un enfoque declarativo, obtiene lo siguiente:
function makeSentence(person) { return `${person.name} is ${person.age} years old`; } function peopleSentences(people) { return people.map(makeSentence); } peopleSentences(people);
De hecho, la idea principal aquí es que debe hacer algo con cada elemento de la matriz original y luego colocarlo en una nueva matriz.
Aquí hay otra tarea con el Hacker Rank, que se dedica a
actualizar la lista . Es decir, estamos hablando de cambiar los valores de los elementos de una matriz numérica existente a sus valores absolutos. Entonces, por ejemplo, al procesar una matriz
[1, 2, 3, -4, 5]
tomará la forma
[1, 2, 3, 4, 5]
ya que el valor absoluto de
-4
es
4
.
Aquí hay un ejemplo de una solución simple a este problema, cuando iteramos sobre una matriz y cambiamos los valores de sus elementos a sus valores absolutos.
var values = [1, 2, 3, -4, 5]; for (var i = 0; i < values.length; i++) { values[i] = Math.abs(values[i]); } console.log(values);
Aquí, para convertir los valores de los elementos de la matriz, se usa el método
Math.abs()
, los elementos modificados se escriben en el mismo lugar donde estaban antes de la conversión.
.
, , , . . , , , .
, ,
map()
. ?
,
abs()
, , .
Math.abs(-1);
, , .
, ,
Math.abs()
map()
. , ?
map()
. :
let values = [1, 2, 3, -4, 5]; function updateListMap(values) { return values.map(Math.abs); } updateListMap(values);
, , , , , .
▍ reduce()
reduce()
.
. , -.
Product 1
,
Product 2
,
Product 3
Product 4
. .
, . Por ejemplo, podría verse así:
var orders = [ { productTitle: "Product 1", amount: 10 }, { productTitle: "Product 2", amount: 30 }, { productTitle: "Product 3", amount: 20 }, { productTitle: "Product 4", amount: 60 } ]; var totalAmount = 0; for (var i = 0; i < orders.length; i++) { totalAmount += orders[i].amount; } console.log(totalAmount);
reduce()
, (
sumAmount()
), ,
reduce()
:
let shoppingCart = [ { productTitle: "Product 1", amount: 10 }, { productTitle: "Product 2", amount: 30 }, { productTitle: "Product 3", amount: 20 }, { productTitle: "Product 4", amount: 60 } ]; const sumAmount = (currentTotalAmount, order) => currentTotalAmount + order.amount; function getTotalAmount(shoppingCart) { return shoppingCart.reduce(sumAmount, 0); } getTotalAmount(shoppingCart);
shoppingCart
, ,
sumAmount()
, (
order
,
amount
), —
currentTotalAmount
.
reduce()
,
getTotalAmount()
,
sumAmount()
,
0
.
map()
reduce()
. «»? ,
map()
shoppingCart
,
amount
,
reduce()
sumAmount()
. :
const getAmount = (order) => order.amount; const sumAmount = (acc, amount) => acc + amount; function getTotalAmount(shoppingCart) { return shoppingCart .map(getAmount) .reduce(sumAmount, 0); } getTotalAmount(shoppingCart);
getAmount()
amount
.
map()
, , ,
[10, 30, 20, 60]
. ,
reduce()
, .
▍ filter(), map() reduce()
, ,
filter()
,
map()
reduce()
. , , .
-. , :
let shoppingCart = [ { productTitle: "Functional Programming", type: "books", amount: 10 }, { productTitle: "Kindle", type: "eletronics", amount: 30 }, { productTitle: "Shoes", type: "fashion", amount: 20 }, { productTitle: "Clean Code", type: "books", amount: 60 } ]
. :
, :
let shoppingCart = [ { productTitle: "Functional Programming", type: "books", amount: 10 }, { productTitle: "Kindle", type: "eletronics", amount: 30 }, { productTitle: "Shoes", type: "fashion", amount: 20 }, { productTitle: "Clean Code", type: "books", amount: 60 } ] const byBooks = (order) => order.type == "books"; const getAmount = (order) => order.amount; const sumAmount = (acc, amount) => acc + amount; function getTotalAmount(shoppingCart) { return shoppingCart .filter(byBooks) .map(getAmount) .reduce(sumAmount, 0); } getTotalAmount(shoppingCart);
Resumen
JavaScript-. , .
Estimados lectores! ?
