La programaci贸n funcional es una forma de entender nuestro software como una serie de funciones matem谩ticas en las que, dadas unas entradas determinadas, siempre obtenemos las mismas salidas.

Aunque est茅 "de moda", este paradigma existe desde antes que la orientaci贸n a objetos, y est谩 basado en el c谩lculo lambda. Sin embargo, hasta hace relativamente poco permanec铆a en el 谩mbito acad茅mico y cient铆fico.

En el caso de JavaScript, aunque no es un lenguaje funcional puro (ya que implementa caracter铆sticas imperativas), su flexibilidad y dinamismo nos permiten adoptar un enfoque funcional y m茅todos como map, filter y reduce nos son de gran ayuda.

Los beneficios de la FP sobre nuestro c贸digo son:

  • Es mucho m谩s f谩cil de testear.
  • Se reduce la complejidad, al enfocarse en qu茅 vamos a hacer, y no c贸mo lo haremos.
  • El c贸digo es m谩s modular, y por lo tanto, m谩s sencillo de entender.
  • Tambi茅n es m谩s confiable, al tener la seguridad de que no cambiaremos el valor de ning煤n recurso compartido.

Algunos conceptos de la programaci贸n funcional

Par谩metros y argumentos

Los argumentos son los valores con los que llamamos a las funciones, mientras que los par谩metros son las variables nombradas que reciben estos valores dentro de nuestra funci贸n:

const double = x => x * 2; // x es el par谩metro de nuestra funci贸n

double(2); // 2 es el argumento con el que llamamos a nuestra funci贸n

Aridad

La aridad de una funci贸n hace referencia al n煤mero de par谩metros que tiene. As铆 pues, una funci贸n de aridad 1 (o unaria) tiene 1 par谩metro, una unidad de aridad 2 (o binaria) tiene 2 par谩metros y as铆 sucesivamente (por cierto, a la de aridad 3 se le llama ternaria).

En JavaScript, es posible llamar a una funci贸n con m谩s argumentos de los par谩metros que soporta. Simplemente ser谩n ignorados.

const double = x => x * 2;

console.log(double(2, 5)); // double(2); // 4;

Funciones puras

Una funci贸n es pura cuando:

  • Su salida depende s贸lamente de los par谩metros recibidos (es determinista), por lo que una llamada a la funci贸n se podr铆a sustituir por el valor que devuelve sin que el funcionamiento de la aplicaci贸n se viese alterado (transparencia referencial).
  • Los par谩metros no son modificados y no se producen efectos colaterales. Imaginemos que varias partes de la aplicaci贸n apuntasen a la misma referencia que ha recibido nuestra funci贸n como argumento y que esta referencia original fuese alterada鈥

En el caso de JavaScript, esto tiene especial importancia, porque tanto los arrays como los objetos (tambi茅n las funciones) se copian por referencia y no por valor. Veamos un ejemplo de un efecto colateral.

// Queremos crear un objeto igual a objA, pero con "prop2" igual a  "newVal2"

const objA = {
  prop1: 'val1',
  prop2: 'val2',
};

const objB = objA;
objB.prop2 = 'newVal2';

console.log(objB); // { prop1: 'val1', prop2: 'newVal2' } 鉁 Tenemos nuestro nuevo objeto, pero...
console.log(objA); // { prop1: 'val1', prop2: 'newVal2' } 鉂 hemos modificado el objeto original

console.log(objA === objB); // true 鉂

Si queremos obtener un nuevo objecto, tan solo tenemos que hacer una destructuraci贸n del objeto, a帽adiendo las claves que queremos modificar, o bien usar Object.assign:

const objA = {
  prop1: 'val1',
  prop2: 'val2',
};

const objB = { ...objA, prop2: 'newVal2');
// o
const objB = Object.assign({}, objA, { prop2: 'newVal2' });

console.log(objB); // { prop1: 'val1', prop2: 'newVal2' } 鉁
console.log(objA); // { prop1: 'val1', prop2: 'val2' } 鉁

console.log(objA === objB); // false 鉁

Debemos llevar cuidado con m茅todos como reverse, splice, etc, ya que mutan el array original. Una soluci贸n ser铆a buscar un m茅todo alternativo o bien crear un nuevo array antes de llamar a esos m茅todos:

const options = ['a', 'b', 'c', 'd'];

// en vez de splice(1, 2);
const filteredOptions = options.filter(option => option !== 'b' && option !== 'c'); 

const reversedOptions = [...options].reverse();

Algunas librer铆as, como React, basan su filosof铆a en el concepto de immutabilidad, ya que los componentes son renderizados solo cuando alguna de las propiedades cambia.

En el caso anterior de efecto colateral, como objA es igual a objB, si se pasase ese objeto como propiedad, podr铆a suceder que el componente no fuese capaz de detectar esos cambios, y reflejase un estado desactualizado.

Con repecto a la transparencia referencial, imaginemos esta funci贸n:

const runAtFive = () => {
    if ((new Date()).getHours() === 17) {
        externalFunction();
    }
};

Si quisieramos testearla correctamente, necesitar铆amos hacer un mock del objeto Date (o ejecutar el test solo a las 17h 馃ぃ), adem谩s de tener que observar externalFunction.

Una aproximaci贸n m谩s correcta ser铆a pasar como argumento la hora actual, as铆 como el callback que queremos ejecutar. El retorno ser铆a el resultado de ejecutar el callback, o false en caso contrario, lo que hace a nuestra funci贸n mucho m谩s sencilla de testear.

const runAtFive = (hour, cb) => hour === 17 && cb();

Algunos autores consideran que la utilizaci贸n de constantes dentro de las funciones no viola los principios anteriormente descritos. Por ejemplo:

const SITE_NAME = 'Software Crafters';

const getFullTitle = (sectionTitle) => `${sectionTitle} | ${SITE_NAME}`;

En este caso, getFullTitle producir铆a siempre el mismo resultado para una determinada entrada y, adem谩s, si en la funci贸n reemplaz谩semos el uso de SITE_NAME por su valor, el resultado de la funci贸n permanecer铆a inalterable.

Funciones de Alto Orden (Higher Order Functions)

Las funciones de alto orden son aquellas que se env铆an como argumento a otra funci贸n, o bien son devueltas como resultado de la ejecuci贸n de otra funci贸n.

const double = x => x * 2;
[1,2,3,4,5].map(double); // double actuar铆a como HOF (Higher Order Function);

// add devuelve una HOF
const add = (x) => {
  if (typeof x === 'number') {
    return y => x + y;
  }
  return y => `${x}${y}`;
}

const addFive = add(5); HOF: y => 5 + y;
console.log(addFive(3)); // 8
console.log(addFive(12)); // 17

Dado que, como veremos m谩s adelante, una de las caracter铆sticas principales de la programaci贸n funcional es la composici贸n de funciones, este concepto nos resulta realmente 煤til.

Forma declarativa

Tal y como la propia palabra expresa, cuando programamos de forma declarativa estamos haciendo uso de un alto nivel de abstracci贸n, para decirle al lenguaje (o librer铆a) qu茅 es lo que queremos obtener, en vez de decirle c贸mo debe obtenerlo.

En el ejemplo anterior, podemos escribir en forma declarativa:

[1,2,3,4,5].map(double);

o bien en forma imperativa:

let arr = [];
for(i=0; i<5; i++) {
  arr.push((i + 1) * 2);
}

Ejemplos de lenguajes y librer铆as declarativas podr铆an ser GraphQL y React:

export const USERS_QUERY = gql`
  query usersQuery {
    users {
      edges {
        node {
          id
          title
          description
          categories
        }
      }
    }  
  }
`;


const { data, loading, error } = useQuery(USERS_QUERY);
return (
  <div>
    {data.map(({ id, title }) => <div key={id}>{title}</div>)}
  </div>
)

Recursividad

La recursividad se da cuando una funci贸n se llama a si misma, y es esencial cuando queremos trabajar de forma funcional con estructuras de datos.

const concatenateAll = (target, source) => {
  if (Array.isArray(source)) {
    return source.reduce(concatenateAll, target);
  }
  if (typeof source === 'object') {
    return Object.values(source).reduce(concatenateAll, target);
  }
  return `${target}#${source}`;
};

const data = {
  name: 'Jos茅 Manuel',
  surname: 'Lucas',
  pet: {
    type: 'dog',
    name: 'Hustle',
  },
  hobbies: ['travelling', 'music', 'mountain biking'],
};

console.log(concatenateAll('', data)); // #Jos茅 Manuel#Lucas#dog#Hustle#travelling#music#mountain biking

Composici贸n de funciones

Imaginemos estas dos funciones:

const add3 = x => x + 3;
const double = x => x * 2;

Podr铆amos decir que double(add3(2)) es el resultado de componer "double" y "add3" sobre "2".

  • Se calcula el resultado de aplicar la funci贸n "add3" a "2".
  • Se calcula el resultado de aplicar la funci贸n "double" al resultado anterior.

Dado que podemos predecir el resultado de "add3" y de "double" por separado bas谩ndonos en su argumento, podr铆amos predecir de igual manera el resultado de la composici贸n de las dos funciones.

Para facilitar la escritura de las composiciones de nuestras funciones, se suele hacer uso de las utilidades compose y pipe. Inclu铆das en librer铆as como ramda o lodash/fp, si bien es cierto que la implementaci贸n b谩sica es muy sencilla:

const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

La diferencia es el orden en el que se pasan las funciones:

  • Compose: Derecha a izquierda. M谩s cercano a la notaci贸n matem谩tica.
  • Pipe: Izquierda a derecha, m谩s cercano al orden de evaluaci贸n.
console.log(pipe(add3, double)(2)); // 10 鉁
console.log(compose(add3, double)(2)); // 7鉂
console.log(compose(double, add3)(2)); // 10 鉁

Estilo t谩cito o "point free"

Consiste en omitir los argumentos en aquellos casos donde es posible y hace que nuestra composici贸n de funciones quede mucho m谩s limpia.

Comparemos el ejemplo anterior con un ejemplo sin aplicar el estilo t谩cito:

pipe(add3, double)(2);

//vs

pipe(
  x => add3(x),
  x => double(x),
)(2);

Currificaci贸n

La currificaci贸n (estricamente hablando) consiste en transformar una funci贸n con x par谩metros en una secuencia de x funciones con un solo argumento.

const sum3 = (a, b, c) => a + b + c;
const curriedSum3 = curry(sum3);

console.log(sum3(2, 4, 3)); // 9
console.log(curriedSum3(2)(4)(3)); // 9

Una implementaci贸n sencilla podr铆a ser:

const curry = (fn, arity = fn.length) => {
  const nextCurried = prevArgs => nextArg => {
    const args = [...prevArgs, nextArg];
    return args.length >= arity ? fn(...args) : nextCurried(args);
  };
  return nextCurried([]);
}

Sin embargo, en librer铆as como ramda, la utilidad curry permite llamar a la funci贸n en cualquiera de sus formas, y no s贸lo argumento a argumento:

import { curry } from 'ramda'; 

const sum3 = (a, b, c) => a + b + c;
const curriedSum3 = curry(sum3);

sum3(2, 4, 3);
sum3(2)(4)(3);
sum3(2, 4)(3);
sum3(2)(4, 3);

Aplicaci贸n parcial

La aplicaci贸n parcial es una t茅cnica similar a la currificaci贸n, solo que en este caso producimos una funci贸n con una aridad que no necesariamente es 1, simplemente es menor que la funci贸n original:

const operation = (a, b, c) => a + b - c;

const partialedOp = partial(operation, [2]);
console.log(partialedOp(4, 3)); // 3

// o

const partialedOp = partial(operation, [2, 4]);
console.log(partialedOp(3)); // 3

La implementaci贸n podr铆a ser algo as铆:

const partial = (fn, predefArgs) => (...args) => fn(... predefArgs, ...args);

Tambi茅n es posible la aplicaci贸n parcial desde la derecha, es decir, pasar los 煤ltimos argumentos y despu茅s los primeros:

const operation = (a, b, c) => a + b - c;

const partialedOp = partialRight(operation, [3]);
console.log(partialedOp(2, 4)); // 3

// o

const partialedOp = partialRight(operation, [4, 3]);
console.log(partialedOp(2)); // 3

Aislando los efectos colaterales

Si nuestras aplicaciones no pudiesen manejar efectos colaterales bajo ning煤n concepto no podr铆an hacer uso de:

  • Estado compartido
  • Eventos
  • Peticiones a una API
  • Input del usuario
  • Lecturas de tiempo
  • Escritura o lectura en disco
  • Generaci贸n de hashes o de n煤meros aleatorios
  • etc

Al aplicar un enfoque funcional a nuestro c贸digo, el objetivo debe ser aislar todos esos efectos colaterales, no sustituirlos por completo.

Esto se podr铆a resolver mediante una inyecci贸n simple de dependencias鈥

/**
 * C脫DIGO PURO
 */
const updateKey = (key, fn) => (obj) => ({
    ...obj,
    [key]: fn(obj[key]),
});

const applyPrizeDecrease = updateKey('prize', currentPrize => currentPrize * 0.95);
const applyExpiryDecrease = updateKey('expiry', currentExpiry => currentExpiry > 0 ? currentExpiry - 1 : 0);

const updateItem = pipe(
    applyPrizeDecrease,
    applyExpiryDecrease,
);
const updateData = currentData => currentData.map(getUpdatedItem);

/**
 * C脫DIGO IMPURO
 */
(async () => {
    // Usamos un array vac铆o como salvaguarda, en caso de que la API responda con un valor "falsy"
    let data = await APIService.get('/endpoint') || [];
    setTimeout(() => {
        data = updateData(data);
    }, 86400000);
})();

Imaginemos que queremos obtener el breakpoint actual en base a la anchura de nuestra pantalla. Tan s贸lo deber铆amos aislar nuestros side effects (obtener el objeto window) e inyectarlo como dependencia en nuestras funciones puras, que se encargan de extraer la anchura de la pantalla, y de transformarla en un string con el valor del breakpoint.

/**
 * C脫DIGO PURO
 **/
const getWidth = x => x.screen.width;
const getBreakpoint = (x) => {
  if (x > 1200) {
    return 'desktop';
  }
  if (x > 600) {
    return 'tablet';
  }
  return 'mobile';
}

const getBreakpointFromWidth = pipe(getWidth, getBreakpoint);

/**
 * C脫DIGO IMPURO
 **/
 console.log(getBreakpointFromWidth(width));

Otra opci贸n ser铆a usar un functor, que es una funci贸n que se encarga de transformar una categor铆a en otra.

Un ejemplo sencillo ser铆a el functor Array, que con el m茅todo map nos permite transformar cada uno de sus elementos de una categor铆a a otra (por ejemplo de number a string, mediante la funci贸n getFeelFromTemp).

const getFeelFromTemp = temp => (temp > 22 ? 'hot' : 'cold');

const temps = [24, 19, 13, 32];
const feels = temps.map(getFeelFromTemp);
console.log(feels); // ['hot', 'cold', 'cold', 'hot']

Para el prop贸sito que nos ocupa, crearemos un functor effect, que se encargar谩 de mantener nuestro c贸digo puro mientras lo "mapeamos", hasta que lo lancemos con "run".

/**
 * C脫DIGO PURO
 */
class Effect {
  static of(f) {
    return new Effect(f);
  }

  constructor(f) {
    this.f = f;
  }

  map(g) {
    return Effect.of(x => g(this.f(x)));
  }

  run() {
    return this.f();
  }
}

const getWindow = () => window;
const getWidth = x => x.screen.width;
const getBreakpoint = (x) => {
  if (x > 1200) {
    return 'desktop';
  }
  if (x > 600) {
    return 'tablet';
  }
  return 'mobile';
}

const breakpointEffect = Effect.of(getWindow).map(getWidth).map(getBreakpoint);

/**
 * C脫DIGO IMPURO
 */
console.log(breakpointEffect.run(100));

Bibliograf铆a

Descrubre nuestro e-book

Si quieres continuar mejorando como desarrollador Javascript te recomendamos nuestro e-book de Clean Code, SOLID y Testing aplicado a JavaScript .

Profundizamos en temas como la deuda t茅cnica y cuales son los tipos, Clean Code desde el punto de vista de mejorar la legibilidad, SOLID para obtener un c贸digo m谩s intuitivo y tolerante a cambios, y Unit testing para obtener proyectos de mayor calidad y seguridad... Adem谩s, puedes empezar a leer los primeros cap铆tulos gratis.

e-book de Clean Code, SOLID y Testing aplicado a JavaScript