Translate

viernes, 22 de agosto de 2025

Maybe en Elm


A medida que trabajemos más con Elm, veremos el tipo Maybe con bastante frecuencia. Se define así:


type Maybe a

  = Just a

  | Nothing


-- Just 3.14 : Maybe Float

-- Just "hi" : Maybe String

-- Just True : Maybe Bool

-- Nothing   : Maybe a


Este tipo tiene dos variantes: Nothing o Just a value. La variable de tipo permite tener Maybe Float y Maybe String según el valor.

Esto puede ser útil en dos escenarios principales: funciones parciales y campos opcionales.

A veces se necesita una función que responda a algunas entradas, pero no a otras. Mucha gente se encuentra con esto con String.toFloat al intentar convertir la entrada del usuario en números. Veámoslo en acción:


> String.toFloat

<function> : String -> Maybe Float


> String.toFloat "3.1415"

Just 3.1415 : Maybe Float


> String.toFloat "abc"

Nothing : Maybe Float


No todas las cadenas tienen sentido como números, así que esta función lo modela explícitamente. ¿Se puede convertir una cadena en un valor de punto flotante? ¡Quizás! A partir de ahí, podemos hacer una coincidencia de patrones con los datos resultantes y continuar según corresponda.

Otro lugar donde se ven comúnmente valores "Maybe" es en registros con campos opcionales.

Por ejemplo, supongamos que gestionamos una red social. Conectamos personas, buscamos amistad, etc. Ya conoces la explicación. The Onion describió nuestros verdaderos objetivos en 2011: extraer la mayor cantidad de datos posible para la CIA. Y si queremos todos los datos, debemos facilitar el acceso a los usuarios. Permitir que los añadan más adelante. Añadir funciones que los animen a compartir más y más información con el tiempo.

Comencemos con un modelo simple de usuario. Debe tener un nombre, pero la edad será opcional.


type alias User =

  { name : String

  , age : Maybe Int

  }


Ahora digamos que Sue crea una cuenta, pero decide no proporcionar su fecha de nacimiento:


sue : User

sue =

  { name = "Sue", age = Nothing }


Sin embargo, los amigos de Sue no pueden desearle un feliz cumpleaños. Me pregunto si realmente les importa... Más tarde, Tom crea un perfil y sí proporciona su edad:


tom : User

tom =

  { name = "Tom", age = Just 24 }


Genial, eso será genial en su cumpleaños. Pero lo más importante es que Tom forma parte de un grupo demográfico valioso. Los anunciantes estarán encantados.

Bien, ahora que tenemos algunos usuarios, ¿cómo podemos promocionarles alcohol sin infringir ninguna ley? Probablemente se enfadarían si lo hiciéramos para menores de 21 años, así que vamos a comprobarlo:


canBuyAlcohol : User -> Bool

canBuyAlcohol user =

  case user.age of

    Nothing ->

      False


    Just age ->

      age >= 21


Observa que el tipo Maybe nos obliga a realizar una coincidencia de patrones con la edad del usuario. De hecho, es imposible escribir código olvidando que los usuarios pueden no tener edad. ¡Elm se encarga de ello! Ahora podemos anunciar alcohol con la seguridad de que no estamos influyendo directamente en menores. Solo en sus compañeros mayores.


martes, 19 de agosto de 2025

Manejo de errores en Elm


Una de las garantías de Elm es que no se observarán errores de ejecución en la práctica. Esto se debe, en parte, a que Elm trata los errores como datos. En lugar de bloquearse, modelamos explícitamente la posibilidad de fallo con tipos personalizados. Por ejemplo, supongamos que desea convertir la entrada del usuario en una edad. Podría crear un tipo personalizado como este:


type MaybeAge

= Age Int

| InvalidInput


toAge : String -> MaybeAge

toAge userInput =

...


-- toAge "24" == Age 24

-- toAge "99" == Age 99

-- toAge "ZZ" == InvalidInput


Independientemente de la entrada que se proporcione a la función toAge, siempre produce un valor. Una entrada válida produce valores como Age 24 y Age 99, mientras que una entrada no válida produce el valor InvalidInput. A partir de ahí, utilizamos la coincidencia de patrones para garantizar que se consideren ambas posibilidades. ¡Sin bloqueos!

¡Este tipo de problemas surgen constantemente! Por ejemplo, quizás quieras convertir un conjunto de entradas de usuario en una publicación para compartir. Pero, ¿qué ocurre si olvidan añadir un título? ¿O si la publicación no tiene contenido? Podríamos modelar todos estos problemas explícitamente:


type MaybePost

= Post { title : String, content : String }

| NoTitle

| NoContent


toPost : String -> String -> MaybePost

toPost title content =

...


-- toPost "hi" "¿qué tal?" == Post { title = "hi", content = "¿qué tal?" }

-- toPost "" "" == NoTitle

-- toPost "hi" "" == NoContent


En lugar de simplemente indicar que la entrada no es válida, describimos cada una de las posibles causas del error. Si tenemos una función viewPreview : MaybePost -> Html msg para previsualizar publicaciones válidas, ahora podemos mostrar mensajes de error más específicos en el área de vista previa cuando algo falla.

Este tipo de situaciones son extremadamente comunes. Suele ser útil crear un tipo personalizado para cada situación, pero en algunos casos más sencillos, puede usar un tipo estándar. 



Inner Classes en Java vs Nested Classes en C#


En muchos lenguajes podemos anidar clases dentro de otras, pero no siempre significan lo mismo ni tienen las mismas capacidades. Veamos la comparación entre Java y C#.

Java tiene Inner Classes. Cuando declaramos una clase dentro de otra, tenemos dos opciones principales:

Static nested class: funciona como una clase estática, no tiene referencia a la instancia externa.

Inner class (no estática): mantiene una referencia implícita a la instancia de la clase que la contiene.

Esto permite escribir cosas como:


class Contenedora {

    private int valor = 42;


    class Inner {

        void mostrar() {

            // Acceso implícito a la instancia externa

            System.out.println("Valor: " + Contenedora.this.valor);

        }

    }


    public static void main(String[] args) {

        Contenedora c = new Contenedora();

        Contenedora.Inner i = c.new Inner();

        i.mostrar(); // Valor: 42

    }

}


Aquí, Contenedora.this es una referencia implícita que permite acceder directamente a la instancia de la clase externa.


En C# tenemos Nested Classes. En C#, todas las clases anidadas son conceptualmente clases estáticas (aunque no las declares static).

Esto significa que no existe referencia implícita a la instancia externa.


Ejemplo equivalente en C#:


public class Contenedora

{

    private int valor = 42;


    public class Nested

    {

        // No existe "Contenedora.this"

        public void Mostrar(Contenedora externa)

        {

            Console.WriteLine($"Valor: {externa.valor}");

        }

    }


    public static void Main()

    {

        var c = new Contenedora();

        var n = new Nested();

        n.Mostrar(c); // Valor: 42

    }

}


Si querés acceder a los atributos de la clase externa, tenés que pasar explícitamente la referencia (Contenedora externa en este caso).

Entonces, Java Inner Class:

  • Tiene un puntero implícito a la instancia externa.
  • Podés usar Outer.this.atributo.
  • Muy útil en callbacks o cuando la clase interna depende fuertemente de la externa.


C# Nested Class:

  • No tiene referencia implícita.
  • No existe Outer.this.
  • Son más “independientes”, como una clase estática que solo vive en el scope de otra.


¿Por qué esta diferencia?

  • Java diseñó las inner classes como un mecanismo para trabajar con GUI y callbacks (ejemplo clásico: listeners en Swing). Por eso necesitan la referencia a la instancia externa.
  • C# tomó otro camino: sus delegates y lambdas con closures resuelven ese escenario sin necesidad de que las nested classes tengan referencia implícita.



viernes, 15 de agosto de 2025

Pattern Matching en elm


Aprendimos a crear tipos personalizados con la palabra clave type. Nuestro ejemplo principal fue un usuario en una sala de chat:


type User

  = Regular String Int

  | Visitor String


Los usuarios habituales tienen nombre y edad, mientras que los visitantes solo tienen nombre. Ya tenemos nuestro tipo personalizado, pero ¿cómo lo usamos?

Supongamos que queremos una función toName que decida el nombre que se mostrará para cada usuario. Necesitamos usar una expresión case:


toName : User -> String

toName user =

  case user of

    Regular name age ->

      name

    Visitor name ->

      name


-- toName (Regular "Thomas" 44) == "Thomas"

-- toName (Visitor "kate95")    == "kate95"


La expresión case nos permite ramificar según la variante que veamos, así que, independientemente de si vemos a Thomas o a Kate, siempre sabremos cómo mostrar su nombre.

Y si probamos argumentos inválidos como toName (Visitar "kate95") o toName Anonymous, el compilador nos lo notifica inmediatamente. Esto significa que muchos errores simples se pueden corregir en segundos, en lugar de informar a los usuarios y consumir mucho más tiempo.

La función toName que acabamos de definir funciona de maravilla, pero ¿se da cuenta de que la edad no se utiliza en la implementación? Cuando algunos de los datos asociados no se utilizan, es habitual usar un comodín en lugar de asignarles un nombre:


toName : User -> String

toName user =

  case user of

    Regular name _ ->

      name

    Visitor name ->

      name


El _ reconoce los datos, pero también indica explícitamente que nadie los está utilizando.



martes, 12 de agosto de 2025

Sobrecarga de métodos en TypeScript: limitaciones y soluciones


A diferencia de lenguajes como Java o C#, TypeScript sí permite declarar sobrecargas… pero con una particularidad: solo podemos implementar un único método que soporte todas las variantes declaradas.

Esto significa que no podemos tener múltiples implementaciones distintas como en otros lenguajes, sino que debemos manejar la lógica dentro de un único cuerpo de función.

Supongamos que queremos una función procesar que:

  • Si recibe un string, lo devuelva en mayúsculas.
  • Si recibe un number, devuelva su cuadrado.
  • Si recibe dos number, devuelva su suma.


En Java o C# esto serían tres métodos distintos.

En TypeScript lo declaramos así:


class Procesador {

  procesar(dato: string): string;

  procesar(dato: number): number;

  procesar(a: number, b: number): number;


  // Implementación única obligatoria

  procesar(arg1: string | number, arg2?: number): string | number {

    if (typeof arg1 === 'string') {

      return arg1.toUpperCase();

    }

    if (typeof arg1 === 'number' && typeof arg2 === 'number') {

      return arg1 + arg2;

    }

    if (typeof arg1 === 'number') {

      return arg1 * arg1;

    }

    throw new Error('Parámetros no soportados');

  }

}


const p = new Procesador();

console.log(p.procesar('hola'));    // "HOLA"

console.log(p.procesar(5));         // 25

console.log(p.procesar(3, 4));      // 7


En otros lenguajes, cada sobrecarga tiene su propio cuerpo.

En TypeScript, todas las sobrecargas deben compartir una única implementación que maneje la lógica de todos los casos.

¿Cómo simular implementaciones separadas?  Una opción para acercarnos al comportamiento tradicional es delegar la lógica a métodos privados:


class ProcesadorSeparado {

  procesar(dato: string): string;

  procesar(dato: number): number;

  procesar(a: number, b: number): number;


  procesar(arg1: string | number, arg2?: number): string | number {

    if (typeof arg1 === 'string') return this.procesarString(arg1);

    if (typeof arg1 === 'number' && typeof arg2 === 'number') return this.procesarDosNumeros(arg1, arg2);

    if (typeof arg1 === 'number') return this.procesarUnNumero(arg1);

    throw new Error('Parámetros no soportados');

  }


  private procesarString(valor: string): string {

    return valor.toUpperCase();

  }


  private procesarUnNumero(valor: number): number {

    return valor * valor;

  }


  private procesarDosNumeros(a: number, b: number): number {

    return a + b;

  }

}


const p2 = new ProcesadorSeparado();

console.log(p2.procesar('hola')); // "HOLA"

console.log(p2.procesar(5));      // 25

console.log(p2.procesar(3, 4));   // 7



Esto no elimina la restricción del lenguaje, pero hace que el código sea más limpio y cercano al estilo de sobrecarga en otros lenguajes.


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.