Translate

Mostrando las entradas con la etiqueta elm. Mostrar todas las entradas
Mostrando las entradas con la etiqueta elm. Mostrar todas las entradas

lunes, 23 de marzo de 2026

Elementos personalizados en Elm




Los navegadores parecen estar admitiendo cada vez más elementos personalizados, lo cual resulta muy útil para integrar JavaScript en programas Elm.

Veamos un ejemplo mínimo de cómo usar elementos personalizados para la localización e internacionalización.

Supongamos que queremos localizar fechas, pero esta funcionalidad aún no está disponible en los paquetes principales de Elm. Quizás quieran escribir una función que localice las fechas:


//

//   localizeDate('sr-RS', 12, 5) === "петак, 1. јун 2012."

//   localizeDate('en-GB', 12, 5) === "Friday, 1 June 2012"

//   localizeDate('en-US', 12, 5) === "Friday, June 1, 2012"

//

function localizeDate(lang, year, month)

{

    const dateTimeFormat = new Intl.DateTimeFormat(lang, {

        weekday: 'long',

        year: 'numeric',

        month: 'long',

        day: 'numeric'

    });


    return dateTimeFormat.format(new Date(year, month));

}


Pero, ¿cómo usamos eso en Elm? Los navegadores más recientes permiten crear nuevos tipos de nodos DOM como este:


//

//   <intl-date lang="sr-RS" year="2012" month="5">

//   <intl-date lang="en-GB" year="2012" month="5">

//   <intl-date lang="en-US" year="2012" month="5">

//

customElements.define('intl-date',

    class extends HTMLElement {

        // things required by Custom Elements

        constructor() { super(); }

        connectedCallback() { this.setTextContent(); }

        attributeChangedCallback() { this.setTextContent(); }

        static get observedAttributes() { return ['lang','year','month']; }


        // Our function to set the textContent based on attributes.

        setTextContent()

        {

            const lang = this.getAttribute('lang');

            const year = this.getAttribute('year');

            const month = this.getAttribute('month');

            this.textContent = localizeDate(lang, year, month);

        }

    }

);


Las partes más importantes aquí son attributeChangedCallback y observedAttributes. Necesitas una lógica similar para detectar los cambios en los atributos que te interesan.

Cárgala antes de inicializar tu código Elm y podrás escribir código como este:


import Html exposing (Html, node)

import Html.Attributes (attribute)


viewDate : String -> Int -> Int -> Html msg

viewDate lang year month =

  node "intl-date"

    [ attribute "lang" lang

    , attribute "year" (String.fromInt year)

    , attribute "month" (String.fromInt month)

    ]

    []


Ahora puedes llamar a viewDate cuando necesites acceder a ese tipo de información localizada en tu vista.


miércoles, 18 de marzo de 2026

Puertos en Elm

 


Los puertos permiten la comunicación entre Elm y JavaScript.

Los puertos se utilizan con mayor frecuencia para WebSockets y localStorage. Centrémonos en el ejemplo de WebSockets.

Aquí tenemos prácticamente el mismo HTML que hemos usado en las páginas anteriores, pero con un poco de código JavaScript adicional. Creamos una conexión a wss://echo.websocket.org que simplemente repite lo que le enviamos.

<!DOCTYPE HTML>

<html>


<head>

  <meta charset="UTF-8">

  <title>Elm + Websockets</title>

  <script type="text/javascript" src="elm.js"></script>

</head>


<body>

    <div id="myapp"></div>

</body>


<script type="text/javascript">


// Start the Elm application.

var app = Elm.Main.init({

    node: document.getElementById('myapp')

});


// Create your WebSocket.

var socket = new WebSocket('wss://echo.websocket.org');


// When a command goes to the `sendMessage` port, we pass the message

// along to the WebSocket.

app.ports.sendMessage.subscribe(function(message) {

    socket.send(message);

});


// When a message comes into our WebSocket, we pass the message along

// to the `messageReceiver` port.

socket.addEventListener("message", function(event) {

    app.ports.messageReceiver.send(event.data);

});


// If you want to use a JavaScript library to manage your WebSocket

// connection, replace the code in JS with the alternate implementation.

</script>


</html>


Llamamos a Elm.Main.init() como en todos nuestros ejemplos de interoperabilidad, pero esta vez estamos usando el objeto app resultante. Nos suscribimos al puerto sendMessage y enviamos mensajes al puerto messageReceiver.


Observa las líneas que utilizan la palabra clave port en el archivo Elm correspondiente. Así es como definimos los puertos que acabamos de ver en JavaScript.

port module Main exposing (..)


import Browser

import Html exposing (..)

import Html.Attributes exposing (..)

import Html.Events exposing (..)

import Json.Decode as D




-- MAIN



main : Program () Model Msg

main =

  Browser.element

    { init = init

    , view = view

    , update = update

    , subscriptions = subscriptions

    }





-- PORTS



port sendMessage : String -> Cmd msg

port messageReceiver : (String -> msg) -> Sub msg




-- MODEL



type alias Model =

  { draft : String

  , messages : List String

  }



init : () -> ( Model, Cmd Msg )

init flags =

  ( { draft = "", messages = [] }

  , Cmd.none

  )




-- UPDATE



type Msg

  = DraftChanged String

  | Send

  | Recv String



-- Use the `sendMessage` port when someone presses ENTER or clicks

-- the "Send" button. Check out index.html to see the corresponding

-- JS where this is piped into a WebSocket.

--

update : Msg -> Model -> ( Model, Cmd Msg )

update msg model =

  case msg of

    DraftChanged draft ->

      ( { model | draft = draft }

      , Cmd.none

      )


    Send ->

      ( { model | draft = "" }

      , sendMessage model.draft

      )


    Recv message ->

      ( { model | messages = model.messages ++ [message] }

      , Cmd.none

      )




-- SUBSCRIPTIONS



-- Subscribe to the `messageReceiver` port to hear about messages coming in

-- from JS. Check out the index.html file to see how this is hooked up to a

-- WebSocket.

--

subscriptions : Model -> Sub Msg

subscriptions _ =

  messageReceiver Recv




-- VIEW



view : Model -> Html Msg

view model =

  div []

    [ h1 [] [ text "Echo Chat" ]

    , ul []

        (List.map (\msg -> li [] [ text msg ]) model.messages)

    , input

        [ type_ "text"

        , placeholder "Draft"

        , onInput DraftChanged

        , on "keydown" (ifIsEnter Send)

        , value model.draft

        ]

        []

    , button [ onClick Send ] [ text "Send" ]

    ]




-- DETECT ENTER



ifIsEnter : msg -> D.Decoder msg

ifIsEnter msg =

  D.field "key" D.string

    |> D.andThen (\key -> if key == "Enter" then D.succeed msg else D.fail "some other key"


Fíjate que la primera línea dice "port module" en lugar de simplemente "module". Esto permite definir puertos dentro de un módulo. El compilador ofrece una pista si es necesario, ¡así que esperemos que nadie se quede atascado en esto!


Bien, ¿pero qué ocurre con las declaraciones de puerto para sendMessage y messageReceiver?

La declaración sendMessage nos permite enviar mensajes desde Elm.

port sendMessage: String -> Cmd msg

Aquí declaramos que queremos enviar valores de tipo String, pero podríamos enviar cualquier tipo que funcione con indicadores. 

A partir de ahí, podemos usar sendMessage como cualquier otra función. Si tu función de actualización genera un comando sendMessage "hello", lo recibirás en JavaScript:


app.ports.sendMessage.subscribe(function(message) {

socket.send(message);

});


Este código JavaScript se suscribe a todos los mensajes salientes. Puedes suscribir varias funciones y cancelar suscripciones por referencia, pero generalmente recomendamos mantener la configuración estática.

También recomendamos enviar mensajes más completos, en lugar de crear múltiples puertos individuales. Esto podría implicar tener un tipo personalizado en Elm que represente todo lo que necesites comunicar a JavaScript, y luego usar Json.Encode para enviarlo a una única suscripción de JavaScript. Muchos consideran que esto crea una separación de responsabilidades más clara. El código de Elm gestiona claramente parte del estado, y JavaScript gestiona otra parte.


La declaración messageReceiver nos permite escuchar los mensajes que llegan a Elm.


messageReceiver: (String -> msg) -> Sub msg


Estamos indicando que vamos a recibir valores de tipo String, pero también podemos escuchar cualquier tipo que pueda llegar a través de indicadores o puertos de salida. Simplemente reemplazamos el tipo String por uno de los tipos que pueden cruzar el límite.


Podemos usar messageReceiver como cualquier otra función. En nuestro caso, llamamos a messageReceiverRecv al definir nuestras suscripciones porque queremos recibir mensajes entrantes de JavaScript. Esto nos permitirá recibir mensajes como Recv "¿cómo estás?"` en nuestra función de actualización.


En JavaScript, podemos enviar datos a este puerto cuando queramos:


socket.addEventListener("message", function(event) {

    app.ports.messageReceiver.send(event.data);

});


En este caso, enviamos mensajes cada vez que el websocket recibe uno, pero también se pueden enviar en otros momentos. Quizás también estemos recibiendo mensajes de otra fuente de datos. Está bien, ¡y Elm no necesita saber nada al respecto! Simplemente envía las cadenas a través del puerto correspondiente.


Los puertos sirven para establecer límites claros. Definitivamente, no intentes crear un puerto para cada función de JavaScript que necesites. Puede que te guste mucho Elm y quieras hacerlo todo con él, cueste lo que cueste, pero los puertos no están diseñados para eso. En su lugar, concéntrate en preguntas como "¿quién controla el estado?" y usa uno o dos puertos para enviar y recibir mensajes. Si te encuentras en un escenario complejo, incluso puedes simular valores de Msg enviando JavaScript como `{ tag: "active-users-changed", list: ... }`, donde tienes una etiqueta para cada variante de información que podrías enviar.


Aquí tienes algunas pautas sencillas y errores comunes:


Se recomienda enviar `Json.Encode.Value` a través de puertos. Al igual que con las banderas, ciertos tipos básicos también pueden pasar a través de puertos. Esto es de la época anterior a los decodificadores JSON, y puedes leer más al respecto aquí.


Todas las declaraciones de puertos deben aparecer en un módulo de puerto. Probablemente sea mejor organizar todos los puertos en un solo módulo para que la interfaz sea más fácil de visualizar en un solo lugar.


Los puertos son para aplicaciones. Un módulo de puerto está disponible en las aplicaciones, pero no en los paquetes. Esto garantiza que los desarrolladores de aplicaciones tengan la flexibilidad que necesitan, pero el ecosistema de paquetes está completamente escrito en Elm. Creemos que esto creará un ecosistema y una comunidad más sólidos a largo plazo, y analizaremos las ventajas y desventajas en profundidad en la siguiente sección sobre los límites de la interoperabilidad entre Elm y JavaScript.


Los puertos pueden eliminarse como código muerto. Elm tiene un sistema de eliminación de código muerto bastante agresivo y eliminará los puertos que no se utilicen dentro del código Elm. El compilador desconoce lo que sucede en JavaScript, así que intente conectar las cosas en Elm antes que en JavaScript.


miércoles, 11 de marzo de 2026

Elm y su Interoperabilidad con JavaScript: Flags



Las Flags o banderas permiten pasar valores a Elm durante la inicialización.

Usos comunes incluyen claves de API, variables de entorno y datos de usuario. Esto puede ser útil si se genera el HTML dinámicamente. También nos ayudan a cargar información almacenada en caché en este ejemplo de almacenamiento local.

El HTML es básicamente el mismo que antes, pero con un argumento adicional de banderas para la función Elm.Main.init().


<html>

<head>

  <meta charset="UTF-8">

  <title>Main</title>

  <script src="main.js"></script>

</head>


<body>

  <div id="myapp"></div>

  <script>

  var app = Elm.Main.init({

    node: document.getElementById('myapp'),

    flags: Date.now()

  });

  </script>

</body>

</html>


En este ejemplo, pasamos la hora actual en milisegundos, pero cualquier valor JS que pueda decodificarse en JSON puede utilizarse como indicador.

Estos datos adicionales se denominan "indicadores" porque son similares a los indicadores de la línea de comandos. Puedes llamar a elm make src/Main.elm, pero puedes añadir indicadores como --optimize y --output=main.js para personalizar su comportamiento. 

Para gestionar las banderas en Elm, necesitas modificar ligeramente tu función init:


module Main exposing (..)


import Browser

import Html exposing (Html, text)



-- MAIN


main : Program Int Model Msg

main =

  Browser.element

    { init = init

    , view = view

    , update = update

    , subscriptions = subscriptions

    }



-- MODEL


type alias Model = { currentTime : Int }


init : Int -> ( Model, Cmd Msg )

init currentTime =

  ( { currentTime = currentTime }

  , Cmd.none

  )



-- UPDATE


type Msg = NoOp


update : Msg -> Model -> ( Model, Cmd Msg )

update _ model =

  ( model, Cmd.none )



-- VIEW


view : Model -> Html Msg

view model =

  text (String.fromInt model.currentTime)



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg

subscriptions _ =

  Sub.none


Lo importante aquí es que la función init indica que acepta un argumento Int. Así es como el código de Elm obtiene acceso inmediato a las banderas que pasas desde JavaScript. Desde ahí, puedes añadir elementos a tu modelo o ejecutar comandos. Lo que necesites.

Pero, ¿qué sucede si init indica que acepta una bandera Int, pero alguien intenta inicializar con Elm.Main.init({ flags: "jaja, ¿y ahora qué?" })?

Elm comprueba este tipo de cosas, asegurándose de que las banderas sean exactamente las esperadas. Sin esta comprobación, podrías pasar cualquier cosa, lo que provocaría errores de ejecución en Elm. Hay varios tipos que se pueden usar como indicadores:

Mucha gente siempre usa Json.Decode.Value porque les da un control muy preciso. Pueden escribir un decodificador para gestionar cualquier situación inusual en el código Elm, recuperándose de datos inesperados de forma sencilla.

Los demás tipos compatibles son anteriores a que se descubriera una forma de crear decodificadores JSON. Si decide usarlos, hay algunas sutilezas que debe tener en cuenta. Los siguientes ejemplos muestran el tipo de bandera deseado, y los subpuntos muestran qué sucedería con un par de valores JS diferentes:

  • init : Int -> ...

    • 0 => 0
    • 7 => 7
    • 3.14 => error
    • 6.12 => error
  • init : Maybe Int -> ...

    • null => Nothing
    • 42 => Just 42
    • "hi" => error
  • init : { x : Float, y : Float } -> ...

    • { x: 3, y: 4, z: 50 } => { x = 3, y = 4 }
    • { x: 3, name: "Tom" } => error
    • { x: 360, y: "why?" } => error
  • init : (String, Int) -> ...

    • ["Tom", 42] => ("Tom", 42)
    • ["Sue", 33] => ("Sue", 33)
    • ["Bob", "4"] => error
    • ["Joe", 9, 9] => error

Ten en cuenta que cuando una conversión falla, se genera un error en el lado de JS. Adoptamos la política de "fallo rápido". En lugar de que el error se transmita por el código de Elm, se reporta lo antes posible. Esta es otra razón por la que se prefiere usar Json.Decode.Value para las banderas. En lugar de generar un error en JS, el valor inusual pasa por un decodificador, lo que garantiza la implementación de algún tipo de comportamiento alternativo.

miércoles, 4 de marzo de 2026

Elm y su Interoperabilidad con JavaScript

 


Ya hemos visto la arquitectura de Elm: tipos, comandos, suscripciones e incluso tenemos Elm instalado localmente.

Pero ¿qué sucede cuando necesitas integrarlo con JavaScript? Quizás exista una API de navegador que aún no tenga un paquete Elm equivalente. ¿Quizás quieras integrar un widget JavaScript en tu aplicación Elm? Veamos los tres mecanismos de interoperabilidad de Elm:

  • Banderas
  • Puertos
  • Elementos personalizados

Antes de analizar los tres mecanismos, necesitamos saber cómo compilar programas Elm a JavaScript.

Ejecutar elm make genera archivos HTML por defecto. Entonces, si dices:


elm make src/Main.elm


Se genera un archivo index.html que puedes abrir y empezar a experimentar. Si te estás iniciando en la interoperabilidad de JavaScript, te conviene generar archivos JavaScript:


elm make src/Main.elm --output=main.js


Esto genera un archivo JavaScript que expone una función Elm.Main.init(). Una vez que tengas main.js, puedes escribir tu propio archivo HTML con la función que desees.


Aquí está el HTML mínimo necesario para que tu archivo main.js aparezca en un navegador:


<html>

<head>

    <meta charset="UTF-8">

    <title>Main</title>

    <script src="main.js"></script>

</head>


<body>

    <div id="myapp"></div>

    <script>

            var app = Elm.Main.init({

                node: document.getElementById('myapp')

            });

    </script>

</body>

</html>


Quiero destacar las líneas importantes:

  • <head> - Tenemos una línea para cargar nuestro archivo main.js compilado. ¡Es obligatorio! Si compilas un módulo Elm llamado Main, obtendrás una función Elm.Main.init() disponible en JavaScript. Si compilas un módulo Elm llamado Home, obtendrás una función Elm.Home.init() en JavaScript. Etc.
  • <body> - Necesitamos hacer dos cosas aquí. Primero, creamos un <div> que queremos que nuestro programa Elm controle. ¿Quizás esté dentro de una aplicación más grande, rodeado de un montón de otras cosas? ¡No hay problema! Segundo, tenemos un <script> para inicializar nuestro programa Elm. Aquí llamamos a la función Elm.Main.init() para iniciar nuestro programa, pasando el nodo que queremos controlar.

Este es un archivo HTML normal, así que puedes incluir lo que quieras. Mucha gente carga archivos JS y CSS adicionales en el <head>. Esto significa que es perfectamente posible escribir el CSS a mano o generarlo de alguna manera. Agrega algo como <link rel="stylesheet" href="whatever-you-want.css"> en tu <head> y tendrás acceso a él. (También hay algunas opciones excelentes para especificar tu CSS dentro de Elm, ¡pero ese es otro tema!)

Ahora que sabemos cómo incrustar programas Elm en un documento HTML, es hora de empezar a explorar las tres opciones de interoperabilidad: indicadores, puertos y elementos personalizados en próximos posts.

martes, 3 de marzo de 2026

Empezando con Elm


Primero, instalemos elm, en ubuntu sería así: 


sudo apt update

sudo apt install nodejs npm -y

sudo npm install -g elm


Para ver si se instaló: 


elm --version


Ahora hagamos un nuevo proyecto, en una carpeta vacia pongamos: 


elm init


Y nos va a preguntar si queremos crear un archivo elm.json y un directorio src/:


elm.json describe tu proyecto.

src/ contiene todos tus archivos Elm.


Decile que si y metele. 


Ahora vamos a crear un archivo llamado src/Main.elm en tu editor y copia el código del ejemplo de los botones.


elm reactor te ayuda a crear proyectos Elm sin tener que modificar demasiado la terminal. Simplemente ejecútalo en la raíz de tu proyecto, así:


elm reactor


Esto inicia un servidor en http://localhost:8000. Puedes navegar a cualquier archivo Elm y ver su aspecto. Ejecuta elm reactor, sigue el enlace localhost e intenta revisar tu archivo src/Main.elm.


Puedes compilar código Elm a HTML o JavaScript con comandos como este:


elm make


# Crea un archivo index.html que puedas abrir en tu navegador.

# Crea un archivo index.html que puedas abrir en tu navegador. elm make src/Main.elm


# Crea un archivo JS optimizado para incrustarlo en un documento HTML personalizado.


elm make src/Main.elm --optimize --output=elm.js


Esta es la forma más general de compilar código Elm. Es extremadamente útil cuando tu proyecto se vuelve demasiado avanzado para Elm Reactor.


elm install


Todos los paquetes de Elm se encuentran en package.elm-lang.org.

Supongamos que buscas y decides que necesitas elm/http y elm/json para realizar algunas solicitudes HTTP. Puedes configurarlos en tu proyecto con los siguientes comandos:


elm install elm/http

elm install elm/json


Esto añade estas dependencias a tu archivo elm.json, haciendo que estos paquetes estén disponibles en tu proyecto. Esto te permitirá importar Http y usar funciones como Http.get en tus programas.


Consejos y trucos:

  • Primero, ¡no te preocupes por recordar todo esto!
  • Siempre puedes ejecutar elm --help para obtener una descripción completa de las capacidades de Elm.
  • También puedes ejecutar comandos como elm make --help y elm repl --help para obtener sugerencias sobre un comando específico. Esto es genial si quieres comprobar qué indicadores están disponibles y qué hacen.
  • Segundo, no te preocupes si te lleva un tiempo familiarizarte con la terminal en general.
  • Llevo usándola más de una década y todavía no recuerdo cómo comprimir archivos, encontrar todos los archivos de Elm en un directorio, etc. ¡Todavía busco información!


Ahora que tenemos nuestro editor configurado y elm disponible en la terminal, ¡volvamos a aprender Elm!

domingo, 1 de marzo de 2026

Reloj digital en elm


Hasta ahora nos hemos centrado en los comandos. Con los ejemplos de HTTP y aleatoriedad, le ordenamos a Elm que realizara una tarea específica inmediatamente, pero ese es un patrón un tanto extraño para un reloj. Siempre queremos saber la hora actual. ¡Aquí es donde entran en juego las suscripciones!


import Browser

import Html exposing (..)

import Task

import Time




-- MAIN



main =

  Browser.element

    { init = init

    , view = view

    , update = update

    , subscriptions = subscriptions

    }




-- MODEL



type alias Model =

  { zone : Time.Zone

  , time : Time.Posix

  }



init : () -> (Model, Cmd Msg)

init _ =

  ( Model Time.utc (Time.millisToPosix 0)

  , Task.perform AdjustTimeZone Time.here

  )




-- UPDATE



type Msg

  = Tick Time.Posix

  | AdjustTimeZone Time.Zone




update : Msg -> Model -> (Model, Cmd Msg)

update msg model =

  case msg of

    Tick newTime ->

      ( { model | time = newTime }

      , Cmd.none

      )


    AdjustTimeZone newZone ->

      ( { model | zone = newZone }

      , Cmd.none

      )




-- SUBSCRIPTIONS



subscriptions : Model -> Sub Msg

subscriptions model =

  Time.every 1000 Tick




-- VIEW



view : Model -> Html Msg

view model =

  let

    hour   = String.fromInt (Time.toHour   model.zone model.time)

    minute = String.fromInt (Time.toMinute model.zone model.time)

    second = String.fromInt (Time.toSecond model.zone model.time)

  in

  h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]


Todo lo nuevo proviene del paquete elm/time. 


Para trabajar con el tiempo correctamente en programación, necesitamos tres conceptos diferentes:

Hora Humana: Es lo que se ve en los relojes (8 a. m.) o en los calendarios (3 de mayo). ¡Genial! Pero si mi llamada es a las 8 a. m. en Boston, ¿qué hora es para mi amigo en Vancouver? Si es a las 8 a. m. en Tokio, ¿es el mismo día en Nueva York? (¡No!) Por lo tanto, entre las zonas horarias basadas en fronteras políticas en constante cambio y el uso inconsistente del horario de verano, ¡la hora humana básicamente nunca debería almacenarse en el modelo ni en la base de datos! ¡Es solo para visualización!

POSIX Time: Con POSIX Time, no importa dónde vivas ni en qué época del año sea. Es simplemente la cantidad de segundos transcurridos desde un momento arbitrario (en 1970). Donde quiera que vayas en la Tierra, la hora POSIX es la misma.

Zonas Horarias: Una "zona horaria" es un conjunto de datos que te permite convertir la hora POSIX en hora humana. ¡Pero no se trata solo de UTC-7 o UTC+3! Las zonas horarias son mucho más complicadas que una simple diferencia horaria. Cada vez que Florida cambia al horario de verano para siempre o Samoa cambia de UTC-11 a UTC+13, alguien añade una nota a la base de datos de zonas horarias de la IANA. Esa base de datos se carga en cada ordenador, y entre la hora POSIX y todos los casos excepcionales de la base de datos, ¡podemos calcular la hora humana!


Así que, para mostrarle la hora a un ser humano, siempre debes conocer Time.Posix y Time.Zone. ¡Eso es todo! Así que todo lo relacionado con la "hora humana" es para la función de vista, no para el modelo. De hecho, puedes verlo en nuestra vista:


vista: Modelo -> Mensaje HTML

vista modelo =

let

hora = String.fromInt (Time.toHour modelo.zone modelo.time)

minuto = String.fromInt (Time.toMinute modelo.zone modelo.time)

segundo = String.fromInt (Time.toSecond modelo.zone modelo.time)

in

h1 [] [texto (hora ++ ":" ++ minuto ++ ":" ++ segundo) ]


La función Time.toHour toma Time.Zone y Time.Posix nos devuelve un entero de 0 a 23 que indica qué hora es en tu zona horaria.


¿Cómo obtenemos nuestro Time.Posix? ¡Con una suscripción! Suscripciones: Modelo -> Submensaje


subscriptions : Model -> Sub Msg

subscriptions model =

  Time.every 1000 Tick


Usamos la función Time.every:


every : Float -> (Time.Posix -> msg) -> Sub msg


Requiere dos argumentos:

  • Un intervalo de tiempo en milisegundos. Dijimos 1000, que significa cada segundo. Pero también podríamos decir 60 * 1000 por cada minuto, o 5 * 60 * 1000 por cada cinco minutos.
  • Una función que convierte la hora actual en un mensaje. Por lo tanto, cada segundo, la hora actual se convertirá en un Tick <time> para nuestra función de actualización.

Ese es el patrón básico de cualquier suscripción. Se proporciona cierta configuración y se describe cómo generar valores de mensaje. 

Obtener Time.Zone es un poco más complicado. Nuestro programa creó un comando con:


Task.perform AdjustTimeZone Time.here


Leer la documentación de la tarea es la mejor manera de comprender esta línea. La documentación está escrita para explicar los nuevos conceptos, y creo que sería una digresión excesiva incluir una versión peor de esa información aquí. La cuestión es simplemente que ordenamos al entorno de ejecución que nos proporcione la Time.Zone dondequiera que se ejecute el código.

miércoles, 18 de febrero de 2026

Controlando los Efectos Colaterales: Elm, Akka y Koka


Los efectos colaterales son inevitables en cualquier programa real. Tarde o temprano necesitamos leer datos, escribir en pantalla, hacer una petición HTTP o guardar algo en una base de datos.

El problema no está en tener efectos, sino en no saber dónde y cuándo ocurren.


Lenguajes y frameworks como Elm, Akka y Koka proponen tres enfoques distintos para enfrentar este desafío, pero todos comparten la misma filosofía:

Mantener la lógica pura y controlada, y ejecutar los efectos en un punto bien definido.


Elm es un lenguaje funcional puro que se ejecuta en el navegador. En Elm, ninguna función puede tener efectos secundarios directamente.

En su lugar, el programa describe qué debería pasar, y el runtime de Elm se encarga de hacerlo realidad.


Por ejemplo, el corazón de un programa Elm suele tener esta función:


update : Msg -> Model -> (Model, Cmd Msg)


Cuando el usuario genera un mensaje (Msg), la función update:

  • recibe el estado actual (Model),
  • devuelve un nuevo modelo,
  • y opcionalmente un comando (Cmd Msg), que describe un efecto.


El punto clave es que update no ejecuta el efecto; simplemente lo describe.

Es el runtime de Elm quien decide cuándo y cómo hacerlo.

Así, todo el código de tu aplicación es puro y determinista, y los efectos están completamente bajo control.


Un ejemplo simple:


update msg model =

    case msg of

        LoadData ->

            ( model, Http.get { url = "/data", expect = GotData } )


        GotData response ->

            ( { model | data = response }, Cmd.none )


Aquí, LoadData devuelve una descripción de un efecto HTTP, pero Elm no lo ejecuta directamente.

La ejecución real ocurre más tarde, en un punto centralizado del runtime.


Akka, en el mundo de Scala y Java, aplica un principio muy similar desde la programación concurrente.

En Akka, los actores son unidades independientes que:

  • procesan un mensaje a la vez,
  • mantienen su propio estado interno,
  • y pueden enviar mensajes a otros actores.


Cada actor es como una pequeña cápsula que contiene su estado y sus efectos.

Nada del mundo exterior puede acceder a su estado directamente; solo se comunica mediante mensajes.


Por ejemplo:


class Counter extends Actor {

  var count = 0


  def receive: Receive = {

    case "inc" =>

      count += 1

      sender() ! count

  }

}


Este actor mantiene su propio contador y solo responde a mensajes.

El efecto (enviar una respuesta, escribir logs, actualizar estado) está encapsulado dentro del actor.


El runtime de Akka —el ActorSystem— se encarga de distribuir los mensajes y ejecutar los efectos concurrentemente, garantizando aislamiento y seguridad.

Desde afuera, un actor parece una función pura: recibe un mensaje y devuelve una respuesta.

Pero los efectos reales ocurren dentro del sistema, no en tu código.


Koka lleva la idea aún más lejos.

En lugar de confiar en un runtime que ejecuta los efectos, Koka los modela en el sistema de tipos.

Cada función en Koka declara explícitamente qué efectos puede producir.

Por ejemplo:


fun add(x : int, y : int) : int {

  x + y

}


Esta función es completamente pura.

En cambio:


fun printAdd(x : int, y : int) : <io> int {

  println("Sumando...")

  x + y

}


Aquí, el tipo <io> indica que esta función puede realizar operaciones de entrada/salida.

Koka rastrea los efectos de manera estática: el compilador sabe exactamente qué partes del programa son puras y cuáles no.


Esto permite escribir programas donde los efectos son explícitos, controlados y predecibles.

No hace falta leer todo el código para saber si una función puede modificar el mundo: el tipo te lo dice.


Aunque Elm, Akka y Koka se desarrollan en contextos muy diferentes —frontend funcional, backend concurrente y teoría de tipos—, los tres comparten una visión profunda de la pureza y el control de efectos.

  • Elm describe los efectos y delega su ejecución al runtime.
  • Akka encapsula los efectos en actores que procesan mensajes de forma aislada.
  • Koka anota los efectos en los tipos, haciéndolos explícitos desde el nivel más básico del lenguaje.


En los tres casos, la meta es la misma: escribir código predecible, testeable y sin sorpresas.


miércoles, 11 de febrero de 2026

Generar valores aleatorios en Elm



Hasta ahora solo hemos visto comandos para realizar solicitudes HTTP, pero también podemos ordenar otras cosas, como generar valores aleatorios. Vamos a crear una aplicación que tira dados y produce un número aleatorio entre 1 y 6.


import Browser

import Html exposing (..)

import Html.Events exposing (..)

import Random


-- MAIN

main =

  Browser.element

    { init = init

    , update = update

    , subscriptions = subscriptions

    , view = view

    }


-- MODEL

type alias Model =

  { dieFace : Int

  }


init : () -> (Model, Cmd Msg)

init _ =

  ( Model 1

  , Cmd.none

  )


-- UPDATE

type Msg

  = Roll

  | NewFace Int


update : Msg -> Model -> (Model, Cmd Msg)

update msg model =

  case msg of

    Roll ->

      ( model

      , Random.generate NewFace (Random.int 1 6)

      )


    NewFace newFace ->

      ( Model newFace

      , Cmd.none

      )


-- SUBSCRIPTIONS

subscriptions : Model -> Sub Msg

subscriptions model =

  Sub.none


-- VIEW

view : Model -> Html Msg

view model =

  div []

    [ h1 [] [ text (String.fromInt model.dieFace) ]

    , button [ onClick Roll ] [ text "Roll" ]

    ]


La novedad es el comando que se ejecuta en la función de actualización:


Random.generate NewFace (Random.int 1 6)


La generación de valores aleatorios funciona de forma ligeramente diferente a la de lenguajes como JavaScript, Python, Java, etc. ¡Veamos cómo funciona en Elm!


Para ello, utilizamos el paquete elm/random, en particular el módulo Random.

La idea principal es que contamos con un generador aleatorio que describe cómo generar un valor aleatorio. Por ejemplo:


import Random


probability : Random.Generator Float

probability =

  Random.float 0 1


roll : Random.Generator Int

roll =

  Random.int 1 6


usuallyTrue : Random.Generator Bool

usuallyTrue =

  Random.weighted (80, True) [ (20, False) ]


Aquí tenemos tres generadores aleatorios. El generador "roll" indica que producirá un entero, y más específicamente, un entero entre 1 y 6 inclusive. Del mismo modo, el generador "usuallyTrue" indica que producirá un booleano, y más específicamente, será verdadero el 80% de las veces.

La cuestión es que todavía no estamos generando los valores. Simplemente estamos describiendo cómo generarlos. A partir de ahí, se usa "Random.generate" para convertirlo en un comando:

generate : (a -> msg) -> Generator a -> Cmd msg

Al ejecutar el comando, el generador genera un valor que se convierte en un mensaje para la función de actualización. En nuestro ejemplo, el generador genera un valor entre 1 y 6, que se convierte en un mensaje como NewFace 1 o NewFace 4. Eso es todo lo que necesitamos para obtener nuestras tiradas de dados aleatorias, ¡pero los generadores pueden hacer mucho más!

Una vez que tengamos generadores simples como probability y usuallyTrue, podemos empezar a combinarlos con funciones como map3. Imaginemos que queremos crear una máquina tragamonedas sencilla. Podríamos crear un generador como este:


import Random


type Symbol = Cherry | Seven | Bar | Grapes


symbol : Random.Generator Symbol

symbol =

  Random.uniform Cherry [ Seven, Bar, Grapes ]


type alias Spin =

  { one : Symbol

  , two : Symbol

  , three : Symbol

  }


spin : Random.Generator Spin

spin =

  Random.map3 Spin symbol symbol symbol


Primero creamos un símbolo para describir las imágenes que pueden aparecer en la máquina tragamonedas. Luego, creamos un generador aleatorio que genera cada símbolo con la misma probabilidad.

A partir de ahí, usamos map3 para combinarlos en un nuevo generador de giros. Este indica que se deben generar tres símbolos y luego combinarlos para formar un giro.

La idea es que, a partir de pequeños bloques, podemos crear un generador que describe un comportamiento bastante complejo. Y luego, desde nuestra aplicación, solo tenemos que ejecutar algo como Random.generate NewSpin spin para obtener el siguiente valor aleatorio.

domingo, 1 de febrero de 2026

JSON Decoders de elm



El paquete elm/json nos proporciona el módulo Json.Decode. Está lleno de pequeños decodificadores que podemos conectar.

Para obtener "edad" de { "nombre": "Tom", "edad": 42 }, crearíamos un decodificador como este:


import Json.Decode exposing (Decoder, field, int)


ageDecoder : Decoder Int

ageDecoder =

  field "age" int


 -- int : Decoder Int

 -- field : String -> Decoder a -> Decoder a


La función campo toma dos argumentos:

String: el nombre del campo. Por lo tanto, solicitamos un objeto con un campo "edad".

Decoder a: el decodificador que se probará a continuación. Si el campo "edad" existe, probaremos este decodificador con el valor que contiene.

En resumen, el campo "edad" int solicita un campo "edad" y, si existe, ejecuta el Decoder Int para intentar extraer un entero.

Hacemos prácticamente lo mismo para extraer el campo "nombre":


import Json.Decode exposing (Decoder, field, string)


nameDecoder : Decoder String

nameDecoder =

  field "name" string


-- string : Decoder String


En este caso, solicitamos un objeto con un campo "nombre" y, si existe, queremos que su valor sea una cadena.


¿Y si queremos decodificar dos campos? Unimos los decodificadores con map2:


map2 : (a -> b -> value) -> Decoder a -> Decoder b -> Decoder value

Esta función toma dos decodificadores. Los prueba y combina sus resultados. Ahora podemos combinar dos decodificadores diferentes:


import Json.Decode exposing (Decoder, map2, field, string, int)


type alias Person =

  { name : String

  , age : Int

  }


personDecoder : Decoder Person

personDecoder =

  map2 Person

      (field "name" string)

      (field "age" int)


Por lo tanto, si usáramos personDecoder en { "name": "Tom", "age": 42 }, obtendríamos un valor Elm como Person "Tom" 42.

Si realmente quisiéramos adentrarnos en la esencia de los decodificadores, definiríamos personDecoder como map2 Person nameDecoder ageDecoder utilizando nuestras definiciones anteriores. ¡Siempre conviene construir los decodificadores a partir de bloques más pequeños!

Muchos datos JSON no son tan claros ni planos. Imagina si /api/random-quotes/v2 se hubiera publicado con información más completa sobre los autores:


{

  "quote": "December used to be a month but it is now a year",

  "source": "Letters from a Stoic",

  "author":

  {

    "name": "Seneca",

    "age": 68,

    "origin": "Cordoba"

  },

  "year": 54

}

Podríamos gestionar este nuevo escenario anidando nuestros decodificadores:

import Json.Decode exposing (Decoder, map2, map4, field, int, string)


type alias Quote =

  { quote : String

  , source : String

  , author : Person

  , year : Int

  }


quoteDecoder : Decoder Quote

quoteDecoder =

  map4 Quote

    (field "quote" string)

    (field "source" string)

    (field "author" personDecoder)

    (field "year" int)


type alias Person =

  { name : String

  , age : Int

  }


personDecoder : Decoder Person

personDecoder =

  map2 Person

    (field "name" string)

    (field "age" int)


Observe que no nos molestamos en decodificar el campo "origen" del autor. Los decodificadores no tienen ningún problema en omitir campos, lo que puede ser útil al extraer poca información de valores JSON muy grandes.

Json en Elm




Acabamos de ver un ejemplo que usa HTTP para obtener el contenido de un libro. Pero muchos servidores devuelven datos en un formato especial llamado Notación de Objetos JavaScript, o JSON.

Nuestro siguiente ejemplo muestra cómo obtener datos JSON, lo que nos permite pulsar un botón para mostrar citas aleatorias de una selección aleatoria de libros. 


import Browser

import Html exposing (..)

import Html.Attributes exposing (style)

import Html.Events exposing (..)

import Http

import Json.Decode exposing (Decoder, map4, field, int, string)


-- MAIN

main =

  Browser.element

    { init = init

    , update = update

    , subscriptions = subscriptions

    , view = view

    }


-- MODEL

type Model

  = Failure

  | Loading

  | Success Quote


type alias Quote =

  { quote : String

  , source : String

  , author : String

  , year : Int

  }


init : () -> (Model, Cmd Msg)

init _ =

  (Loading, getRandomQuote)


-- UPDATE

type Msg

  = MorePlease

  | GotQuote (Result Http.Error Quote)


update : Msg -> Model -> (Model, Cmd Msg)

update msg model =

  case msg of

    MorePlease ->

      (Loading, getRandomQuote)


    GotQuote result ->

      case result of

        Ok quote ->

          (Success quote, Cmd.none)


        Err _ ->

          (Failure, Cmd.none)


-- SUBSCRIPTIONS

subscriptions : Model -> Sub Msg

subscriptions model =

  Sub.none


-- VIEW

view : Model -> Html Msg

view model =

  div []

    [ h2 [] [ text "Random Quotes" ]

    , viewQuote model

    ]


viewQuote : Model -> Html Msg

viewQuote model =

  case model of

    Failure ->

      div []

        [ text "I could not load a random quote for some reason. "

        , button [ onClick MorePlease ] [ text "Try Again!" ]

        ]


    Loading ->

      text "Loading..."


    Success quote ->

      div []

        [ button [ onClick MorePlease, style "display" "block" ] [ text "More Please!" ]

        , blockquote [] [ text quote.quote ]

        , p [ style "text-align" "right" ]

            [ text "— "

            , cite [] [ text quote.source ]

            , text (" by " ++ quote.author ++ " (" ++ String.fromInt quote.year ++ ")")

            ]

        ]


-- HTTP

getRandomQuote : Cmd Msg

getRandomQuote =

  Http.get

    { url = "https://elm-lang.org/api/random-quotes"

    , expect = Http.expectJson GotQuote quoteDecoder

    }


quoteDecoder : Decoder Quote

quoteDecoder =

  map4 Quote

    (field "quote" string)

    (field "source" string)

    (field "author" string)

    (field "year" int)


Este ejemplo es bastante similar al anterior:

  • init inicia en el estado Cargando, con un comando para obtener una cita aleatoria.
  • update gestiona el mensaje GotQuote cuando hay una nueva cita disponible. Pase lo que pase, no tenemos comandos adicionales. También gestiona el mensaje MorePlease cuando alguien presiona el botón, emitiendo un comando para obtener más citas aleatorias.
  • view muestra las citas!

La principal diferencia está en la definición de getRandomCatGif. En lugar de usar Http.expectString, hemos cambiado a Http.expectJson. ¿Qué ocurre con esto?


Cuando se solicita a /api/random-quotes una cita aleatoria, el servidor produce una cadena JSON como esta:


{

"quote": "Diciembre solía ser un mes, pero ahora es un año",

"source": "Cartas de un estoico",

"author": "Séneca",

"year": 54

}


No ofrecemos garantías sobre la información aquí contenida. El servidor puede cambiar los nombres de los campos, y estos pueden tener diferentes tipos en distintas situaciones. ¡Es un mundo complejo!

En JavaScript, el enfoque consiste en convertir JSON en objetos JavaScript y esperar que todo salga bien. Pero si hay algún error tipográfico o datos inesperados, se produce una excepción de tiempo de ejecución en alguna parte del código. ¿Era incorrecto el código? ¿Eran incorrectos los datos? ¡Es hora de investigar para averiguarlo!

En Elm, validamos el JSON antes de que llegue a nuestro programa. Por lo tanto, si los datos tienen una estructura inesperada, lo detectamos de inmediato. Es imposible que datos incorrectos se filtren y provoquen una excepción de tiempo de ejecución tres archivos después. Esto se logra con decodificadores JSON.

viernes, 30 de enero de 2026

HTTP en elm

 


A menudo resulta útil obtener información de otras fuentes de internet.

Por ejemplo, supongamos que queremos cargar el texto completo de "Opinión Pública" de Walter Lippmann. Publicado en 1922, este libro ofrece una perspectiva histórica sobre el auge de los medios de comunicación y sus implicaciones para la democracia. Para nuestros propósitos, nos centraremos en cómo usar el paquete elm/http para integrar este libro en nuestro programa.

Si queres ver el ejemplo en acción, hace click en este link: https://elm-lang.org/examples/book


import Browser

import Html exposing (Html, text, pre)

import Http


-- MAIN


main =

  Browser.element

    { init = init

    , update = update

    , subscriptions = subscriptions

    , view = view

    }


-- MODEL


type Model

  = Failure

  | Loading

  | Success String


init : () -> (Model, Cmd Msg)

init _ =

  ( Loading

  , Http.get

      { url = "https://elm-lang.org/assets/public-opinion.txt"

      , expect = Http.expectString GotText

      }

  )


-- UPDATE


type Msg

  = GotText (Result Http.Error String)


update : Msg -> Model -> (Model, Cmd Msg)

update msg model =

  case msg of

    GotText result ->

      case result of

        Ok fullText ->

          (Success fullText, Cmd.none)


        Err _ ->

          (Failure, Cmd.none)


-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg

subscriptions model =

  Sub.none


-- VIEW


view : Model -> Html Msg

view model =

  case model of

    Failure ->

      text "I was unable to load your book."


    Loading ->

      text "Loading..."


    Success fullText ->

      pre [] [ text fullText ]


Algunas partes de esto deberían resultar familiares de ejemplos anteriores de la arquitectura Elm. Seguimos teniendo un modelo de nuestra aplicación. Seguimos teniendo una actualización que reacciona a los mensajes. Seguimos teniendo una función de vista que muestra todo en pantalla.

Las nuevas partes amplían el patrón principal que vimos antes con algunos cambios en init y update, y la incorporación de la suscripción.

La función init describe cómo inicializar nuestro programa:


init : () -> (Model, Cmd Msg)

init _ =

  ( Loading

  , Http.get

      { url = "https://elm-lang.org/assets/public-opinion.txt"

      , expect = Http.expectString GotText

      }

  )

Como siempre, debemos generar el modelo inicial, pero ahora también generamos un comando que indica lo que queremos hacer inmediatamente. Ese comando generará un mensaje que se introduce en la función de actualización.

Nuestro sitio web del libro se inicia en estado de carga y queremos obtener el texto completo. Al realizar una solicitud GET con Http.get, especificamos la URL de los datos que queremos obtener y qué datos esperamos que contengan. En nuestro caso, la URL apunta a datos del sitio web de Elm y esperamos que sea una cadena grande que podamos mostrar en pantalla.

La línea Http.expectString GotText indica algo más que esperamos una cadena. También indica que, cuando recibamos una respuesta, esta debe convertirse en un mensaje GotText:


type Msg

  = GotText (Result Http.Error String)


-- GotText (Ok "The Project Gutenberg EBook of ...")

-- GotText (Err Http.NetworkError)

-- GotText (Err (Http.BadStatus 404))


Tengamos en cuenta que usamos el tipo Result, mencionado en un par de secciones anteriores. Esto nos permite considerar completamente los posibles fallos en nuestra función de actualización. Hablando de funciones de actualización...

Nuestra función de actualización también devuelve un poco más de información:


update : Msg -> Model -> (Model, Cmd Msg)

update msg model =

  case msg of

    GotText result ->

      case result of

        Ok fullText ->

          (Success fullText, Cmd.none)


        Err _ ->

          (Failure, Cmd.none)


Al observar la firma de tipo, vemos que no solo devolvemos un modelo actualizado. También generamos un comando que indica lo que queremos que haga Elm.

Pasando a la implementación, realizamos la coincidencia de patrones con los mensajes de forma normal. Cuando llega un mensaje GotText, inspeccionamos el resultado de nuestra solicitud HTTP y actualizamos nuestro modelo según si fue un éxito o un fracaso. La novedad es que también proporcionamos un comando.

En caso de obtener el texto completo correctamente, ejecutamos Cmd.none para indicar que no hay más trabajo que hacer. ¡Ya obtuvimos el texto completo!

Y en caso de algún error, también ejecutamos Cmd.none y simplemente nos rendimos. El texto del libro no se cargó. Si quisiéramos ser más sofisticados, podríamos realizar la coincidencia de patrones con Http.Error y reintentar la solicitud si se agota el tiempo de espera o algo similar.

La cuestión es que, independientemente de cómo decidamos actualizar nuestro modelo, también podemos ejecutar nuevos comandos. ¡Necesito más datos! ¡Quiero un número aleatorio! Etc.

La otra novedad de este programa es la función de suscripción. Permite consultar el modelo y decidir si se desea suscribirse a cierta información. En nuestro ejemplo, usamos Sub.none para indicar que no necesitamos suscribirnos a nada, pero pronto veremos un ejemplo de un reloj al que queremos suscribirnos a la hora actual.

Al crear un programa con Browser.element, configuramos un sistema como este:

Podemos ejecutar comandos desde init y update. Esto nos permite, por ejemplo, realizar solicitudes HTTP cuando queramos. También podemos suscribirnos a información interesante. (¡Veremos un ejemplo de suscripciones más adelante!)


miércoles, 17 de diciembre de 2025

Consumir una API REST desde Elm


La idea es que simplemente pintemos "hola mundo" con él consumiendo una API Rest: 


module Main exposing (..)


import Browser

import Html exposing (Html, div, text, button)

import Html.Events exposing (onClick)

import Http

import Json.Decode exposing (Decoder, field, string)



-- MODEL


type alias Model =

    { message : String

    , loading : Bool

    }


init : () -> ( Model, Cmd Msg )

init _ =

    ( { message = "", loading = False }, Cmd.none )


-- UPDATE


type Msg

    = FetchMessage

    | GotMessage (Result Http.Error String)



update : Msg -> Model -> ( Model, Cmd Msg )

update msg model =

    case msg of

        FetchMessage ->

            ( { model | loading = True }

            , getMessage

            )


        GotMessage (Ok message) ->

            ( { model | message = message, loading = False }, Cmd.none )


        GotMessage (Err _) ->

            ( { model | message = "Error fetching message", loading = False }, Cmd.none )




-- HTTP REQUEST


getMessage : Cmd Msg

getMessage =

    Http.get

        { url = "http://localhost:8080/hello"

        , expect = Http.expectString GotMessage

        }


-- VIEW


view : Model -> Html Msg

view model =

    div []

        [ button [ onClick FetchMessage ] [ text "Fetch Message" ]

        , div []

            [ text

                (if model.loading then

                    "Loading..."

                 else

                    model.message

                )

            ]

        ]




-- MAIN


main : Program () Model Msg

main =

    Browser.element

        { init = init

        , update = update

        , subscriptions = always Sub.none

        , view = view

        }


  • El modelo (Model) guarda el mensaje recibido y un flag de carga (loading).
  • Al hacer clic en el botón, se dispara el mensaje FetchMessage, que a su vez ejecuta getMessage.
  • Http.get realiza una petición a la URL http://localhost:8080/hello.
  • Si la respuesta es exitosa, el mensaje GotMessage (Ok ...) actualiza el modelo con el texto devuelto por el servidor.
  • La vista muestra un botón y el resultado de la petición (o un “Loading...” mientras espera).


Podés usar cualquier backend —por ejemplo, un mini servidor Node.js o Express:


// server.js

import express from "express";

const app = express();


app.get("/hello", (req, res) => {

  res.send("Hello from the API!");

});


app.listen(8080, () => console.log("API running on http://localhost:8080/hello"));


miércoles, 12 de noviembre de 2025

Elm vs Haskell: dos caminos del paradigma funcional


Haskell y Elm comparten una raíz común: ambos son lenguajes funcionales puros, con tipado estático, inferencia de tipos y un fuerte énfasis en la inmutabilidad.

Sin embargo, cada uno tomó un camino distinto.

Haskell apostó por la abstracción, la teoría de tipos avanzada y la expresividad; Elm, por la simplicidad, la seguridad y la experiencia del desarrollador.

Haskell nació en el ámbito académico, con el objetivo de ser un lenguaje funcional puro que sirviera como base de investigación.

Por eso prioriza la expresividad, la abstracción y la corrección formal.

Su lema podría ser: “todo puede expresarse en tipos”.

Elm, en cambio, nació del mundo web.

Su meta no es la investigación, sino la confiabilidad.

Fue diseñado para construir interfaces web sin errores en tiempo de ejecución.

Su lema podría ser: “ningún runtime exception, nunca”.


En resumen:

Haskell es una herramienta para explorar los límites del paradigma funcional.

Elm es una herramienta para aplicar ese paradigma con seguridad y pragmatismo.


Ambos tienen tipado estático e inferencia, pero el sistema de tipos de Haskell es mucho más poderoso.

Haskell permite type classes, kind polymorphism, type families, GADTs, monads, existentials y un sinfín de extensiones.

Elm tiene un sistema de tipos mucho más pequeño, pero más legible y predecible.

En Haskell, el tipo puede expresar conceptos avanzados:


fmap :: Functor f => (a -> b) -> f a -> f b


Mientras que en Elm, los tipos se mantienen simples y directos:


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


En Haskell podés crear tus propias type classes (Eq, Ord, Monad, etc.).

En Elm no existen type classes: sólo hay un conjunto fijo de restricciones (number, comparable, appendable).


Haskell es un lenguaje donde los tipos son una herramienta de abstracción.

Elm los usa más bien como una herramienta de seguridad.


Tanto Haskell como Elm son lenguajes funcionales puros: ninguna función puede tener efectos secundarios sin declararlo explícitamente.

Pero los abordan de forma distinta.


Haskell utiliza el sistema de monads para modelar efectos: IO, Maybe, State, etc.

Cada efecto se encapsula en un tipo, y se encadenan usando do notation.


main :: IO ()

main = do

    name <- getLine

    putStrLn ("Hola, " ++ name)


Elm evita completamente las monads.

En su lugar, usa un modelo explícito de efectos: los Commands (Cmd) y Subscriptions (Sub), que forman parte del Elm Architecture.

Esto mantiene la pureza del lenguaje sin exponer al programador a conceptos teóricos complejos.


update : Msg -> Model -> (Model, Cmd Msg)

update msg model =

    case msg of

        CargarDatos ->

            ( model, Http.get {...} )


        DatosCargados datos ->

            ( { model | items = datos }, Cmd.none )


Haskell ofrece poder; Elm ofrece control.

En Haskell, el manejo de efectos es flexible y extensible.

En Elm, es seguro y predecible.


Elm está diseñado para ser simple y consistente.

La sintaxis es limpia, sin operadores ambiguos ni extensiones opcionales.

Haskell, en cambio, permite una gran expresividad, pero también puede resultar críptico.


En Elm:


sumar : Int -> Int -> Int

sumar x y =

    x + y


En Haskell:


sumar :: Int -> Int -> Int

sumar x y = x + y


Parecen casi iguales, pero Haskell permite redefinir operadores, crear infix personalizados o usar point-free style, lo que puede aumentar la complejidad.

Elm evita deliberadamente esa flexibilidad para mantener el código legible para todos.


Haskell es un lenguaje generalista: se usa en compiladores, sistemas financieros, backends web, análisis estático y más.

Su ecosistema es vasto y diverso, aunque muchas librerías varían en calidad y mantenimiento.


Elm se centra exclusivamente en el frontend web.

Todo su diseño gira en torno a construir aplicaciones en el navegador.

No hay ambigüedad: un proyecto Elm siempre es una aplicación web.

A cambio de esa limitación, ofrece una experiencia coherente, con un compilador extremadamente útil y mensajes de error ejemplares.


La diferencia más grande entre ambos lenguajes quizá sea emocional. Haskell a veces puede parecer un rompecabezas: poderoso, elegante, pero con una curva de aprendizaje pronunciada.

Elm, en cambio, busca que programar sea agradable, incluso para quienes no tienen experiencia previa en programación funcional.

El compilador de Elm no sólo te dice qué está mal, sino cómo arreglarlo.

El de Haskell, aunque más sofisticado, puede ser más críptico si no conocés sus fundamentos teóricos.


Haskell y Elm son dos lenguajes que muestran dos filosofías complementarias del mundo funcional.

Haskell te da un universo para explorar la abstracción.

Elm te da un terreno firme donde construir sin errores.

martes, 11 de noviembre de 2025

Type constraints en Elm


Elm es un lenguaje de tipado estático e inferencia fuerte: el compilador deduce los tipos automáticamente, pero también te permite declarar funciones genéricas que funcionan con más de un tipo.

Por ejemplo:


identity : a -> a

identity x = x


Esta función acepta cualquier tipo a.

Sin embargo, a veces queremos restringir qué tipos son válidos.

Ahí entran en juego los type constraints (restricciones de tipo).

En Elm, los type constraints permiten decir:

> “Este tipo genérico debe cumplir con ciertas propiedades (por ejemplo, ser comparable o numérico)”.


A diferencia de Haskell o Scala, Elm no tiene type classes, pero ofrece un pequeño conjunto de restricciones integradas que cubren los casos más comunes.

Elm define cuatro categorías de tipos con restricciones que podés usar en tus firmas de tipo:

  • number: Tipos que soportan operaciones aritméticas como Int, Float
  • comparable: Tipos que pueden ordenarse o compararse  como Int, Float, Char, String, tuples de comparables
  • appendable: Tipos que pueden concatenarse como String, List a
  • compappend:| Tipos que son a la vez comparable y appendable 


Podés restringir una función a operar solo sobre números:


sumar : number -> number -> number

sumar x y =

    x + y


Esto funciona con Int o Float, pero no con String.


Si necesitás ordenar o comparar valores:


menor : comparable -> comparable -> comparable

menor a b =

    if a < b then

        a

    else

        b


O incluso:


ordenar : List comparable -> List comparable

ordenar lista =

    List.sort lista


Cuando querés concatenar elementos:


concatenar : appendable -> appendable -> appendable

concatenar a b =

    a ++ b


Funciona con:


concatenar "Hola, " "mundo!"        -- "Hola, mundo!"

concatenar [1,2] [3,4]              -- [1,2,3,4]


En Elm no se pueden definir tipos personalizados que sean comparable o appendable.

Por ejemplo, este tipo:


type alias Persona =

    { nombre : String, edad : Int }


No puede usarse en una función List.sort directamente.

Pero podés ordenarlo con una clave usando List.sortBy:


ordenarPorEdad : List Persona -> List Persona

ordenarPorEdad personas =

    List.sortBy .edad personas


O definir un criterio personalizado:


ordenarPorNombreDesc : List Persona -> List Persona

ordenarPorNombreDesc personas =

    List.sortWith (\a b -> compare b.nombre a.nombre) personas


Elm mantiene su sistema de tipos simple pero poderoso: no hay typeclasses ni herencia, pero sí restricciones útiles y seguras para los casos más comunes.