Composición de funciones: hablar de la receta sin nombrar los ingredientes
Encadenar transformaciones sobre un dato concreto y construir una función nueva a partir de otras son dos operaciones distintas que la sintaxis de Elm mantiene deliberadamente separadas. Esta lección estudia la segunda. Se analizan los dos operadores de composición, el que fluye hacia adelante y el que sigue la tradición matemática de derecha a izquierda, con sus tipos exactos y su asociatividad, y se explica por qué componer produce un valor de tipo función mientras que encadenar produce un valor de tipo dato. A partir de ahí se desarrolla el estilo sin puntos, la práctica de definir una función mencionando solo las operaciones que la forman y omitiendo por completo el argumento sobre el que actúan, junto con su fundamento en la eta-reducción. La segunda mitad de la lección es un ejercicio de honestidad técnica: se examinan los casos donde ese estilo aclara de verdad, los casos donde produce firmas ilegibles y errores de tipo desconcertantes, la interacción problemática con la restricción del valor y las anotaciones, y el criterio práctico para decidir entre componer y encadenar.
Una tubería y una composición se parecen tanto en la página que es fácil creerlas equivalentes, y sin embargo responden a preguntas distintas. Cuando escribes una tubería estás diciendo qué le ocurre a este dato; cuando escribes una composición estás diciendo qué función resulta de aplicar estas otras en orden, sin mencionar dato alguno. La diferencia no es de estilo sino de tipo: una tubería se evalúa a un valor del dominio, una composición se evalúa a una función que todavía espera su entrada. Esa distinción abre la puerta a una forma de escribir programas en la que las definiciones dejan de hablar de los datos y pasan a hablar solo de las operaciones, un estilo que recibe el nombre poco afortunado de sin puntos y que produce, en sus buenos días, definiciones de una limpieza notable: una función queda definida como la sucesión de las transformaciones que la componen, igual que se describe un proceso industrial nombrando sus máquinas y no las piezas que pasan por ellas. En sus malos días, ese mismo estilo produce expresiones que solo su autor entiende, mensajes de error que señalan sitios lejanos y una sensación persistente de estar resolviendo un acertijo en vez de escribiendo un programa. Esta lección trata de ambas caras con igual seriedad, porque la habilidad valiosa no es componer sino saber cuándo hacerlo.
- Distinguir con precisión entre encadenar un dato y componer funciones, atendiendo al tipo que produce cada operación.
- Usar los dos operadores de composición conociendo su dirección, su tipo y su asociatividad.
- Escribir definiciones en estilo sin puntos mediante eta-reducción y revertirlas cuando la legibilidad lo pida.
- Reconocer los casos donde la composición ofusca, incluidas las anotaciones obligatorias y los mensajes de error desplazados.
Componer no es encadenar
Los dos operadores de composición toman dos funciones y devuelven una función nueva. El que fluye hacia adelante se lee en el mismo orden que la tubería; el que fluye hacia atrás respeta la convención matemática de siglos y se lee de derecha a izquierda. Sus tipos lo dicen todo.
-- Composicion hacia adelante y hacia atras
(>>) : (a -> b) -> (b -> c) -> (a -> c)
(<<) : (b -> c) -> (a -> b) -> (a -> c)
-- Encadenar produce un dato; componer produce una funcion
"elm" |> String.toUpper |> String.reverse -- "MLE" : String
String.toUpper >> String.reverse -- String -> String
-- Y por eso una definicion puede prescindir del argumento
gritarAlReves : String -> String
gritarAlReves =
String.toUpper >> String.reverse
-- La misma funcion, escrita con el argumento a la vista
gritarAlRevesExplicita : String -> String
gritarAlRevesExplicita texto =
texto |> String.toUpper |> String.reverse
Observa que las dos últimas definiciones no solo son equivalentes: la primera se obtiene de la segunda por una transformación mecánica. Si una función se limita a aplicar algo a su único argumento y devolver el resultado, ese argumento puede desaparecer de ambos lados de la definición sin que nada cambie. Esa transformación tiene nombre propio, eta-reducción, y es la maquinaria que hay detrás de todo el estilo sin puntos. El nombre engaña un poco, porque lo que se omite no son puntos sino los argumentos, llamados puntos en la tradición matemática que dio origen a la expresión.
El estilo sin puntos y dónde brilla
La composición muestra su mejor cara en un sitio muy concreto: cuando lo que necesitas es una función para dársela a otra función. En ese contexto, componer evita el rodeo de escribir una función anónima que no hace más que aplicar dos cosas seguidas.
-- Con funcion anonima: el argumento aparece tres veces y no aporta nada
List.map (\nombre -> String.toUpper (String.trim nombre)) nombres
-- Compuesta: se nombra la receta, no el ingrediente
List.map (String.trim >> String.toUpper) nombres
-- Muy comun al construir predicados y comparadores
esVacio : String -> Bool
esVacio =
String.trim >> String.isEmpty
-- Y al adaptar accesores dentro de una ordenacion
List.sortBy (.usuario >> .apellido) comentarios
El patrón que emerge es reconocible: la composición aclara cuando los eslabones son funciones ya nombradas, cuando son pocas, y cuando el resultado va a usarse como argumento de una función de orden superior. En esos tres casos la versión compuesta dice exactamente lo mismo con menos ruido, y el nombre que se ahorra era un nombre inventado sin significado, del tipo x o elemento, que no informaba de nada. Ahorrarse un nombre vacío es una mejora real; ahorrarse un nombre del dominio no lo es.
Existe un criterio mecánico para decidir si eta-reducir. Mira si el argumento aparece exactamente una vez y en la última posición del cuerpo. Si es así, la reducción es segura y casi siempre mejora la lectura. Si aparece dos veces, o en medio de la expresión, o dentro de una rama de un case, no hay eta-reducción posible sin acrobacias, y las acrobacias son precisamente lo que hace que la composición tenga mala prensa. La versión con el argumento a la vista no es una versión inferior a la que haya que aspirar a mejorar: es la versión correcta siempre que la otra no sea evidentemente más clara.
Componer y encadenar en la misma expresión
Los dos operadores no compiten: cada uno resuelve un nivel distinto y lo habitual en código real es verlos convivir en la misma función. La tubería gobierna el recorrido del dato principal, y la composición aparece dentro de un eslabón para construir sobre la marcha la función que ese eslabón necesita.
-- La tuberia lleva el dato; la composicion fabrica un eslabon
resumen : List Comentario -> String
resumen comentarios =
comentarios
|> List.filter (.autor >> .verificado)
|> List.map (.texto >> String.trim >> String.left 80)
|> String.join " | "
-- La equivalencia que conviene tener presente
lista |> List.map (f >> g)
lista |> List.map f |> List.map g
Las dos últimas líneas expresan una identidad que no es una curiosidad sino una herramienta de refactorización con respaldo formal: componer dentro de un solo recorrido y hacer dos recorridos encadenados producen exactamente el mismo resultado. La primera forma recorre la lista una vez y la segunda dos, de modo que hay una diferencia de rendimiento cuando la lista es grande, pero no la hay de significado. Poder afirmar eso con certeza, y no como una intuición que habría que comprobar caso por caso, es una consecuencia directa de la pureza: si las funciones no pueden observar ni alterar nada fuera de sus argumentos, reagrupar el trabajo no puede cambiar el resultado.
Cuándo la composición ofusca
Toca ahora la otra mitad, que suele omitirse en los textos entusiastas y que en la práctica importa más. La composición tiene tres costes concretos, y ninguno es una cuestión de gusto.
-- 1. Cadenas largas: la firma desaparece de la vista
procesar =
String.trim >> String.toLower >> String.words >> List.sort >> String.join "-"
-- 2. El error de tipo aparece lejos del punto donde esta el fallo:
-- el compilador senala la composicion entera, no el eslabon culpable
-- 3. Sin anotacion, una definicion sin argumentos puede quedar
-- demasiado generalizada y obligar a escribir la firma a mano
El primer coste es de legibilidad: en una composición larga desaparece el nombre del dato y con él la pista de qué está circulando, de modo que hay que reconstruir el tipo intermedio de cabeza en cada eslabón. El segundo es de diagnóstico: cuando un eslabón no encaja, el compilador informa sobre la expresión compuesta completa, y aunque los mensajes de Elm son extraordinariamente buenos, el sitio que señalan no es donde está el error sino donde se manifiesta. El tercero es más técnico y sorprende al llegar: una definición sin argumentos a la izquierda del igual es un valor, y el algoritmo de inferencia trata los valores de forma más conservadora que las funciones, de manera que ciertas composiciones perfectamente correctas exigen una anotación explícita para compilar. Ninguno de los tres es motivo para renunciar a componer; los tres son motivo para hacerlo con moderación.
Componer devuelve función
f >> g es un valor de tipo función que aún espera su entrada. Encadenar con tubería devuelve ya el dato transformado.
Eta-reducción
Si el argumento aparece una sola vez y al final, puede desaparecer de los dos lados de la definición sin cambiar nada.
Su mejor uso
Como argumento de una función de orden superior, para no escribir una función anónima que solo aplica dos cosas seguidas.
Su peor uso
Cadenas largas sin nombres intermedios: se pierde el tipo que circula y el error de tipo señala la expresión entera.
flowchart LR subgraph Encadenar D1[dato] --> P1[paso uno] --> P2[paso dos] --> R1[dato nuevo] end subgraph Componer F1[funcion uno] --> C[funcion compuesta] F2[funcion dos] --> C C --> R2[espera un dato] end style R1 fill:#a6e3a1,color:#11111b style C fill:#89b4fa,color:#11111b style R2 fill:#f9e2af,color:#11111b
Conviene entender qué se está afirmando cuando alguien defiende el estilo sin puntos, porque la defensa habitual, que es más corto, es la menos interesante de las posibles. La afirmación fuerte es otra: que nombrar un dato que solo existe para ser pasado de una función a la siguiente es introducir en el texto una entidad que no forma parte del problema. Cuando escribes que el nombre limpio es el resultado de recortar y poner en mayúsculas el nombre, has inventado la palabra nombre dos veces para hablar de algo que en realidad es una única cosa: una transformación. La composición permite describir esa transformación sin invocar al fantasma. Vista así, es la misma clase de progreso que supuso pasar de los saltos con etiquetas a las estructuras de control, o de los índices explícitos a las operaciones sobre colecciones que veremos en la próxima lección: en cada caso desaparece una variable auxiliar que existía solo por limitaciones de la notación, y con ella desaparecen los errores que esa variable permitía cometer. La afirmación es sólida y tiene un corolario práctico verificable: el código sin puntos se refactoriza mejor, porque insertar un paso nuevo en una composición es añadir un eslabón y no tocar ninguna variable. Ahora bien, la tesis tiene un precio y la comunidad de Elm ha decidido, de manera bastante consciente, no pagarlo entero. El precio es que el estilo sin puntos escala mal hacia arriba. Funciona espléndidamente con dos o tres eslabones nombrados y se deteriora deprisa en cuanto hacen falta funciones que combinen argumentos, reordenen parámetros o apliquen una función a dos ramas distintas del mismo dato. En lenguajes con una tradición más larga en este terreno existe un vocabulario de combinadores para esos casos, y quien lo domina escribe expresiones de una densidad admirable que sus compañeros no pueden leer. Elm, que tiene como principio rector que un recién llegado pueda entender el código de un veterano, no ofrece ese vocabulario y desaconseja de hecho su reinvención. La consecuencia es una posición intermedia y bastante sabia: componer cuando la composición se lee en voz alta como una frase, y volver al argumento explícito en cuanto haga falta pensar para reconstruir qué circula por la cadena. La abstracción que ahorra un nombre vacío es un regalo; la que ahorra un nombre del dominio es una deuda que se paga en la siguiente lectura.
- Escribe la misma función en tres versiones, con argumento explícito, con tubería y compuesta, y anota qué tipo tiene cada expresión antes de compilar.
- Aplica la eta-reducción a una función cuyo argumento aparezca una sola vez al final, y después inténtalo con una donde aparezca dos veces; describe qué te lo impide.
- Escribe la misma composición con los dos operadores, hacia adelante y hacia atrás, y decide cuál prefieres justificando la decisión por el orden de lectura.
- Construye una composición de cinco eslabones sin anotación, provoca un error de tipo en el tercero y observa qué expresión señala el compilador.
- Encuentra un caso en tu código donde una función anónima solo aplique dos funciones seguidas y sustitúyela por una composición.
- Toma una composición larga que hayas escrito, pártela dando nombre de dominio a un tramo intermedio y decide con honestidad cuál de las dos versiones enseñarías a alguien que llega hoy al proyecto.