Un programa escrito en un estilo procedural es una sucesión de pasos que modifican el estado del programa y de la máquina en la que se ejecuta. Estos pasos se agrupan en subrutinas que recogen una colección de instrucciones que tiene sentido ejecutar juntas y les dan un nombre y una estructura. Por lo general, son estas subrutinas las que abstraen la complejidad de las operaciones. Si miramos su función
o equivalente, el punto más abajo en la call stack definible por el usuario, nos encontramos con una sucesión de llamadas a funciones que hacen el trabajo de verdad. Estas funciones suelen por lo general mutar estado en variables privadas de clases o en variables globales que no es accesible al usuario. Por lo tanto, podemos decir que la complejidad de un programa procedural está en lo alto de la call stack, en las funciones finales que hacen la interacción con objetos o con el entorno. La abstracción funciona mediante la encapsulación, que se encarga de que el estado no sea visible ni modificable para el usuario excepto mediante las operaciones permitidas.
En cambio, las funciones en lo alto de la call stack de un programa funcional suelen ser más bien simples. Es lo que sucede cuando una función no puede hacer otra cosa que transformar sus parámetros en resultados, sin ningún tipo de efecto secundario. Es común para un programa funcional que las funciones de los estratos más bajos de la call stack sean las que tienen acceso a una parte mayor del estado del programa, y que cuanto más arriba una mayor parte de ese estado sea inaccesible ya que estas funciones sólo operan en unas pocas variables de todas las que forman el estado del programa. Es más, la verdadera complejidad sucede debajo del código que el programador puede escribir. En Haskell, un programa es una función que devuelve una sucesión de comandos o instrucciones a ejecutar por el entorno. El código que escribe el usuario es simplemente una función pura que calcula la secuencia de instrucciones por hacer, y la verdadera magia sucede cuando esta función termina y Haskell se pone a ejecutar estas instrucciones. El entorno de Haskell es enormemente complejo, pero el programa nunca ve esta complejidad. Ni siquiera tiene acceso a ella. Por lo que al programa respecta, tan sólo está evaluando una función pura, y lo que pase después no es de su incumbencia. Similarmente en Elm tenemos una interfaz parecida. El programa del usuario está compuesto por unas pocas funciones puras que transforman el estado del programa o generan la interfaz de usuario a partir de él. Por debajo, la biblioteca de Elm recibe los comandos generados por estas funciones y lleva a cabo toda la comunicación con el navegador y la red, y la gestión de la interacción con el usuario.
Este fenómeno no sucede únicamente a nivel de lenguaje, ni en una sola capa. Si observamos la biblioteca Elm-UI por ejemplo, vemos que la mayor parte del código escrito por el usuario son funciones que generan o transforman elementos de UI, que al fin y al cabo son unos tipos de datos muy simples. En la base de todo, la función
coge el árbol de elementos generado por el usuario y lo transforma todo en un documento de HTML para ser entregado al navegador. Esta función puede encapsular toda la complejidad de la biblioteca y permitir que el resto de funciones por encima sean simples transformaciones de datos.
De esto podemos aprender que el diseño de buenas bibliotecas en lenguajes procedurales y funcionales es muy distinto. Una buena biblioteca procedural abstrae la complejidad recogiéndola en subrutinas que mutan el estado del programa y está diseñada para que el código del usuario consista en llamadas a la función que llevan a cabo las mutaciones y efectos que el usuario quiere hacer. De la misma forma, si la biblioteca necesita interactuar de vuelta con código del usuario, pedirá funciones a llamar para hacer esa interacción. Este patrón es lo que se conoce como dependency injection y es un pilar importante de la orientación a objetos. Al contrario, una buena biblioteca funcional quiere mover toda la complejidad posible no encima sino debajo del código del usuario, y desarrollar una estructura de datos que permita hacer de puente en la comunicación entre el código del usuario y la biblioteca. De esta manera, el programa, en lugar de consistir en llamadas a subrutinas que causan efectos, se compone de funciones que generan esta estructura. La biblioteca luego ofrece funciones que consumen esta estructura de datos y generan como resultado los datos o efectos deseados.
Una de las cosas más complicadas de entender al empezar a programar en lenguajes funcionales es cómo escribir buenas bibliotecas no triviales. Esto se debe en parte a que las preconcepciones que traemos sobre cómo escribir bibliotecas que nos han funcionado muy bien hasta ahora no necesariamente encajan con un estilo funcional. Entender esta inversión de dónde va la complejidad a la hora de diseñar una biblioteca puede ser un primer paso a la hora de desaprender los patrones procedurales y empezar a entender los funcionales.