Todo es una función: definir, llamar y vivir sin métodos
En Elm no hay clases, no hay objetos, no hay métodos y no existe la palabra this. Esta lección parte de esas ausencias para reconstruir el vocabulario mínimo del lenguaje: una definición es un nombre ligado a una expresión, y no existe diferencia de categoría entre una constante y una función porque una función es simplemente un valor más. Se estudia la sintaxis de declaración sin palabra clave y sin return, el papel de let como definición local, la aplicación por yuxtaposición y el hecho decisivo de que los paréntesis agrupan pero nunca llaman, con el corolario práctico de que escribir una llamada al estilo de otros lenguajes produce una tupla y un error desconcertante. Después se examina por qué un nombre cualificado como String.toUpper se parece a un método sin serlo, qué significa que en Elm no exista despacho dinámico y cómo el receptor implícito de la orientación a objetos se convierte aquí en un argumento explícito y ordinario. La conclusión es que eliminar el receptor no es una pérdida de expresividad sino la condición que hace posibles la inferencia total, la composición sin adaptadores y la eliminación agresiva de código muerto.
Quien llega a Elm desde cualquier lenguaje de la familia dominante trae consigo un inventario de herramientas que aquí no existe: no hay clases, no hay interfaces, no hay objetos con comportamiento, no hay métodos, no hay herencia y no existe la palabra this. La reacción inicial suele ser leer esa lista como una carencia, como si Elm fuera un lenguaje al que todavía no le han añadido lo importante. La lectura correcta es la inversa. Elm no es un lenguaje incompleto sino un lenguaje al que se le ha quitado todo lo que no resultaba necesario, y lo que queda cuando terminas de quitar es asombrosamente poco: hay valores, y hay funciones que transforman valores en otros valores. Nada más. Ni siquiera son dos categorías distintas, porque una función es también un valor y puede guardarse en un record, pasarse como argumento o devolverse como resultado igual que un número. Este nivel entero desarrolla las consecuencias de esa reducción, y esta primera lección se ocupa del punto de partida: cómo se define una función, cómo se la llama, y por qué el receptor implícito que otros lenguajes esconden detrás de un punto aquí se escribe a la vista de todos como un argumento cualquiera.
- Declarar funciones sin palabra clave y sin
return, entendiendo que el cuerpo es una expresión y no una secuencia de instrucciones. - Aplicar funciones por yuxtaposición y reconocer que los paréntesis agrupan expresiones pero nunca representan una llamada.
- Explicar por qué
String.toUpperno es un método pese a su apariencia, y qué implica que en Elm no exista despacho dinámico. - Reescribir en estilo funcional un fragmento orientado a objetos convirtiendo el receptor implícito en un argumento explícito.
Definir: un nombre, unos argumentos y una expresión
Una definición en Elm no tiene ceremonia. No hay palabra clave que anuncie que viene una función, no hay llaves que delimiten el cuerpo, no hay return que marque la salida y no hay punto y coma que cierre nada. Hay un nombre, cero o más argumentos, un signo igual y una expresión. Esa expresión es el valor de la función, porque en Elm no existe la noción de ejecutar pasos: existe la de reducir una expresión hasta su valor.
-- Sin palabra clave y sin return: el cuerpo ES el valor devuelto
doble : Int -> Int
doble n =
n * 2
-- Una definicion sin argumentos no es una funcion de cero argumentos:
-- es sencillamente un valor, y se calcula una sola vez
limite : Int
limite =
18
-- let introduce definiciones locales, con las mismas reglas
precioFinal : Float -> Float -> Float
precioFinal base descuento =
let
rebaja =
base * descuento
neto =
base - rebaja
in
neto * 1.21
Conviene detenerse en limite, porque marca una frontera que otros lenguajes desdibujan. En Elm no existen las funciones de cero argumentos: una definición sin parámetros es un valor y se evalúa una única vez, no cada vez que se menciona. Eso significa que no puedes usar la ausencia de argumentos para retrasar un cálculo ni para provocar un efecto en cada consulta, y esa imposibilidad es coherente con la pureza del lenguaje. Si algo no depende de una entrada, su valor no puede cambiar, y por tanto calcularlo dos veces sería malgastar trabajo para obtener lo mismo.
Llamar: yuxtaposición, y el paréntesis que no llama
Aplicar una función consiste en escribirla seguida de sus argumentos separados por espacios. No hay paréntesis de llamada ni comas separadoras, y esa elección sintáctica, que al principio incomoda, es exactamente lo que permite que la aplicación parcial de la próxima lección resulte invisible y natural. Los paréntesis siguen existiendo, pero cumplen un único papel: agrupar una expresión para que se evalúe antes.
-- Yuxtaposicion: la funcion, un espacio, y sus argumentos
doble 21 -- 42
max 3 7 -- 7
String.repeat 3 "ab" -- "ababab"
-- Los parentesis agrupan; jamas llaman
sqrt 16 + 9 -- 13 : primero sqrt 16, despues la suma
sqrt (16 + 9) -- 5 : primero la suma, despues sqrt
-- La aplicacion liga mas fuerte que cualquier operador del lenguaje
List.length lista + 1 -- equivale a (List.length lista) + 1
La regla de precedencia que gobierna todo esto es única y no admite excepciones: la aplicación de funciones liga más fuerte que cualquier operador. Por eso sqrt 16 + 9 no es lo que un ojo entrenado en C esperaría, y por eso los paréntesis que aparecen en el código de Elm casi siempre están ahí para envolver un argumento compuesto, no para marcar una invocación.
Si escribes doble(21), el código compila y funciona, pero no porque los paréntesis llamen a nada: estás aplicando doble a la expresión agrupada 21, que es simplemente 21. La trampa aparece con dos argumentos. sumar(2, 3) no pasa dos argumentos, pasa uno solo: la tupla (2, 3). El compilador te dirá entonces que esperaba un Int y encontró una tupla, un mensaje impecable que resulta incomprensible hasta que entiendes que la coma dentro de paréntesis construye tuplas y que la llamada nunca necesitó paréntesis. Es el error de sintaxis más frecuente de la primera semana y desaparece para siempre en cuanto se interioriza que el paréntesis agrupa.
Ni métodos ni this: el receptor se vuelve argumento
Un nombre como String.toUpper engaña a la vista. Tiene un punto en medio y por tanto parece un método invocado sobre algo, pero ese punto no separa un receptor de una operación: separa un módulo de una función que vive dentro de él. String no es un objeto ni una clase, es un espacio de nombres, y toUpper es una función normal y corriente que recibe un String como cualquier otro argumento. La diferencia no es de estilo: en una llamada a método el código que se ejecuta depende del tipo del receptor en tiempo de ejecución, mientras que aquí el compilador sabe exactamente qué función se está usando en cada punto del programa.
-- El estilo con receptor implicito, tal como se escribe en otros lenguajes:
-- texto.toUpperCase().trim()
-- En Elm el verbo va delante y el dato es un argumento mas
String.trim (String.toUpper texto)
-- Las funciones son valores: se guardan, se pasan y se devuelven
incrementar : Int -> Int
incrementar =
\n -> n + 1
-- Los operadores tambien son funciones cuando se les ponen parentesis
List.foldl (+) 0 [ 1, 2, 3 ] -- 6
-- Y los constructores de un custom type son funciones sin mas
type Estado
= Cargando
| Listo String
marcarListos : List String -> List Estado
marcarListos =
List.map Listo
La consecuencia práctica de no tener this es que desaparece una categoría entera de errores. En los lenguajes que lo tienen, this es un argumento oculto cuyo valor depende de cómo se haya invocado la función, no de cómo se haya escrito, y de ahí nacen los métodos que pierden su enlace al pasarse como callback, las funciones flecha usadas como parche y las capas de artefactos para volver a atar lo que la sintaxis desató. En Elm no hay nada que atar porque no hay nada implícito: el dato entra por la puerta principal, con nombre y tipo, y la función no tiene acceso a ningún contexto que no le hayas dado.
Una sola categoría
Valores y funciones no son cosas distintas. Una función se guarda en un record, viaja como argumento y vuelve como resultado igual que un Int.
El espacio es la llamada
Aplicar es yuxtaponer. Los paréntesis solo agrupan, y la aplicación liga más fuerte que cualquier operador.
El punto es un módulo
En List.map, List es un espacio de nombres, no un receptor. No hay despacho dinámico en ningún punto del lenguaje.
Sin contexto oculto
Sin this, sin campos privados accesibles por dentro, sin estado ambiental. Todo lo que la función usa está en su firma.
flowchart LR A[Dato] --> B[Funcion del modulo] B --> C[Dato nuevo] D[Objeto] --> E[Metodo con receptor implicito] E --> F[Estado mutado dentro del objeto] style B fill:#a6e3a1,color:#11111b style C fill:#89b4fa,color:#11111b style E fill:#f38ba8,color:#11111b
Para convertir cualquier método en una función de Elm basta con aplicar una regla: el receptor se convierte en el último parámetro. Un método que en otro lenguaje se escribiría sobre un carrito para añadirle un artículo pasa a ser una función que recibe el artículo y el carrito, en ese orden, y devuelve un carrito nuevo. El orden importa y no es arbitrario: dejar el dato principal al final es lo que permitirá encadenar la función en una tubería. Esa convención recorre entera la biblioteca estándar y es la razón de que List.map, String.replace y Dict.insert coloquen su dato al final.
Vale la pena entender qué se está negociando cuando un lenguaje decide que no habrá métodos, porque la decisión parece estética y en realidad es la viga maestra sobre la que descansan casi todas las garantías de Elm. Un método es, en su núcleo, un mecanismo de despacho: un mismo nombre designa implementaciones distintas y la elección se resuelve mirando el valor que hay a la izquierda del punto. Esa indirección es potentísima y tiene un coste que se paga en todas partes a la vez. El compilador deja de saber qué código se ejecuta en cada llamada, y por tanto no puede inferir tipos sin anotaciones ni descartar con seguridad el código que nadie usa; el receptor se convierte en un argumento invisible cuyo enlace depende de la forma de la invocación, y con él llegan los métodos desatados y los adaptadores para volver a atarlos; la organización del programa queda fijada por tipo en lugar de por operación, de modo que añadir una operación nueva obliga a tocar todas las clases, que es la mitad incómoda del célebre problema de la expresión. Elm renuncia a todo eso y a cambio obtiene un lenguaje donde cada aplicación de función es un sitio estáticamente determinado. De ahí se sigue la inferencia total sin anotaciones obligatorias, porque no hay ambigüedad que resolver. De ahí se sigue que el compilador pueda recorrer el programa entero y descartar cualquier función que no sea alcanzable, que es la razón técnica de que los artefactos de Elm salgan tan pequeños comparados con los de un ecosistema donde el despacho dinámico obliga a conservar por si acaso. De ahí se sigue que la composición sea una operación total, porque no hay que casar receptores con parámetros ni preguntarse a quién pertenece cada operación. E incluso se sigue una propiedad más sutil y más valiosa a largo plazo: como la función no puede consultar nada que no esté en su firma, leer la firma es leer el contrato completo, sin cláusulas ocultas en el estado del objeto ni en la cadena de prototipos. Cuando alguien dice que Elm es un lenguaje pequeño no está describiendo su gramática sino esta propiedad: hay un solo mecanismo, la aplicación de una función a un argumento, y todo lo demás es ese mecanismo repetido. La potencia no vino de añadir el objeto, vino de atreverse a quitarlo.
- Define tres funciones de una, dos y tres entradas sin escribir la firma, y después añádela comprobando que coincide con lo que el compilador ya había inferido.
- Escribe
sqrt 16 + 9ysqrt (16 + 9), predice ambos resultados antes de ejecutarlos y explica con la regla de precedencia por qué difieren. - Provoca a propósito el error de escribir una llamada con paréntesis y coma, lee el mensaje del compilador y señala en qué punto menciona la tupla.
- Toma una clase pequeña de otro lenguaje con tres métodos y tradúcela a tres funciones de Elm dejando el receptor como último parámetro.
- Guarda una función dentro de un record, recupérala y aplícala; razona por qué eso no convierte al record en un objeto.
- Usa
.nombrecomo función suelta dentro deList.mapy explica qué firma tiene ese accesor y por qué es polimórfico.