DEV Community

loading...

La utilidad de los reducers

vonheikemen profile image Heiker Originally published at vonheikemen.github.io ・8 min read

En un artículo anterior les hablé del método .reduce, cómo funciona y el caso ideal en el que podemos usarlo (en mi opinion). Esta vez voy a mostrar más casos en los que podría ser una buena opción. Ahora bien, no tienen que haber leído ese artículo pero de aquí en adelante voy a asumir que saben cómo funciona el método Array.reduce. Al finalizar espero que aprendan a reconocer en qué lugares .reduce podría funcionar perfectamente.

¿Qué estamos buscando?

Patrones, buscamos patrones. Bueno... sólo uno. Y para saber qué estamos buscando tenemos que ver los requerimientos de un reducer. Piensen en ellos por un momento, cuando empiezan a escribir uno que quieren usar con Array.reduce tal vez luce así.

function (accumulator, value) {
  /*
    algo de lógica por aquí
  */
  return accumulator;
}
Enter fullscreen mode Exit fullscreen mode

Okey, por lo general retornamos una copia modificada de accumulator pero eso no es importante, el punto es que retornamos el mismo "tipo" de dato que obtuvimos en el primer parámetro. Entonces tenemos que la comportamiento de la función es el siguiente.

(Accumulator, Value) -> Accumulator
Enter fullscreen mode Exit fullscreen mode

Pero en este caso lo que tenemos aquí es un ejemplo concreto. Yo quiero que vean esto de una forma más abstracta. Lo que en realidad buscamos son funciones con esta forma.

(A, B) -> A
Enter fullscreen mode Exit fullscreen mode

Eso es básicamente todo lo que necesitan saber. Para que un reduce pueda hacer bien su trabajo sólo debe ser capaz de retornar el mismo tipo de dato que recibió en el primer parámetro.

¿Aún están confundidos? No se preocupen, pasaremos lo que queda de este artículo revisando ejemplos donde este patrón puede aparecer.

Casos de uso

Acumuladores

Aquí generalmente es la parte donde les muestro una situación donde sumamos un arreglo de números o algo parecido. No hagamos eso. Podemos imaginarnos un escenario más complejo donde un acumulador nos sea útil.

Entonces, vamos a fingir que estamos trabajando en un proyecto que tiene una especie de blog y nos encontramos creando la página del perfil de usuario. Queremos mostrar todos los tags donde el usuario tiene por lo menos un artículo. Quizá quieran extraer ese dato de la base de datos usando una consulta elaborada pero eso llevaría mucho tiempo. Primero hagamos un prototipo.

Antes de hacer las cosas de la manera adecuada lo que vamos a hacer es transformar un arreglo que contiene todos los artículos en un Set con todos los tags, para eso usaremos Array.reduce.

// Imaginen que estos objetos son más complejos
const posts = [
  { tags: ["javascript", "discuss"] },
  { tags: ["javascript", "react", "vue-is-better"] },
  { tags: ["discuss"] },
  { tags: ["javascript"] },
];

function dangerously_add_tags(acc, post) {
  for(let value of post.tags) {
    acc.add(value);
  }

  return acc;
}

posts.reduce(dangerously_add_tags, new Set());
Enter fullscreen mode Exit fullscreen mode

Este sería el resultado.

Set(4) [ "javascript", "discuss", "react", "vue-is-better" ]
Enter fullscreen mode Exit fullscreen mode

Ahora piensen en el comportamiento de nuestro reducer. Tenemos un Set con tags que hace el papel de Accumulator y un objeto que representa un post como nuestro Value. Podríamos decir que se comporta de la siguiente manera.

(Set, Objeto) -> Set
Enter fullscreen mode Exit fullscreen mode

Bueno, técnicamente Objeto no puede ser cualquier objeto, debe tener una propiedad llamada tags. Así que sería algo más como esto.

(Set, Artículo) -> Set
Enter fullscreen mode Exit fullscreen mode

En fin, este es el patrón del que les hablaba (A, B) -> A. La implementación de dangerously_add_tags demanda que B sea un Artículo. Pero para que esta función sea un reducer debe ser capaz de retornar el mismo tipo de dato que recibió en el primer parámetro (Set), y eso lo logramos retornando acc.

Transformaciones

Probablemente han escuchado que pueden usar Array.reduce para suplantar otros métodos del prototipo Array, pero aunque esto suene como un dato interesante no tiene mucha utilidad. ¿Por qué harían algo así? No tiene sentido para mí. Sin embargo, aún puede resultar útil si planean "fusionar" las características de varios de esos métodos en uno. ¿Alguna vez han querido filtrar y transformar un arreglo al mismo tiempo? Con .reduce eso es posible.

Vamos a reusar nuestra variable posts aquí también.

const posts = [
  {
    category: "javascript",
    tags: ["javascript", "discuss"]
  },
  {
    category: "frameworks",
    tags: ["javascript", "react", "vue-is-better"]
  },
  {
    category: "watercooler",
    tags: ["discuss"]
  },
  {
    category: "functional programming",
    tags: ["javascript"]
  },
];
Enter fullscreen mode Exit fullscreen mode

Esta vez lo que queremos hacer es filtrar los que tengan el tag discuss, y por cada uno que pase la prueba queremos extraer la categoría y capitalizar el valor. ¿Cómo haríamos eso?

function capitalize(str) {
  return str[0].toUpperCase() + str.slice(1);
}

function filter_map_posts(acc, post) {
  // aquí estamos filtrando
  if(post.tags.includes('discuss')) {
    return acc.concat(
      // esta es la transformación
      capitalize(post.category)
    );
  }

  return acc;
}

posts.reduce(filter_map_posts, []);
Enter fullscreen mode Exit fullscreen mode

Aquí tenemos nuestro resultado.

Array [ "Javascript", "Watercooler" ]
Enter fullscreen mode Exit fullscreen mode

¿Por qué funciona? Si revisan el comportamiento de filter_map_posts tenemos esto.

(Arreglo, Artículo) -> Arreglo
Enter fullscreen mode Exit fullscreen mode

Coordinación

Si han indagado un poco en librerías enfocadas en el paradigma funcional hay una alta probabilidad de que se hayan topado con una función llamada pipe. Con esta función podemos combinar una cantidad arbitraria de funciones. Esta es la manera en la que se usa.

pipe(
  una_funcion,
  otra,
  proceso_serio,
  efectos_adelante,
);
Enter fullscreen mode Exit fullscreen mode

La idea detrás de esto se centra en transportar el resultado de una función hacia la siguiente en la lista. En efecto lo que hacemos aquí es coordinar llamadas de funciones. En este caso, el fragmento anterior es equivalente a esto:

function pipe(arg) {
  return efectos_adelante(proceso_serio(otra(una_funcion(arg))));
}
Enter fullscreen mode Exit fullscreen mode

Si se preguntan por qué les digo esto, es porque podemos implementar pipe usando .reduce. Si se fijan bien notarán que lo único que hacemos en esa función es aplicar funciones a un argumento. Eso es todo. No hay nada más.

¿Y qué?

¡Es una operación binaria! Podemos transformar eso en una función.

function apply(arg, fn) {
  return fn(arg);
}
Enter fullscreen mode Exit fullscreen mode

¿Y saben qué funciona bien con operaciones binarias? Nuestro amigo .reduce.

function pipe(...fns) {
  return function(some_arg) {
    return fns.reduce(apply, some_arg);
  };
}
Enter fullscreen mode Exit fullscreen mode

Lo primero que hacemos en pipe es recolectar la lista de funciones que usaremos y convertirla en un arreglo. El segundo paso es retornar una función que desencadenará las llamadas a las funciones en nuestro arreglo, también en este paso obtenemos nuestro argumento inicial. Al final de eso, con todo en su lugar, .reduce se encarga del resto. Pueden probarlo ustedes mismos.

const post = { 
  category: "javascript",
  tags: ["javascript", "discuss"] 
}

function capitalize(str) {
  return str[0].toUpperCase() + str.slice(1);
}

function get_prop(key) {
  return function(obj) {
    return obj[key];
  }
}

function exclaim(str) {
  return str + "!!";
}

const exciting_category = pipe(
  get_prop("category"),
  capitalize,
  exclaim
);

exciting_category(post);
// => Javascript!!
Enter fullscreen mode Exit fullscreen mode

Okey, okey. Ahora bien, ¿Cómo es que apply sigue el patrón?

Ah, buena pregunta. Es algo raro pero aún podemos darle sentido. Mírenlo de esta manera.

(Algo, Función) -> Algo
Enter fullscreen mode Exit fullscreen mode

Si tienen una unidad de lo que sea (literalmente cualquier cosa) y una función, apply hará su trabajo. Pero tengan presente que aquí no hay garantía de que su función no explote, esa sería responsabilidad de ustedes.

Cambios de estado a través del tiempo

Este bonus track es para todos aquellos desarrolladores frontend allí afuera.

Si han pasado cualquier cantidad de tiempo investigando sobre librerías para manejar el estado de una aplicación tal vez hayan escuchado de una cosa llamada redux. Esta librería tiene un enfoque interesante porque espera que el usuario (el desarrollador) suministre un reducer que sea capaz de manejar los cambios en el estado de la aplicación. A algunos les parece genial, a otros no. Pero bien sea que estén de acuerdo con esto o no, su enfoque tiene mucho sentido. Déjenme mostrarles.

Empecemos con el reducer. En esta ocasión necesitamos uno con este comportamiento.

(Estado, Acción) -> Estado
Enter fullscreen mode Exit fullscreen mode

Estado y Acción son objetos. Aquí no hay nada extravagante. La "forma" de nuestro Estado depende de la aplicación en la que trabajamos, los desarrolladores pueden hacer lo que quieran con él. La Acción por otro lado debe tener una propiedad type, y redux se asegura de esto.

Entonces, vamos a fingir que este es el estado de una aplicación imaginaria en la que estamos trabajando.

const state = {
  count: 40,
  flag: false
};
Enter fullscreen mode Exit fullscreen mode

Ah, sí. Un milagro de la ingeniería.

Ahora que sabemos cómo luce el Estado, y también sabemos qué necesita una Acción, podemos comenzar a escribir nuestro reducer.

function reducer(state, action) {
  switch(action.type) {
    case 'add':
      return {
        ...state,
        count: state.count + 1,
      };
    case 'subtract':
      return {
        ...state,
        count: state.count - 1,
      };
    case 'toggle_flag':
      return {
        ...state,
        flag: !state.flag,
      };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

Aquí viene la parte curiosa: no necesitamos redux para probar nuestro reducer. Es un reducer genérico, bien podríamos usarlo con Array.reduce para ver qué puede hacer.

const actions = [
  { type: 'add' },
  { type: 'add' },
  { type: 'subtract' },
  { type: 'add' },
  { type: 'subtract' },
  { type: 'add' },
  { type: 'toggle_flag' }
];

actions.reduce(reducer, state);
Enter fullscreen mode Exit fullscreen mode

actions.reduce debería devolvernos otra "instancia" de nuestro estado. En nuestro caso, después de aplicar todas esas acciones, tendríamos el siguiente resultado.

{
  count: 42,
  flag: true
}
Enter fullscreen mode Exit fullscreen mode

Y allí lo tienen, la funcionalidad principal de redux sin redux.

Vamos a dar un paso adelante en nuestro proceso e introduzcamos el concepto de tiempo. Para esto vamos a agregar una tienda "falsa" de redux. Bueno... la tienda será "real" pero será una imitación barata. Empecemos.

function Store(reducer, state) {
  let _listener = null;

  const get_state = function() {
    return state;
  };

  const subscribe = function(listener) {
    _listener = listener;
  };

  const dispatch = function(action) {
    state = reducer(state, action);
    _listener && _listener();

    return action;
  };

  return { get_state, dispatch, subscribe };
}
Enter fullscreen mode Exit fullscreen mode

¿Todo bien? ¿Saben lo que ocurre ahí? La parte que realmente nos interesa es la del dispatch. Esto de aquí.

const dispatch = function(action) {
  state = reducer(state, action);
  _listener && _listener();

  return action;
};
Enter fullscreen mode Exit fullscreen mode

Esta función se encarga de reemplazar el Estado actual. Como mencioné antes, el reducer se encarga de la lógica que dice cómo actualizar el Estado. La tienda (Store) se encarga de la lógica que dice cuando debe actualizarse. Suficiente palabrería, vamos a probarlo.

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const store = Store(reducer, state);
store.subscribe(function() {
  console.log(store.get_state());
});

(async function() {
  store.dispatch({ type: 'add' });
  await delay(500);

  store.dispatch({ type: 'add' });
  await delay(500);

  store.dispatch({ type: 'subtract' });
  await delay(700);

  store.dispatch({ type: 'add' });
  await delay(400);

  store.dispatch({ type: 'subtract' });
  await delay(800);

  store.dispatch({ type: 'add' });
  await delay(100);

  store.dispatch({ type: 'toggle_flag' });
})();
Enter fullscreen mode Exit fullscreen mode

Si ejecutan eso notarán cómo los mensajes aparecen en pantalla (o la cónsola del navegador) con una pequeña demora entre cada uno.

- { count: 41, flag: false }
- { count: 42, flag: false }
- { count: 41, flag: false }
- { count: 42, flag: false }
- { count: 41, flag: false }
- { count: 42, flag: false }
- { count: 42, flag: true }
Enter fullscreen mode Exit fullscreen mode

¿Se dieron cuenta que el resultado final es el mismo que nos dio Array.reduce? ¿No es genial?

Si quieren jugar con el verdadero redux aquí les dejo un ejemplo en codepen.

Conclusión

Espero que en este punto los reducers no parezcan tan misteriosos y aterradores. Sólo recuerden que se trata de una función con este comportamiento.

(A, B) -> A
Enter fullscreen mode Exit fullscreen mode

Eso es todo. No hay ninguna magia extraña detrás de eso. Si pueden hacer que una función tenga esas características pueden estar seguros que funcionará de maravilla con cualquier cosa que actue como .reduce.

Fuentes

Discussion (4)

pic
Editor guide
Collapse
juanfrank77 profile image
Juan F Gonzalez

Que interesante esto sobre funcionalidad de Redux sin Redux. Este método reduce me cae bien no sólo por ser bien útil sino porque recuerda la funcionalidad de los "folds" en Haskell y Functional Programming FTW!

Collapse
vonheikemen profile image
Heiker Author

Que interesante esto sobre funcionalidad de Redux sin Redux.

Era un dato curioso que siempre quise mencionar. Ahora que en react tiene un hook llamado useReducer ese dato puede resultar útil incluso si la gente ya no usa redux.

recuerda la funcionalidad de los "folds" en Haskell y Functional Programming FTW!

No sé mucho de haskell pero según la especificación Fantasy Land, definitivamente hay una relación.

Collapse
juanfrank77 profile image
Juan F Gonzalez

Nunca había visto eso de Fantasy Land aunque se ve cool. Pero si reduce en JS es básicamente un foldl en Haskell osea coger un arreglo y "doblarlo" de izquierda a derecha.
También está foldr que hace lo mismo pero al revés.

Thread Thread
vonheikemen profile image
Heiker Author

También existe el método .reduceRight en los arreglos.