Funciones puras, impuras y efectos

August 01, 2019

...

En la programación funcional se incentiva discernir entre funciones puras e impuras, donde debiésemos intentar escribir más funciones puras que impuras ya que estas son mas fáciles de predecir y también de escribirle pruebas. Sin embargo todos los tutoriales que vemos en internet está lleno de funciones impuras, por lo que es difícil encontrar la manera correcta de cómo deberíamos separar nuestro código. Cuando le tomamos la mano al desarrollo con funciones puras y dejamos los efectos o funciones impuras de lado nuestra vida cambiará para siempre y para bien!, desarrollar con funciones puras hace que sea un verdadero alivio redactar pruebas para nuestra aplicación y también hace que este sea más fácil de componer. Las funciones impuras también pueden componerse, pero la composición de funciones tanto puras como impuras las veremos mas adelante, por ahora nos centraremos en entender que son y cómo separarlas.

Efectos

Las funciones impuras son aquellas que no siempre retornan lo mismo o que pueden leer y/o escribir de un estado que puede cambiar.

Vamos a ver un ejemplo:

const getTimestamp = () => (new Date()).getTime()

const a = getTimestamp()

// unos segundos después...

const b = getTimestamp()

a === b // false

Al crear un objeto de fecha con new Date() javascript tomará la fecha de tu computador en ese momento determinado, o la fecha del servidor si estas desarrollando remoto. La fecha, o el tiempo, siempre esta avanzando por lo que la próxima vez que llames a esta función esta devolverá otro objeto de fecha el cual se encuentra en el futuro. Luego sí intentamos obtener el timestamp con el método getTime() este será distinto al siguiente timestamp debido a que el tiempo siempre esta avanzando. Por ende, no importa cuántas veces llamemos a esta función esta siempre nos devolverá algo distinto.

En este caso la fecha tiene un estado interno. Si no tienes claro lo que es un estado estos son "datos que cambian con el tiempo o interacciones", como los de una base de datos, una api, un libro en el cual tu escribes, una hoja de papel que puedes romper, etc... pero en el mundo de TI son datos que cambian constantemente. Y si vas a consultar estos datos no siempre te devolverán lo mismo.

Ahora vamos a ver un ejemplo más real:

const getUsers = () =>
  fetch('/users').then(x => json())
  
const main = async () => {
  const users = await getUsers()
  console.log(users[0])
}

main()

En este caso tenemos un llamado a la API, no te fijes si esto esta bien hecho o no. Enfoquémonos solo en que es lo que sucede con la función main.

La función main si nosotros la llamamos cada un minuto esta nos devolverá el ultimo usuario de la aplicación, acá quiero hacer una afirmación muy importante:

No importa que tan seguros nos sintamos de que esta nos devolverá un usuario, si un usuario se inscribe ya no es el mismo usuario de antes y además esta función puede fallar.

Esto que digo es sumamente importante ya que no importa la confianza que se tenga, el usuario puede quedarse sin internet en la mitad de la operación, el servidor puede arrojar un error, el usuario se dio de baja, se inscribió otro usuario, etc.

En este caso, la API tiene su propio estado, tiene resultados que pueden ir cambiando con el tiempo y si a eso le sumamos los posibles errores que podamos tener al ejecutar la función tenemos un montón de posibilidades que pueden ocurrir cuando ejecutemos esa función.

Los dos casos anteriores son funciones impuras. No importa que tan seguro estemos de ellas, siempre nos pueden devolver algo distinto bajo condiciones externas a la función.

Recapitulamos, una función impura es una que lee o escribe de un estado y esta no siempre nos devuelve lo mismo.

Con esto también nos damos cuenta de que una función impura es aquella que puede escribir en una base de datos o api:

const addUser = (user) =>
  fetch('/users', {
    method: 'POST',
    body: JSON.stringify(user)
  }).then(x => json())
  
const main = async () => {
  await addUser({ name: 'Chanchito feliz' })
}

main()

En este caso main sigue siendo igual de impura ya que en este caso esta llamando a un servicio externo, el cual no sabemos si vas explotar, nos devuelve algún dato poco predecible o incluso... funciona correctamente 😱!

Ya sabemos que son las funciones impuras, ahora vamos a darle paso a las hermosas, sensuales y maravillosas funciones puras.

Fijate en el siguiente ejemplo:

const suma = (a ,b) => a + b

suma(1, 2) === suma(1, 2) // true

Es impresionante como un ejemplo tan sencillo puede representar tan bien lo que es una función pura. En este caso no importa cuántas veces llamemos a la función suma con los argumentos 1 y 2, el resultado siempre será el mismo: 3. A esta función la podemos llamar millones de veces, el resultado siempre será tres. Esta es la esencia de una función pura. No escribe en un estado externo, no lee de un estado externo, solo realiza computaciones con los argumentos que la función entrega, sin embargo esta función podría utilizar otras funciones y, mientras estas otras funciones sean puras, nuestra función seguirá siendo pura. Esto hace que nuestro código sea muy sencillo de probar, redactar pruebas para estas funciones es muy sencillo y predecir el resultado también lo es!

Con esto quiero decir de que existe un beneficio evidente en las funciones puras, siempre deberíamos intentar escribir código de manera que este sea lo más puro posible y mantener los efectos en su propio lugar y haciendo lo menor posible. Veamos un ejemplo ingenuo de como podemos separar nuestras funciones puras de funciones impuras:

const success = (user) => {
  // user acá siempre va a existir
}

const error = (error) => {
  // acá siempre va a existir un error
}

const fetchUser = (id) => {
  try {
    const { data } = await axios.get(`/users/${id}`)
    success(data)
  } catch (e) {
    error(e)
  }
}

Nuevamente esta no es la mejor implementación, pero miren atentamente que ocurrió:

Tomamos el efecto de ir a buscar el usuario y lo encapsulamos en una función que puede tener éxito o fracaso y nada más, en el caso de éxito utilizamos nuestra función success, en el caso de error utilizamos la función error, de esta manera la función de traer usuario se encargo de traer el usuario y nada más, esto es muy importante porque logramos encapsular el efecto y ademas logramos encapsular el caso de éxito y fracaso, lo que convierte a nuestras funciones en funciones completamente reutilizables y fáciles de probar con tests!

Recapitulamos!, aprendimos lo que son funciones puras e impuras, aprendimos la que son los efectos y los problemas que nos pueden traer, aprendimos también como encapsular el comportamiento que puede generar efectos y también aprendimos a como escribir código que separe las funciones puras de las impuras.

Espero que les haya gustado este post, no olviden seguirme en twitter y youtube! Links más abajo.


Suscríbete

Suscríbete a la lista para más cursos, posts y videos tutoriales. Prometo no enviarte más de un correo semanal 🙏

Creado por Nicolás Schürmann ingeniero e instructor de software. Cuando no está programando, esta frente a una cámara dictando cursos, creyéndose youtuber o apoyando a sus alumnos. Puedes seguirlo en twitter o también suscribirte a su canal de youtube. Considera comprar sus cursos por este medio y así apoyas al instructor.