Pura Felicidad con Funciones Puras

Publicado en

Este artículo es una adaptación del capítulo 3 del libro digital: “Professor Frisby’s Mostly Adequate Guide to Functional Programming” [1] escrito por Brian Lonsdorf.

Hasta hace unos años, la programación funcional se utilizaba más que nada en ámbitos académicos y de investigación. Sin embargo, en los últimos años ha cobrado gran popularidad en el desarrollo de aplicaciones debido a los beneficios de desempeño que otorga al generar código altamente paralelizable. Así que cualquier desarrollador que pretenda destacar, requiere saber programación funcional.

Una de las primeras cosas que hay que entender para hacer programación funcional es el concepto de una función pura.

Una función pura es aquella que, dada una misma entrada, siempre regresa el mismo valor de salida y no tiene otro efecto secundario observable.

Por ejemplo, consideremos las funciones slice y splice de javascript. Podríamos decir que ambas hacen lo mismo —de una forma muy distinta. Decimos que slice es pura porque porque regresa el mismo valor siempre. En cambio, splice modifica el arreglo sobre el que opera, alterándolo de forma permanente.

var xs = [1,2,3,4,5];
// pura
xs.slice(0,3); //=> [1,2,3]
xs.slice(0,3); //=> [1,2,3]
xs.slice(0,3); //=> [1,2,3]
// impura
xs.splice(0,3); //=> [1,2,3]
xs.splice(0,3); //=> [4,5]
xs.splice(0,3); //=> []

Listado 1. Mutación de datos

En la programación funcional, evitamos a funciones como splice que mutan datos. Lo que buscamos son funciones limpias y confiables que regresen el mismo resultado siempre, no funciones que dejen un desastre.

Veamos otro ejemplo:

// impura
var minimo = 18;
var checarEdad = function(edad) {
  return edad >= minimo;
};
// pura
var checarEdad = function(edad) {
  var minimo = 18;
  return edad >= minimo;
};

Listado 2. Dependencia de estado

En la versión impura, checarEdad depende de la variable mutable minimo para determinar el resultado. Es decir que una misma entrada podría dar resultados distintos, dependiendo del estado.

Depender del estado del sistema no es deseable porque al introducir el ambiente externo aumentamos la carga cognitiva (el contexto que debemos monitorear para ejecutar nuestra función) lo cual le agrega puntos posibles de falla a nuestro código además de hacerlo menos portable.

Este sencillo ejemplo parece inofensivo, pero la dependencia en el estado es una de las causas principales de complejidad en los componentes de software [2].

Efectos secundarios

Al definir lo que es una función pura, mencionamos que no debe tener efectos secundarios observables. Con esto nos referimos a cualquier cambio de estado en el sistema o interacción observable con el exterior, que ocurre durante el cálculo del resultado de una función.

Algunos ejemplos comunes de efectos secundarios serían:

  • mutar datos.
  • modificar archivos.
  • insertar un registro en una base de datos.
  • hacer una llamada http.

En esencia, cualquier interacción con el mundo exterior a una función, es un efecto secundario. Viéndolo de esta forma, programar funciones sin efectos secundarios parece poco práctico y poco útil.

En realidad no es que nuestras funciones no puedan interactuar con el exterior, sino que deben hacerlo de manera específica y controlada. Un mecanismo para hacer esto es apoyándose en mónadas y functors, que no revisaremos en este artículo pero el lector puede investigar por su lado [2].

Beneficios de la pureza

Entre las razones para justificar el tener funciones puras, destacan las siguientes:

Transparencia. Una de las consecuencias de utilizar funciones puras es que nos dan transparencia referencial. Un pedazo de código tiene transparencia referencial cuando puede ser sustituido por el resultado de su evaluación sin alterar el comportamiento del programa. El beneficio de la transparencia es que hace el código sea más fácil de comprender y razonar.

Cacheabilidad. Cuando ejecutamos una función por primera vez con cierto argumento, podemos guardar su resultado en caché y la próxima vez que se invoque la misma función con el mismo argumento, ya no es necesario ejecutarla sino que podemos regresar el valor almacenado en caché.

Portabilidad. Las funciones puras son completamente autosuficientes; disponen internamente de todo lo que necesitan para operar. ¿Cómo nos beneficia esto? Por lo pronto, sus dependencias están explícitas y por lo tanto es más sencilla de entender. En el caso de javascript, la portabilidad permite serializar y enviar funciones por medio de un socket.

Testeabilidad. La utilización de funciones puras facilita el testing significativamente ya que no necesitamos simular gateways o estados que requiere nuestro código para operar. Simplemente invocamos la función y evaluamos su resultado.

Paralelismo. Dado que las funciones puras no requieren acceder memoria compartida, se pueden procesar de forma paralela, lo cual abre la posibilidad de tener un mejor desempeño.

Conclusión

Hemos visto en qué consisten las funciones puras y cuáles son sus beneficios. Te recomiendo que al escribir código estés consciente de esto y busques escribir funciones puras.

En la práctica, escribir programas solamente con funciones puras es una tarea laboriosa y que tiene sus propias complicaciones. Por ejemplo, dado que no podemos depender de estado foráneo a nuestras funciones necesitamos estar malabareando datos para pasarlos como argumentos de un lado a otro. Sin embargo, esto es viable aplicando técnicas y herramientas adecuadas. Una de estas técnicas es lo que se conoce como “currying”. En próximos artículos platicaremos al respecto.

Referencias

  1. B. Lonsdorf. “Professor Frisby’s Mostly Adequate Guide to Functional Programming”. http://swgu.ru/r6
  2. B. Moseley, P. Marks. “Out of the Tar Pit”. Software Practice Advancement (SPA), 2006. http://swgu.ru/r7