Tipos de función

Leyendo la documentación de paquetes como elm/core y elm/html te habrás fijado en que aparecen funciones con muchas flechas. Por ejemplo:

String.repeat : Int -> String -> String
String.join : String -> List String -> String

¿Por qué las flechas? ¿Cuál es la idea?

Paréntesis escondidos

Se vuelve más claro si visualizamos los paréntesis. Por ejemplo, también es válido escribir el tipo de String.repeat así:

String.repeat : Int -> (String -> String)

Es una función que recibe un valor Int y produce otra función. Veámoslo en la práctica:

[ { "input": "String.repeat", "value": "\u001b[36m<function>\u001b[0m", "type_": "Int -> String -> String" }, { "input": "String.repeat 4", "value": "\u001b[36m<function>\u001b[0m", "type_": "String -> String" }, { "input": "String.repeat 4 \"ha\"", "value": "\u001b[93m\"hahahaha\"\u001b[0m", "type_": "String" }, { "input": "String.join", "value": "\u001b[36m<function>\u001b[0m", "type_": "String -> List String -> String" }, { "input": "String.join \"|\"", "value": "\u001b[36m<function>\u001b[0m", "type_": "List String -> String" }, { "input": "String.join \"|\" [\"red\", \"yellow\", \"green\"]", "value": "\u001b[93m\"red|yellow|green\"\u001b[0m", "type_": "String" } ]

Es decir que, conceptualmente, todas las funciones aceptan un sólo argumento. Puede retornar otra función que también acepta un argumento, y así sucesivamente. Eventualmente dejará de retornar funciones.

Podríamos siempre escribir los paréntesis para indicar que es esto lo que ocurre, pero empieza a volverse bastante redundante si tenemos muchos argumentos. Es la misma lógica que cuando escribimos 4 * 2 + 5 * 3 en vez de (4 * 2) + (5 * 3). Implica que hay algo más que aprender de antemano, pero es tan común que vale la pena.

Bien hasta aquí, pero ¿y esta funcionalidad de qué nos sirve? ¿Por qué no hacer que sea (Int, String) -> String, y pasar todos los argumentos simultáneamente?

Aplicación parcial

List.map es una función de uso muy frecuentemente en programas Elm:

List.map : (a -> b) -> List a -> List b

Recibe dos argumentos: una función y una lista. Después usa esa función para transformar todos los ítems en la lista. Unos ejemplos:

  • List.map String.reverse ["part", "are"] == ["trap", "era"]
  • List.map String.length ["part", "are"] == [4, 3]

¿Recuerdas que la expresión String.repeat 4 recibe el tipo String -> String al ejecutarse por sí sola? Bueno, eso significa que podemos hacer esto:

  • List.map (String.repeat 2) ["ha","choo"] == ["haha","choochoo"]

La expresión (String.repeat 2) es una función String -> String, o sea que podemos usarla directamente. Ni siquiera necesitamos escribir (\str -> String.repeat 2 str).

Elm también usa la convención de que los datos siempre vienen al final a través de todo su ecosistema. Esto significa que las funciones están diseñadas para hacer que esta técnica sea posible, y efectivamente es una manera muy común de escribir código Elm.

Es importante recordar que es fácil caer en su sobreutilización. La aplicación parcial es a menudo conveniente y muy legible, pero yo encuentro que es mejor usarla en moderación. Por eso, recomiendo separar el código en funciones auxiliares apenas se pongan un poquito complicadas las cosas. Así, le podemos poner un nombre explicativo, los argumentos también llevan nombre, y además queda fácil de testear. En nuestro ejemplo, eso significaría crear esto:

-- List.map reduplicate ["ha","choo"]


reduplicate : String -> String
reduplicate string =
    String.repeat 2 string

Este es un caso muy simple, pero (1) queda más claro que el foco es el fenómeno lingüístico de la reduplicación, y (2) sería bastante fácil añadir nueva lógica a reduplicate a medida que evolucione nuestro programa. Tal vez querramos llegar a soportar reduplicación “shm” también, por decir algo.

En otras palabras, si nuestro uso de aplicación parcial se hace muy largo, creemos una función auxiliar. Y si ocupa múltiples líneas, definitivamente debiera ser convertida en una función auxiliar. Mi consejo aplica (ejem) para las funciones anónimas también.

Nota: Si terminamos creando “demasiadas” funciones después de seguir este consejo, recomiendo la antigua técnica de usar comentarios del tipo -- REDUPLICACIÓN justo delante de las cinco o diez funciones que corresponden a este grupo. Ya lo he demostrado con mis comentarios -- UPDATE y -- VIEW en ejemplos anteriores, pero es una técnica general que uso a través de todo mi código. Y si te preocupa que tus archivos se vuelvan muy largos, te recomiendo ver mi charla “The life of a file” (en inglés).

Tuberías

Elm también tiene un operador “tubo” |> (también coloquialmente llamado “pizza”) cuyo funcionamiento requiere el uso de aplicación parcial. Por ejemplo, si tenemos una función sanitize que convierte un texto ingresado por el usuario en un número entero:

-- BEFORE


sanitize : String -> Maybe Int
sanitize input =
    String.toInt (String.trim input)

Podemos reescribirla para que quede así:

-- AFTER


sanitize : String -> Maybe Int
sanitize input =
    input
        |> String.trim
        |> String.toInt

A través de esta “tubería” (¿pizzería?) pasamos un argumento input, primero por String.trim, y su salida se convierte en el argumento pasado por String.toInt.

Esto es interesante porque nos permite hacer que el código se lea de izquierda a derecha, lo que mucha gente encuentra cómodo, pero las tuberías pueden sobreutilizarse. Si llegamos a tener tres o cuatro pasos, el código puede quedar más claro si lo separamos en una función auxiliar, ya que la transformación adquiere un nombre, los argumentos también, y además le podemos escribir su tipo. Queda autodocumentada, así que seguro que nuestros colegas, y nosotros mismos en el futuro, sabremos apreciar la claridad que aporta. Testear esta lógica también se hace más fácil.

Nota: Yo, personalmente, prefiero cómo queda en el “antes” del ejemplo, pero tal vez es porque aprendí programación funcional en lenguajes que no permiten tuberías.

results matching ""

    No results matching ""