JavaScript es un lenguaje de múltiples paradigmas y se puede escribir siguiendo diferentes paradigmas de programación. Un paradigma de programación es esencialmente un conjunto de reglas que se siguen al escribir código.

Estos paradigmas existen porque resuelven los problemas a los que se enfrentan los programadores y tienen sus propias reglas e instrucciones para ayudarte a escribir mejor código.

Cada paradigma te ayuda a resolver un problema específico. Por eso es útil tener una descripción general de cada uno de ellos. Aquí vamos a cubrir la programación funcional.

Al final de este artículo, hay algunos recursos que puedes utilizar para ir más allá si te ha gustado esta introducción.

También hay un glosario en GitHub que te ayudará a decodificar toda esta jerga que utiliza la programación funcional.

Por último, encontrarás un lugar en el cual ensuciarte las manos con ejemplos prácticos y un repositorio de GitHub con recursos que puedes utilizar para aprender más. Así que, vamos a por ello.

Paradigmas de programación declarativos vs imperativos

Un ejemplo de estos paradigmas de los que hablé al principio, es la programación orientada a objetos. Otro es la programación funcional.

¿Entonces que es exactamente la programación funcional?

La programación funcional es un sub-paradigma del paradigma de programación declarativa, con sus propias reglas a seguir al escribir código.

¿Qué es el paradigma de función declarativa?

Si estás codificando en un lenguaje que sigue el paradigma declarativo, escribe código que especifique lo que quieres hacer, sin decir cómo.

Un ejemplo muy simple de esto es SQL o HTML:

SELECT * FROM clientes
<div></div>

En los ejemplos de código anteriores, no estás implementando SELECT o cómo imprimir un div. Simplemente le estás diciendo a la computadora qué hacer, sin el cómo.

De este paradigma, existen sub-paradigmas como la Programación funcional. Más de esto, en seguida.

¿Qué es el paradigma de programación imperativa?

Si estás codificando en un lenguaje que sigue el paradigma imperativo / procedimental, escribes código que le dice cómo hacer algo.

Por ejemplo, si haces algo como lo siguiente:

for (let i = 0; i < arr.length; i++) {
     incrementar += arr[i];
}

Le estás diciendo a la computadora exactamente lo que debe hacer. Itera a través del arreglo llamado arr, y luego incrementar cada uno de los elementos del arreglo.

Programación declarativa vs imperativa

Puedes escribir JavaScript en el paradigma declarativo o el paradigma imperativo. Esto es lo que la gente quiere decir cuando dice que es un lenguaje de paradigmas múltiples. Es solo que el código funcional sigue el paradigma declarativo.

Si te ayuda a recordar, un ejemplo de un comando declarativo sería pedirle a la computadora que te prepare una taza de té (no me importa cómo lo hagas, solo tráeme mi taza de té).

Mientras que imperativamente, tendrías que decir:

  • Ve a la cocina.
  • Si hay una tetera en la habitación y tiene suficiente agua para una taza de té, enciende la tetera.
  • Si hay una tetera en la habitación y no tiene suficiente agua para una taza de té, llena la tetera con agua suficiente para una de taza de té y enciende la tetera.
  • Y así sucesivamente.

¿Entonces que es programación funcional?

¿Qué significa esto para código funcional?

Debido a que es un sub-paradigma del paradigma declarativo, esto afecta la forma en que escribes código funcional. Generalmente conduce a menos código, porque JavaScript ya tiene muchas de las funciones integradas que normalmente necesitas. Esta es una de las razones por las que a la gente le gusta el código funcional.

También te permite abstraer mucho (no tienes que entender en profundidad cómo se hace algo), simplemente llamas a una función que lo hace por ti.

¿Y cuáles son las reglas que llevan a código funcional?

Programación funcional puede ser explicada simplemente siguiendo estas dos reglas en tu código.

  1. Diseña tu software a partir de funciones puras y aisladas
  2. Evitar mutabilidad y efectos secundarios

Veamos que se trata eso.

1. Diseña tu software a partir de funciones puras y aisladas

Empecemos por el principio,

Código funcional utiliza bastante algunas cosas:

Funciones puras

La misma entrada siempre da la misma salida (idempotencia) y no tiene efectos secundarios.

Una función idempotente es aquella que, cuando vuelve a aplicar los resultados a esa función nuevamente, no produce un resultado diferente.

/// Algunos ejemplos que utiliza Math.abs(retorna el valor absoluto de un numero)
Math.abs('-1');     // 1
Math.abs(-1);       // 1
Math.abs(null);     // 0


Math.abs(Math.abs(Math.abs('-1')));           // Sigue retornando 1
Math.abs(Math.abs(Math.abs(Math.abs('-1')))); // Sigue retornando 1
Ejemplo de función idempotente

Los efectos secundarios son cuando tu código interactúa con (lee o escribe) un estado mutable externo.

El estado mutable externo es literalmente cualquier cosa fuera de la función que cambiaría los datos en tu programa. ¿Establecer una función? ¿Establecer un booleano en un objeto? ¿Eliminar propiedades de un objeto? Todos los cambios de estado fuera de tu función.

function estaDisponible(){
	disponible = true;
}
Un ejemplo de estado mutable que se establece dentro de sus funciones.

Funciones aisladas

No dependen del estado del programa, que incluye variables globales que están sujetas a cambios.

Discutiremos esto más a fondo, pero todo lo que necesites debe pasarse a la función como argumento. Esto hace que tus dependencias (cosas que la función necesita para hacer su trabajo) sean mucho más claras de ver y más detectables.

¿Ok, porque hacemos así las cosas?

Sé que esto parecen muchas restricciones que hacen que tu código sea innecesariamente difícil. Pero no son restricciones, son pautas que intentan evitar que caigas en patrones que comúnmente conducen a errores.

Cuando no estás cambiando la ejecución de tu código, dividir tu código con if's basado en el estado del  booleano, siendo establecido por múltiples lugares en tu código, hace que el código sea más predecible y es más fácil razonar sobre lo que está sucediendo.

Cuando sigas el paradigma funcional, encontrarás que el orden de ejecución de tu código no importa tanto.

Esto tiene bastantes beneficios: uno es, por ejemplo, que para replicar un error no es necesario saber exactamente cuál era el estado de cada booleano y objeto antes de ejecutar sus funciones. Siempre que tengas una pila de llamadas (sabes qué función se está ejecutando / se ha ejecutado antes), se pueden replicar los errores y resolverlos más fácilmente.

Reusabilidad mediante funciones de orden superior

Las funciones que se pueden asignar a una variable, pasar a otra función o devolver desde otra función como cualquier otro valor normal, se denominan funciones de primera clase.

En JavaScript, todas las funciones son funciones de primera clase. Funciones que tienen un estado de primera clase nos permiten crear funciones de orden superior.

Una función de orden superior es una función que toma una función como argumento, devuelve una función o ¡ambas cosas! Puedes usar funciones de orden superior para dejar de repetir código.

Algo así:

// Ejemplo no funcional
const edad = [12,32,32,53]
for (var i=0; i < edad.length; i++) {
    edadFinal += edad[i];
}

// Ejemplo funcional
const edad = [12,32,32,53]
const edadTotal = edad.reduce( function(primerEdad, segundaEdad){
    return primerEdad + segundaEdad;
})

Las funciones de arreglos incluidas en JavaScript .map, .reduce y .filter aceptan una función. Son excelentes ejemplos de funciones de orden superior, ya que iteran sobre un arreglo y llaman a la función que recibieron para cada elemento del arreglo.

Así que podrías hacer lo siguiente:

// Ejemplos de cada uno
const arreglo = [1, 2, 3];

const arregloMap = arreglo.map(function(elemento){
    return elemento + 1;
});
// arregloMap es [2, 3, 4]

const reducido = arreglo.reduce(function(primero, segundo){
	return primero + segundo;
});
// reducido es 6

const filtrarArreglo = arreglo.filter(function(elemento){
    return elemento !== 1;
});
// filtrarArreglo is [2, 3]

Pasar los resultados de funciones a otras funciones, o incluso pasar las funciones en sí, es extremadamente común en el código funcional. Incluí esta breve explicación debido a la frecuencia con la que se usa.

Estas funciones también se utilizan a menudo porque no cambian la función subyacente (no cambian el estado), sino que operan con una copia del arreglo.

2. Evitar la mutabilidad y los efectos secundarios.

La segunda regla es evitar la mutabilidad - ya lo mencionamos brevemente antes, cuando hablamos de limitar los cambios al estado mutable externo - y sus efectos secundarios.

Pero aquí lo explicaremos mejor. Básicamente, todo se reduce a esto: ¡no cambies las cosas! Una vez que lo haz hecho, es inmutable (No cambiará más).

var edades = [12,32,32,53]
edades[1] = 12;  // no!
edades = [];     // no!
edades.push("2") // no!
Este código no lleva patrón funcional.

Si algo tiene que cambiar en tu estructura de datos, haz los cambios en una copia.

const edades = [12,32,32,53]
const edadesNuevas = edades.map(function (edad){
    if (edad == 12) { return 20; }
    else { return edad; }
})
Mucho mejor!

¿Vez que hice una copia con mis cambios necesarios?

Este elemento se repite una y otra vez. ¡No cambies el estado!

Si seguimos esa regla, haremos un uso intensivo de const para saber que las cosas no cambiarán. Pero tenemos que ir más allá. ¿Qué tal lo siguiente?

const objetoCambiante = {
    cambia: 10
}

objetoCambiante.cambia = 10;  // no!
delete obj.cambia      // no!

Las propiedades de objetoCambiante deberían estar completamente bloqueadas. const solo te protegerá de inicializar la misma variable.

const obj = Object.freeze({
    noCambia: 'Locked' }) // La función "freeze" aplica la inmutabilidad.

obj.noCambia = 0      // No cambia el objeto!
delete obj.noCambia   // No cambia el objeto!
obj.agregarProp = "Te la creiste!" // No cambia el objeto!
Utiliza estructuras de datos persistentes


Si no podemos cambiar el estado global de las variables, entonces debemos asegurarnos de:

  • Declarar los argumentos de la función: cualquier cálculo dentro de una función depende solo de los argumentos y no de ningún objeto o variable global.
  • No modificar una variable u objeto: creamos nuevas variables y objetos y los devolvemos si es necesario, desde una función.

Haz que tu código sea referencialmente transparente

Cuando sigues la regla de nunca cambiar de estado, tu código se vuelve referencialmente transparente. Es decir, tus llamadas a funciones se pueden reemplazar con los valores que representan, sin afectar el resultado.

Como un ejemplo simple de verificar si tu código es referencialmente transparente, mira el siguiente fragmento de código:

const conoceAutor = function(){
    return 'Hola Kealan'
}

Deberías poder simplemente intercambiar esa llamada a la función con la cadena que devuelve y no tener problemas.

La programación funcional con expresiones referencialmente transparentes te hace comenzar a pensar en tu código de manera diferente si estás acostumbrado a la orientación a objetos.

¿Pero, por qué?

Porque en lugar de objetos y estados mutables en tu código, comienzas a tener funciones puras, sin cambios de estado. Entiendes claramente lo que esperas que devuelva tu función (ya que nunca cambia, cuando normalmente podría devolver diferentes tipos de datos dependiendo del estado fuera de la función).

Puede ayudarte a comprender mejor el flujo, comprender lo que está haciendo una función con solo escanearla y ser más riguroso con las responsabilidades de cada función para crear mejores sistemas desacoplados.

Puedes aprender más acerca de transparencia referencial aquí.

No hagas iteraciones

Con suerte, si has prestado atención hasta ahora, verás que no estamos cambiando el estado. Así que para aclarar, abandonaremos los bucles for :

for(let i = 0; i < arr.length; i++) {
    total += arr[i];
}

Porque estamos cambiando el estado de una variable. Utiliza en vez, la función de orden superior map.

Más características de la programación funcional

Espero que hasta este punto tengas una buena descripción de lo que es y no es el código funcional. Pero hay algunos conceptos finales que se usan mucho en el código funcional y que tenemos que cubrir.

En todo el código funcional que he leído, estos conceptos y herramientas son los que más se utilizan, y tenemos que cubrirlos para obtener nuestro conocimiento fundamental.

Así que, aquí vamos.

Recursión en programación funcional

Es posible que en JavaScript una función se llame a sí misma.

Lo que siempre hemos podido hacer:

function recurrir(){
    recurrir();
}
Tal vez, esta no sea la mejor idea.

El problema con esto, es que no es útil. Se ejecutará eventualmente hasta que bloquee tu navegador. Pero la idea de recursividad es una función que se llama a sí misma desde dentro. Veamos un ejemplo más útil:

function recurrir(inicio, fin){
    if (inicio == fin) {
        console.log(fin)
        return;
    } else {
        console.log(inicio)
        return recurrir(inicio+1, fin)
    }
}

recurrir(1, 10);
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

Este fragmento de código contará desde el argumento inicio hasta el argumento fin. Y lo hace volviendo a llamar a su propia función.

Así que el orden se verá algo así:

image-135
Un ejemplo de pila de llamadas para esta función recursiva.

Agregue un debugger dentro de los bloques if para seguir esto, por si no te hace mucho sentido. La recursividad es una herramienta que puedes utilizar para iterar en la programación funcional.

¿Qué hace que el primer ejemplo y el segundo ejemplo sean diferentes? El segundo tiene lo que llamamos "un caso base". Un caso base permite que la función deje de llamarse a sí misma infinitamente. Cuando inicio es igual fin, podemos dejar de recurrir. Como sabemos, hemos contado hasta el final de nuestro ciclo.

Pero cada llamada de las funciones vuelve a llamar a su propia función y se suma al argumento de la función.

El ejemplo de código que acabo de incluir para el ejemplo de conteo, no es una función pura. ¿Por qué?

¡Porque console es un estado! Y le hemos registrado strings.

Esta ha sido una breve introducción a la recursividad, pero siéntete libre de ir aquí para obtener más información.

¿Por qué utilizar la recursividad?

Recursividad nos permite dejar de mutar las variables del estado, para empezar.

También hay ciertas estructuras de datos (estructuras de árbol) que son más eficientes cuando se resuelven con recursividad. Por lo general, requieren menos código, por lo que a algunos desarrolladores les gusta lo fácil que es leer la recursividad.

"Currying" en programación funcional

"Currying" es otra herramienta muy utilizada en código funcional. La aridad de una función se refiere a cuántos argumentos recibe.

// Veamos aridad
function aridad2(arg1, arg2){}             // Función tiene una aridad de 2
function aridad0(){}                       // Función tiene una aridad de 0
function aridad2(arg1, arg2, arg3, arg4){} // Función tiene una aridad de 4

Hacer currying a una función convierte esta función que tiene una aridad de más de 1 en 1. Lo hace devolviendo una función interna para tomar el siguiente argumento. He aquí un ejemplo:

function suma(primerNum, segundoNum){
	return primerNum + segundoNum;
}

// "Haciendo curry" a esta función

function currySuma(primerNum){
	return function(segundoNum){
            return primerNum + segundoNum;
    }
}

Esencialmente, reestructura una función para que tome un argumento, pero luego devuelve otra función para tomar el siguiente argumento, tantas veces como sea necesario.

¿Para que usar currying?

El gran beneficio del currying es cuando necesitas reutilizar la misma función varias veces, pero solo cambiar uno (o menos) de los parámetros. Entonces puedes guardar la primera llamada a la función, algo como esto:

function currySuma(primerNum){
	return function(segundoNum){
            return primerNum + segundoNum;
    }
}

let suma10 = currySuma(10);
suma10(2); // Returns 12

let suma20 = currySuma(20);
suma20(2); // Returns 22

Currying también puede hacer que tu código sea más fácil de refactorizar. No tienes que cambiar varios lugares donde estás pasando los argumentos de la función incorrectos, solo el lugar, donde enlazaste la primera llamada a la función al argumento incorrecto.

También es útil si no puedes proporcionar todos los argumentos a una función a la vez. Puede devolver la primera función para llamar a la función interna cuando tengas todos los argumentos más adelante.

Aplicación parcial en programación funcional

De manera similar, la aplicación parcial significa que aplicas algunos argumentos a una función a la vez y devuelve otra función que se aplica a más argumentos. Este es el mejor ejemplo que encontré en los documentos de MDN:

const modulo = {
  altura: 42,
  calcularAltura: function(altura) {
    return this.altura + altura;
  }
};

const aisladoCalcularAltura = modulo.calcularAltura;
console.log(aisladoCalcularAltura(32)); // La función se invoca desde el scope global
// salidas: NaN
// Salida NaN porque this.altura resulta undefined (en el scope de window) 
// undefined + 32 lo que retorna NaN

const unidoCalcularAltura = aisladoCalcularAltura.bind(modulo);
console.log(unidoCalcularAltura(32));
// Salida esperada: 74

bind es el mejor ejemplo de aplicación parcial. ¿Por qué?

Porque devolvemos una función interna que se asigna a unidoCalcularAltura que se llama, con este alcance configurado correctamente y un nuevo argumento pasado más tarde. No asignamos todos los argumentos a la vez, sino que devolvimos una función para aceptar el resto de los argumentos.

¿Por qué utilizar aplicación parcial?

Puedes usar la aplicación parcial siempre que no puedas pasar todos sus argumentos a la vez, pero que puedas devolver funciones de orden superior para lidiar con el resto de los argumentos.

Composición de funciones en programación funcional


El tema final que creo que es fundamental para el código funcional es la composición de funciones.

La composición de funciones nos permite tomar dos o más funciones y convertirlas en una función que hace exactamente lo que hacen las dos funciones (o más).

// Si tenemos esta dos funciones

function suma10(num) {
	return num + 10;
}
function suma100(num) {
    return num + 100;
}

// Podemos componer estas dos a lo siguiente =>
function compuesta(num){
	return suma10(suma100(num));
}

compuesta(1) // Retorna 111


Puedes llevar esto más allá y crear funciones para componer cualquier número de funciones de aridad múltiples juntas si lo necesitas según sea el caso.

¿Por qué usar composición de funciones?

La composición te permite estructurar tu código a partir de funciones reutilizables, para dejar de repetirte a ti mismo. Puedes comenzar a tratar las funciones como pequeños bloques de construcción que puedes combinar para lograr un resultado más complicado.

Estos luego se convierten en las "unidades" o el poder de cálculo en tus programas. Son muchas funciones pequeñas que funcionan genéricamente, todas compuestas en funciones más grandes para hacer el trabajo "real".

Es una forma poderosa de diseñar tu código y evita que crees funciones enormes copiadas y pegadas con pequeñas diferencias entre ellas.

También puede ayudarte a probar cuando tu código no está estrechamente acoplado. Y hace que tu código sea más reutilizable. Puedes simplemente cambiar la composición de tus funciones o agregar funciones más pequeñas a la composición, en lugar de tener todo el código copiado y pegado en todo el código base (para cuando necesites que haga algo similar pero no exactamente igual que otra función) .

El siguiente ejemplo es trivial para ayudarte a comprender, pero espero que veas el poder de la composición de funciones.

/// Este es un ejemplo donde tenemos que copiar y pegar
function suma50(num) {
	return num + 50;
}

// Ok. Ahora necesitamos sumar 30. Pero, TAMBIEN todavia necesitamos sumar 50 en otro lugar 
// Entonces necesitamos una función
function suma30(num){
	return num + 30;
}

// Pff, las cosas vuelven a cambiar
function sumar20(num){
	return num + 20;
}

// Cada vez que necesitamos cambiar la función aunque sea un poco, necesitamos una función nueva.

//Usemos composición

// Nuestra pequeña, función pura reutilizable
function sumar10(num){
	return num + 10;
}

function suma50Compuesta(num){
	return sumar10(sumar10(sumar10(sumar10(sumar10(num)))));
}

function suma30Compuesta(num){
	return sumar10(sumar10(sumar10(num)));
}

function suma20Compuesta(num){
	return sumar10(sumar10(num));
}

¿Ves cómo compusimos nuevas funciones a partir de funciones puras más pequeñas?

Conclusión


Este artículo cubrió mucho. Pero espero que haya explicado el código funcional de manera simple, junto con algunos de los patrones repetidos que verás una y otra vez, en código funcional e incluso no funcional.

El código funcional no es necesariamente el mejor, y tampoco lo es el código orientado a objetos. El código funcional se usa generalmente para problemas más matemáticos como el análisis de datos. También es muy útil para sistemas en tiempo real de alta disponibilidad, como cosas escritas en Erlang (un lenguaje funcional). Pero realmente depende de un problema a otro.

Como aprender más

Puedes comenzar aquí, con la introducción de freeCodeCamp a programación funcional con JavaScript.

Aquí puedes encontrar librerías para practicar, y ser todo un pro en programación funcional.

Traducido del artículo de Kealan Parr - What is Functional Programming? A Beginner's JavaScript Guide