Translate

sábado, 9 de agosto de 2025

Mensajes y Modelos en Elm


Ya vimos un par de ejemplos de definición de un tipo Msg. Este tipo de tipo es extremadamente común en Elm. En nuestra sala de chat, podríamos definir un tipo Msg de la siguiente manera:


type Msg

   = PressedEnter

   | ChangedDraft String

   | ReceivedMessage { user: User, message: String }

   | ClickedExit


Tenemos cuatro variantes. Algunas no tienen datos asociados, otras tienen muchos. Observe que ReceivedMessage tiene un registro como dato asociado. Esto es perfectamente correcto. ¡Cualquier tipo puede ser un dato asociado! Esto le permite describir las interacciones en su aplicación con mucha precisión.

Los tipos personalizados se vuelven extremadamente potentes cuando empieza a modelar situaciones con mucha precisión. Por ejemplo, si está esperando a que se carguen datos, podría querer modelarlos con un tipo personalizado como este:


type Profile

   = Failure

   | Loading

   | Success { name : String, description : String }


Así, puedes comenzar en el estado Loading y luego pasar a Failure o Success según lo que suceda. Esto simplifica enormemente la creación de una función de vista que siempre muestre algo razonable cuando se cargan los datos.

Los tipos personalizados son la característica más importante de Elm. Ofrecen mucha profundidad, especialmente una vez que te acostumbras a modelar escenarios con mayor precisión. 

is, as de C# y el viejo amigo instanceof de Java


En C#, trabajar con tipos en tiempo de ejecución es bastante cómodo gracias a dos operadores clave: is y as.

is nos dice si un objeto es de un tipo determinado, y desde C# 7 nos deja incluso declarar la variable directamente en la comprobación.


if (obj is string texto)

{

    Console.WriteLine(texto.ToUpper());

}


De esta forma, la verificación y el cast se hacen en una sola línea, clara y segura.


El otro operador, as, va un paso más allá: intenta convertir el objeto y, si no puede, devuelve null en lugar de lanzar una excepción. Ideal para esos casos donde la conversión no es obligatoria, pero sí útil si ocurre:


var texto = obj as string;

if (texto != null)

{

    Console.WriteLine(texto.Length);

}


Con estos dos, en C# rara vez tenemos que hacer casts inseguros, y eso se agradece.


Ahora, en el mundo Java siempre hemos tenido a instanceof, que durante años fue… digamos… más básico: verificaba el tipo, pero luego había que castear manualmente:


if (obj instanceof String) {

    String texto = (String) obj;

    System.out.println(texto.toUpperCase());

}


Esto cambió en Java 14, cuando instanceof adoptó pattern matching. Ahora podemos escribir:


if (obj instanceof String texto) {

    System.out.println(texto.toUpperCase());

}


Sí, muy similar a lo que C# ya hacía.



Y con Java 21 llegó la verdadera vuelta de tuerca: pattern matching dentro de switch. Esto no solo ahorra código, sino que hace más expresivas estructuras de control que antes eran un desfile de if/else.


switch (obj) {

    case String s -> System.out.println("Cadena: " + s.toUpperCase());

    case Integer i -> System.out.println("Entero: " + (i * 2));

    default -> System.out.println("Otro tipo");

}


Así que hoy, si trabajás en C#, tenés en is y as dos herramientas muy potentes para escribir código seguro y legible. Y si venís de Java, sabé que instanceof ya no es el operador limitado de antes: está alcanzando la versatilidad de C#, y con switch incluso ofrece caminos nuevos para organizar la lógica.

En ambos lenguajes, dominar estas técnicas significa escribir código más limpio, menos propenso a errores y, sobre todo, más agradable de mantener.


viernes, 8 de agosto de 2025

Tipos personalizados en Elm



Hasta ahora hemos visto varios tipos como Bool, Int y String. Pero ¿cómo definimos los nuestros?

Supongamos que estamos creando una sala de chat. Todos necesitan un nombre, pero algunos usuarios no tienen una cuenta permanente. Simplemente dan un nombre cada vez que acceden.

Podemos describir esta situación definiendo el tipo UserStatus, enumerando todas las posibles variaciones:


type UserStatus = Regular | Visitor


El tipo UserStatus tiene dos variantes: un usuario puede ser Regular o un Visitante. Por lo tanto, podríamos representar a un usuario como un registro de la siguiente manera:


type UserStatus

   = Regular

   | Visitor


type alias User =

{ status : UserStatus

, name : String

}


thomas = { status = Regular, name = "Thomas" }

kate95 = { status = Visitor, name = "kate95" }


Ahora podemos rastrear si un usuario es Regular con cuenta o un Visitante de paso. No es muy complicado, ¡pero podemos simplificarlo!


En lugar de crear un tipo personalizado y un alias, podemos representar todo esto con un solo tipo personalizado. Las variantes Regular y Visitor tienen datos asociados. En nuestro caso, estos datos son un valor de cadena:


type User

   = Regular String

    | Visitor String


thomas = Regular "Thomas"

kate95 = Visitor "kate95"


Los datos se adjuntan directamente a la variante, por lo que ya no es necesario el registro.

Otra ventaja de este enfoque es que cada variante puede tener diferentes datos asociados. Supongamos que los usuarios Regulares proporcionaron su edad al registrarse. No hay una forma sencilla de capturar esto con registros, pero al definir tu propio tipo personalizado, no hay problema. Añadamos algunos datos asociados a la variante Regular :


> type User

   = Regular String Int

   | Visitor String


> Regular

<function> : String -> Int -> User


> Visitor

<function> : String -> User


> Regular "Thomas" 44

Regular "Thomas" 44 : User


> Visitor "kate95"

Visitor "kate95" : User


Solo hemos añadido la edad, pero las variantes de un tipo pueden variar bastante. Por ejemplo, quizá queramos añadir la ubicación de los usuarios Regulares para poder sugerir salas de chat regionales. O quizá queramos tener usuarios anónimos:


type User

  = Regular String Int Location

  | Visitor String

  | Anonymous



martes, 5 de agosto de 2025

Modelado en Elm


Los tipos personalizados se vuelven extremadamente potentes cuando se empiezan a modelar situaciones con mucha precisión. Por ejemplo, si se espera la carga de datos, se podría modelar con un tipo personalizado como este:


type Profile

   = Failure

   | Loading

   | Success { name : String, description : String }


De esta forma, se puede empezar en el estado de carga y luego pasar a Failure o Success según lo que ocurra. Esto simplifica enormemente la creación de una función de vista que siempre muestre algo razonable cuando se cargan los datos.

Los tipos personalizados son la característica más importante de Elm. Ofrecen mucha profundidad, especialmente una vez que se adquiere el hábito de modelar escenarios con mayor precisión. 

¿Qué es un LLM y como empezamos a aprenderlo?


Un Large Language Model (LLM) es un modelo de inteligencia artificial entrenado con enormes volúmenes de texto para entender y generar lenguaje humano. Son la base de tecnologías como ChatGPT, Copilot o traductores automáticos avanzados.

Lo revolucionario de los LLMs es su capacidad para responder preguntas, escribir código, redactar textos, traducir, razonar y aprender patrones del lenguaje con una fluidez que antes parecía imposible.

Los LLMs no son solo una moda: están transformando la manera en que interactuamos con la información, automatizamos tareas y diseñamos productos inteligentes.


Y ¿Por dónde empezar? Libros para aprender sobre LLMs:

  • "Deep Learning" – Ian Goodfellow, Yoshua Bengio, Aaron Courville (Clásico, incluye fundamentos clave para entender redes profundas)
  • "Natural Language Processing with Transformers" – Lewis Tunstall, Leandro von Werra, Thomas Wolf (Excelente para entender cómo funcionan los LLMs modernos, como los de Hugging Face)
  • "Transformers for Natural Language Processing" – Denis Rothman  (Explica en detalle el modelo Transformer y cómo se aplica en NLP)
  • "The Hundred-Page Machine Learning Book" – Andriy Burkov  (Rápido, claro, cubre muchos fundamentos útiles para entender modelos como los LLMs)



Mensajes en Elm


En este post vimos un par de ejemplos de definición de un tipo Msg. Este tipo de tipo es muy común en Elm. En nuestra sala de chat, podríamos definir un tipo Msg de la siguiente manera:


type Msg

    = PressedEnter

    | ChangedDraft String

    | ReceivedMessage {user: User, message: String}

    | ClickedExit


Tenemos cuatro variantes. Algunas no tienen datos asociados, otras sí. Observe que ReceivedMessage tiene un registro como dato asociado. Esto es perfectamente correcto. ¡Cualquier tipo puede ser un dato asociado! Esto le permite describir las interacciones en su aplicación con mucha precisión.


Constructores de Registros en Elm


Al crear un alias de tipo específico para un registro, también se genera un constructor de registros. Por lo tanto, si definimos un alias de tipo Usuario, podemos empezar a construir registros de la siguiente manera:


> type alias Usuario = { nombre: String, edad: Int}


> Usuario

<función>: String -> Int -> Usuario


> Usuario "Sue" 58

{ nombre = "Sue", edad = 58}: Usuario


> Usuario "Tom" 31

{ nombre = "Tom", edad = 31}: Usuario


Ten en cuenta que el orden de los argumentos en el constructor de registros coincide con el orden de los campos en el alias de tipo.


Reiteramos que esto es solo para registros. Crear alias de tipo para otros tipos no generará un constructor.

viernes, 1 de agosto de 2025

Modelos en Elm


Es muy común usar alias de tipo al diseñar un modelo. Cuando estábamos aprendiendo sobre la arquitectura de Elm, vimos un modelo como este:


type alias Model =

  { name : String

  , password : String

  , passwordAgain : String

  }


La principal ventaja de usar un alias de tipo para esto es que al escribir las anotaciones de tipo para las funciones de actualización y vista, es mucho más fácil escribir Msg -> Modelo -> Modelo que la versión completa. Además, permite añadir campos a nuestro modelo sin necesidad de modificar ninguna anotación de tipo.

miércoles, 30 de julio de 2025

Alias de tipo en Elm


Las anotaciones de tipo pueden llegar a ser largas. Esto puede ser un verdadero problema si tiene registros con muchos campos. Esta es la razón principal de ser de los alias de tipo. Un alias de tipo es un nombre más corto para un tipo. Por ejemplo, podría crear un alias de usuario como este:


type alias User =

{ name : String, age : Int }


En lugar de escribir el tipo de registro completo constantemente, podemos simplemente escribir User. Esto nos ayuda a escribir anotaciones de tipo más fáciles de leer:


-- WITH ALIAS


isOldEnoughToVote : User -> Bool

isOldEnoughToVote user =

user.age >= 18


-- WITHOUT ALIAS


isOldEnoughToVote : { name : String, age : Int } -> Bool

isOldEnoughToVote user =

user.age >= 18


Estas dos definiciones son equivalentes, pero la que tiene un alias de tipo es más corta y fácil de leer. Entonces, lo único que estamos haciendo es crear un alias para un tipo largo.

martes, 29 de julio de 2025

Comparando obj != null, obj is { } y obj is not null en C#


En C#, hay varias formas de verificar si una variable no es null. Con la introducción de pattern matching, surgieron nuevas opciones como obj is { } y obj is not null. Aunque parecen equivalentes, no son exactamente lo mismo y conviene entender cuándo usar cada uno.


obj != null


Es la forma clásica de comprobar si una referencia no es null. Evalúa directamente la igualdad.


if (obj != null)

{

    // obj no es null

}


Cuándo usarlo:

  • Para chequeos simples de null.
  • En código tradicional o cuando la variable no participa en pattern matching.


Pero esto, no hace type test ni pattern matching. Solo evalúa igualdad.


obj is not null

Usa pattern matching para comprobar si la variable no es null.


if (obj is not null)

{

    // obj no es null

}


Cuándo usarlo:

  • Cuando querés mantener consistencia con otros patrones (`is int`, `is string s`, etc.).
  • Útil en expresiones más complejas con pattern matching.
  • Funciona igual que obj != null, pero es más expresivo en escenarios donde se usan patrones.


obj is { }


Este patrón comprueba que obj no sea null y coincida con un patrón de objeto.

{ } significa un objeto con cualquier valor de propiedades.


if (obj is { })

{

    // obj no es null

}


Cuándo usarlo:

  • Cuando querés usar pattern matching y, además, extraer propiedades en la misma expresión.


Ejemplo:


if (obj is { Nombre: var nombre, Edad: > 18 })

{

    Console.WriteLine($"{nombre} es mayor de edad");

}


¿Cuál es más performante?

obj != null : más rápido, es una simple comparación de referencia.

obj is not null: ligeramente más lento, pero casi idéntico (usa pattern matching internamente).

obj is { } : un poco más costoso porque implica verificar coincidencia de patrón de objeto.


Pero en la práctica, la diferencia de rendimiento es mínima y solo importa en código crítico (hot paths).


También se puede usar el operador null-coalescing (??):


var valor = obj ?? new MiObjeto();


O el null-conditional (?.):


obj?.Metodo();


Pero ese tema es para otro post ... 


lunes, 28 de julio de 2025

Variables de Tipo Restringidas en Elm


En Elm, existe una variante especial de variables de tipo llamadas variables de tipo restringido. El ejemplo más común es el tipo numérico. La función negate la utiliza:


> negate

<function> : number -> number


Normalmente, las variables de tipo se pueden rellenar con cualquier valor, pero number solo se puede rellenar con valores Int y Float. Esto limita las posibilidades.


La lista completa de variables de tipo restringido es:


number permite Int y Float

appendable permite String y List a

comparable permite Int, Float, Char, String y listas/tuplas de valores comparables

compappend permite String y List comparable

Estas variables de tipo restringido existen para que operadores como (+) y (<) sean un poco más flexibles.


miércoles, 23 de julio de 2025

Variables de tipo en elm


A medida que revise más código de Elm, comenzará a ver anotaciones de tipo con letras minúsculas. Un ejemplo común es la función List.length:


> List.length

<función> : Lista a -> Int


¿Observa la a minúscula en el tipo? Esto se denomina variable de tipo. Puede variar según cómo se use List.length:


> List.length [1,1,2,3,5,8]

6 : Int


> List.length [ "a", "b", "c" ]

3 : Int


> List.length [ Verdadero, Falso ]

2 : Int


Solo necesitamos la longitud, así que no importa el contenido de la lista. Por lo tanto, la variable de tipo a indica que podemos coincidir con cualquier tipo. Veamos otro ejemplo común:


> List.reverse

<función>: Lista a -> Lista a


> List.reverse [ "a", "b", "c" ]

["c","b","a"] : List String


> List.reverse [ Verdadero, Falso ]

[Falso, Verdadero] : List Bool


De nuevo, el tipo de variable a puede variar según cómo se use List.reverse. Pero en este caso, tenemos una a en el argumento y en el resultado. Esto significa que si se proporciona un Int de lista, también se debe obtener un Int de lista. Una vez que definimos qué es a, eso es lo que es en todas partes.

Las variables de tipo deben comenzar con una letra minúscula, pero pueden ser palabras completas. Podríamos escribir el tipo de List.length como List value -> Int y podríamos escribir el tipo de List.reverse como List element -> List element. Esto es correcto siempre que comiencen con una letra minúscula. Las variables de tipo a y b se utilizan por convención en muchos lugares, pero algunas anotaciones de tipo se benefician de nombres más específicos.

lunes, 21 de julio de 2025

Anotaciones de tipo en Elm


Hasta ahora, solo hemos dejado que Elm determine los tipos, pero también permite escribir una anotación de tipo en la línea superior a una definición. Así, al escribir código, se pueden escribir cosas como esta:


half: Float -> Float

half n =

n / 2


-- half 256 == 128

-- half "3" -- ¡error!


hypotenuse: Float -> Float -> Float

hypotenuse a b =

sqrt (a^2 + b^2)


-- hypotenuse 3 4 == 5

-- hypotenuse 5 12 == 13


checkPower: Int -> String

checkPower powerLevel =

if powerLevel > 9000 then "¡Es más de 9000!" else "Meh"


-- checkPower 9001 == "¡Es más de 9000!"

-- checkPower True -- ¡error! 


Añadir anotaciones de tipo no es obligatorio, ¡pero sí muy recomendable! 


Beneficios:

  • Calidad del mensaje de error: Al añadir una anotación de tipo, se le indica al compilador lo que se intenta hacer. La implementación puede tener errores, y ahora el compilador puede compararla con la intención establecida. "¡Dijiste que el argumento powerLevel era un Int, pero se está usando como String!".
  • Documentación: Al revisar el código posteriormente (o cuando un compañero lo consulta por primera vez), puede ser muy útil ver exactamente qué entra y sale de la función sin tener que leer la implementación con mucho cuidado.

Sin embargo, se pueden cometer errores con las anotaciones de tipo, así que ¿qué ocurre si la anotación no coincide con la implementación? El compilador determina todos los tipos por sí solo y comprueba que la anotación coincida con la respuesta real. En otras palabras, el compilador siempre verificará que todas las anotaciones que se añadan sean correctas. Así, se obtienen mejores mensajes de error y la documentación se mantiene siempre actualizada.

martes, 15 de julio de 2025

Operador |> en Elm


Elm también tiene un operador de canalización que se basa en la aplicación parcial. Por ejemplo, supongamos que tenemos una función de sanitización para convertir la entrada del usuario en enteros:


sanitize : String -> Maybe Int

sanitize input =

String.toInt (String.trim input)


Podemos reescribirlo así:


sanitize : String -> Maybe Int

sanitize input =

input

|> String.trim

|> String.toInt


En esta "canalización", pasamos la entrada a String.trim y luego esta a String.toInt.


Esto es útil porque permite una lectura de izquierda a derecha que a muchos les gusta. Cuando tienes tres o cuatro pasos, el código suele ser más claro si se desglosa una función auxiliar de nivel superior. Ahora la transformación tiene un nombre. Los argumentos tienen nombre. Tiene una anotación de tipo. De esta manera, se autodocumenta mucho mejor, ¡y tus compañeros de equipo y tu yo futuro lo agradecerán! Probar la lógica también se vuelve más fácil.

lunes, 14 de julio de 2025

Tipos de funciones en Elm


Veamos el tipo de algunas funciones:


> String.length

<función> : String -> Int


La función String.length es de tipo String -> Int. Esto significa que debe aceptar un argumento String y,  devolverá un valor Int. Intentemos asignarle un argumento:


> String.length "Supercalifragilisticexpialidocious"

34 : Int


Empezamos con una función String -> Int y le asignamos un argumento String. Esto da como resultado un valor Int.

Las funciones que aceptan varios argumentos terminan teniendo cada vez más flechas. Por ejemplo, aquí hay una función que toma dos argumentos:


> String.repeat

<función> : Int -> String -> String


Dar dos argumentos como String.repeat 3 "ja" producirá "jajaja". Parece que "->" es una forma extraña de separar argumentos, pero esto tiene una razón.  

Al revisar paquetes como elm/core y elm/html, seguramente verá funciones con múltiples flechas. Por ejemplo:


String.repeat: Int -> String -> String

String.join: String -> List String -> String

¿Por qué tantas flechas? ¿Qué está pasando?


Se vuelve más claro al ver todos 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 toma un entero y luego produce otra función. Veamos esto en acción:


> String.repeat

<función> : Int -> String -> String


> String.repeat 4

<función> : String -> String


> String.repeat 4 "ha"

"hahahaha" : String


> String.join

<función> : String -> List String -> String


> String.join "|"

<función> : List String -> String


> String.join "|" ["rojo","amarillo","verde"]

"rojo|amarillo|verde" : String


Conceptualmente, cada función acepta un argumento. Puede devolver otra función que acepte un argumento, etc. En algún momento, dejará de devolver funciones.

Siempre podríamos poner los paréntesis para indicar que esto es lo que realmente sucede, pero empieza a ser bastante complicado cuando se tienen varios argumentos. Es la misma lógica detrás de escribir 4 * 2 + 5 * 3 en lugar de (4 * 2) + (5 * 3). Esto significa que hay algo más que aprender, pero es tan común que vale la pena.

Bien, pero ¿cuál es el propósito de esta función en primer lugar? ¿Por qué no usar (Int, String) -> String y proporcionar todos los argumentos a la vez?


Es bastante común usar la función List.map en programas Elm:


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


Toma dos argumentos: una función y una lista. A partir de ahí, transforma cada elemento de la lista con esa función. Aquí hay algunos ejemplos:


List.map String.reverse ["part","are"] == ["trap","era"]

List.map String.length ["part","are"] == [4,3]


¿Recuerdas cómo String.repeat 4 tenía el tipo String -> String por sí solo? Bueno, eso significa que podemos decir:


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

La expresión (String.repeat 2) es una función String -> String, por lo que podemos usarla directamente. No es necesario decir (\str -> String.repeat 2 str).

Elm también utiliza la convención de que la estructura de datos siempre es el último argumento en todo el ecosistema. Esto significa que las funciones suelen diseñarse teniendo en cuenta este posible uso, lo que la convierte en una técnica bastante común.

Es importante recordar que esto puede abusar. A veces es conveniente y claro, pero creo que es mejor usarlo con moderación. Por eso, siempre recomiendo desglosar las funciones auxiliares de alto nivel cuando las cosas se complican un poco. De esta manera, tiene un nombre claro, los argumentos tienen nombre y es fácil probar esta nueva función auxiliar. En nuestro ejemplo, esto significa crear:


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


reduplicar : String -> String

reduplicar string =

String.repeat 2 string


Este caso es muy simple, pero (1) ahora está más claro que me interesa el fenómeno lingüístico conocido como reduplicación y (2) será bastante fácil añadir nueva lógica a la reduplicación a medida que mi programa evolucione. 

En otras palabras, si su aplicación parcial se está haciendo larga, conviértala en una función auxiliar. Y si es multilínea, ¡definitivamente debería convertirse en una función auxiliar de alto nivel! Este consejo también aplica al uso de funciones anónimas.