Translate

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.

domingo, 13 de julio de 2025

Mapeo de Herencia en JPA


JPA ofrece 3 estrategias principales para mapear la herencia :


@Inheritance(strategy = SINGLE_TABLE) : Todo se guarda en una sola tabla con una columna discriminadora. 

@Inheritance(strategy = JOINED): Se usa una tabla por clase, relacionadas por claves foráneas.        

@Inheritance(strategy = TABLE_PER_CLASS): Una tabla para cada clase, sin relaciones.                           


Veamos un ejemplo usando la estrategia: SINGLE_TABLE.



package com.ejemplo.demo.entidad;

import jakarta.persistence.*;


@Entity

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

@DiscriminatorColumn(name = "tipo_empleado", discriminatorType = DiscriminatorType.STRING)

public abstract class Empleado {


    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;


    private String nombre;

    // Getters y setters

}


@Inheritance(...): Define la estrategia de herencia.

@DiscriminatorColumn(...):  Crea una columna adicional (tipo_empleado) que almacena el tipo real de cada instancia.


📄 Gerente.java

package com.ejemplo.demo.entidad;


import jakarta.persistence.*;


@Entity

@DiscriminatorValue("GERENTE")

public class Gerente extends Empleado {


    private String departamento;

    // Getters y setters

}


📄 Desarrollador.java


package com.ejemplo.demo.entidad;


import jakarta.persistence.*;


@Entity

@DiscriminatorValue("DESARROLLADOR")

public class Desarrollador extends Empleado {


    private String lenguajeFavorito;

    // Getters y setters

}


Se crea una única tabla empleado, con las columnas:

id, nombre, departamento, lenguajeFavorito y tipo_empleado (con valores como GERENTE, DESARROLLADOR)


Ventajas de SINGLE_TABLE

    • Más simple y rápida para consultas.

    • Ideal si la mayoría de los campos son comunes.


Desventajas

    • Muchos campos nulos en columnas que sólo usa una subclase.


Otras estrategias: 

    • JOINED: separa en tablas, y junta usando claves foráneas. Útil si los datos son muy distintos y no querés campos nulos.

    • TABLE_PER_CLASS: evita JOINs, y es util cuando no necesitas hacer consultas a nivel de clase padre. 


En conclusión SINGLE_TABLE lo usamos cuando tenemos herencia la cual las clases hijas no agregan muchos o ningun dato. JOINED cuando queremos hacer consultas a nivel clase padre y tenemos muchos datos diferentes y TABLE_PER_CLASS cuando no necesitamos hacer query a nivel clase padre. 

sábado, 12 de julio de 2025

Tipos en Elm parte 2


Seguimos con los tipos de datos en ELM. Ingresemos algunas expresiones simples y veamos qué sucede:


> "hello"

"hello" : String


> not True

False : Bool


> round 3.1415

3 : Int



Bien, pero ¿qué ocurre exactamente? Cada entrada muestra el valor y su tipo. Puedes leer estos ejemplos en voz alta así:

  • El valor "hello" es una cadena.
  • El valor "False" es un booleano.
  • El valor 3 es un entero.
  • El valor 3.1415 es un float.
¡Elm puede determinar el tipo de cualquier valor que introduzcas! Veamos qué ocurre con las listas:

> [ "Alice", "Bob" ]
["Alice","Bob"] : List String

> [ 1.0, 8.6, 42.1 ]
[1.0,8.6,42.1] : List Float

Estos tipos se pueden interpretar como:
  1. Tenemos una lista con valores de cadena.
  2. Tenemos una lista con valores de coma flotante.
El tipo es una descripción aproximada del valor específico que estamos analizando.

lunes, 7 de julio de 2025

Mapeo de Herencia en JPA


JPA ofrece 3 estrategias para mapear herencia:

  • @Inheritance(strategy = SINGLE_TABLE) : Todo se guarda en una sola tabla con una columna discriminadora. 
  • @Inheritance(strategy = JOINED): Se usa una tabla por clase, relacionadas por claves foráneas.        
  • @Inheritance(strategy = TABLE_PER_CLASS): Una tabla para cada clase, sin relaciones.                           

Veamos un ejemplo: Clase base y subclases

Vamos a usar la estrategia: SINGLE_TABLE.


package com.ejemplo.demo.entidad;

import jakarta.persistence.*;


@Entity

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

@DiscriminatorColumn(name = "tipo_empleado", discriminatorType = DiscriminatorType.STRING)

public abstract class Empleado {


    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;

    private String nombre;


    // Getters y setters

}


@Inheritance(...): Define la estrategia de herencia.

@DiscriminatorColumn(...):  Crea una columna adicional (tipo_empleado) que almacena el tipo real de cada instancia.


📄 Gerente.java


package com.ejemplo.demo.entidad;


import jakarta.persistence.*;


@Entity

@DiscriminatorValue("GERENTE")

public class Gerente extends Empleado {


    private String departamento;


    // Getters y setters

}


📄 Desarrollador.java


package com.ejemplo.demo.entidad;


import jakarta.persistence.*;


@Entity

@DiscriminatorValue("DESARROLLADOR")

public class Desarrollador extends Empleado {


    private String lenguajeFavorito;


    // Getters y setters

}


Resultado en la base de datos


Se crea una única tabla empleado, con las columnas:

id, nombre, departamento, lenguajeFavorito y tipo_empleado (con valores como GERENTE, DESARROLLADOR)


Ventajas de SINGLE_TABLE

    • Más simple y rápida para consultas.

    • Ideal si la mayoría de los campos son comunes.

Desventajas

    • Muchos campos nulos en columnas que sólo usa una subclase.


Otras estrategias: 

    • JOINED: separa en tablas, y junta usando claves foráneas. Útil si los datos son muy distintos y no querés campos nulos.

    • TABLE_PER_CLASS: evita JOINs, pero no es tan usada. No permite relaciones polimórficas fácilmente.


domingo, 6 de julio de 2025

Relaciones entre Entidades con JPA


Aprender a modelar relaciones entre tablas en Java usando JPA, como por ejemplo:

  • @OneToMany (uno a muchos)

  • @ManyToOne (muchos a uno)

  • @OneToOne (uno a uno)

  • @ManyToMany (muchos a muchos)


En este ejemplo vamos a modelar una relación simple: Una Persona puede tener varias Direcciones.


package com.ejemplo.demo.entidad;


import jakarta.persistence.*;

import java.util.List;


@Entity

public class Persona {


@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private Long id;

private String nombre;

@OneToMany(mappedBy = "persona", cascade = CascadeType.ALL, orphanRemoval = true)

private List<Direccion> direcciones;


// Constructores, getters y setters

}


Anotaciones clave:

@OneToMany(mappedBy = "persona"): Declara una relación de uno a muchos. Una persona puede tener varias direcciones.

  • mappedBy = "persona": indica que el atributo que mantiene la relación está del otro lado (Direccion.persona).
  • cascade = CascadeType.ALL: al guardar o eliminar una persona, se aplican los cambios también a sus direcciones.
  • orphanRemoval = true: elimina direcciones si se quitan de la lista.



Entidad Direccion:



package com.ejemplo.demo.entidad;


import jakarta.persistence.*;


@Entity

public class Direccion {


@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private Long id;

private String calle;

private String ciudad;


@ManyToOne

@JoinColumn(name = "persona_id")

private Persona persona;


// Constructores, getters y setters

}



Anotaciones clave:

@ManyToOne: Cada dirección pertenece a una sola persona.

@JoinColumn(name = "persona_id"): Crea una columna en la tabla direccion llamada persona_id que actúa como clave foránea a la tabla persona.


Las listas deben ser inicializadas como new ArrayList<>() si se van a usar directamente.

Si no usás orphanRemoval, las direcciones quedan "huérfanas" si quitás una de la lista.


Un pequeño ejemplo para que vayamos incrementando nuestro conocimiento en JPA. 

viernes, 4 de julio de 2025

Tipos en Elm


Una de las principales ventajas de Elm es que, en la práctica, los usuarios no ven errores de ejecución. Esto es posible gracias a que el compilador de Elm puede analizar el código fuente rápidamente para ver cómo fluyen los valores a través del programa. Si un valor se usa de forma inválida, el compilador lo notifica con un mensaje de error intuitivo. Esto se denomina inferencia de tipos. El compilador determina qué tipo de valores entran y salen de todas las funciones.

El siguiente código define una función toFullName que extrae el nombre completo de una persona como una cadena:


toFullName person =

  person.firstName ++ " " ++ person.lastName


fullName =

  toFullName { fistName = "Hermann", lastName = "Hesse" }


Al igual que en JavaScript o Python, simplemente escribimos el código sin complicaciones. ¿Ves el error?

En JavaScript, el código equivalente genera "undefined Hesse". ¡Ni siquiera es un error! Con suerte, algún usuario te lo dirá cuando lo vea en acción. En cambio, el compilador de Elm simplemente revisa el código fuente y te dice:

-- TYPE MISMATCH ---------------------------------------------------------------

The argument to function `toFullName` is causing a mismatch.

6│   toFullName { fistName = "Hermann", lastName = "Hesse" }
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Function `toFullName` is expecting the argument to be:

    { …, firstName : … }

But it is:

    { …, fistName : … }

Hint: I compared the record fields and found some potential typos.

    firstName <-> fistName


Detecta que toFullName recibe el tipo de argumento incorrecto. Como indica la pista del mensaje de error, alguien escribió "fist" por error en lugar de "first".

Es fantástico tener un asistente para errores sencillos como este, pero es aún más valioso cuando tienes cientos de archivos y muchos colaboradores realizando cambios. No importa cuán grandes y complejas sean las cosas, el compilador de Elm comprueba que todo encaje correctamente basándose únicamente en el código fuente.

Cuanto mejor comprendas los tipos, más fácil te resultará el compilador. ¡Así que empecemos a aprender más!

miércoles, 2 de julio de 2025

¿Qué es Spring Data JPA? Parte 2

 


Veamos un ejemplo de entidad de jpa: 

📄 Persona.java


package com.ejemplo.demo.entidad;

import jakarta.persistence.*;


@Entity

public class Persona {

    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;

    private String nombre;

    private String email;


    // Constructores

    public Persona() {}

    public Persona(String nombre, String email) {

        this.nombre = nombre;

        this.email = email;

    }


    // Getters y setters

    public Long getId() { return id; }

    public String getNombre() { return nombre; }

    public void setNombre(String nombre) { this.nombre = nombre; }

    public String getEmail() { return email; }

    public void setEmail(String email) { this.email = email; }

}


@Entity: Marca esta clase como una entidad JPA, lo que significa que será persistida en una tabla de la base de datos. El nombre de la tabla por defecto será el mismo que el nombre de la clase (persona en minúsculas).

@Id: Indica que el atributo id es la clave primaria de la entidad. Cada fila tendrá un valor único en esta columna.

@GeneratedValue(strategy = GenerationType.IDENTITY):  Especifica que el valor del campo id será generado automáticamente por la base de datos (usualmente como autoincremental).  Existen otras estrategias (AUTO, SEQUENCE, TABLE), pero IDENTITY es simple y efectiva para bases H2 o MySQL.


Repositorio

📄 PersonaRepository.java


package com.ejemplo.demo.repositorio;

import com.ejemplo.demo.entidad.Persona;

import org.springframework.data.jpa.repository.JpaRepository;


public interface PersonaRepository extends JpaRepository<Persona, Long> {

}


JpaRepository<Persona, Long>:  Es una interfaz de Spring Data que nos proporciona métodos CRUD listos para usar: findAll(), save(), deleteById(), etc.

Persona: el tipo de entidad

Long: el tipo de la clave primaria (id)

No se necesita ninguna anotación porque Spring escanea esta interfaz y genera una implementación automática.


Servicio

📄 PersonaService.java


@Service

public class PersonaService {

    // ...

}


@Service:  Marca esta clase como un componente de servicio, lo que permite que Spring la detecte y gestione como parte del contexto de la aplicación (inyección de dependencias, ciclo de vida, etc.).

Controlador

📄 PersonaController.java


@RestController

@RequestMapping("/personas")

public class PersonaController {

    // ...

}


    • @RestController:  Marca la clase como un controlador REST. Es equivalente a usar @Controller + @ResponseBody para cada método.

    • @RequestMapping("/personas"):  Define la ruta base de la API. Todos los métodos se expondrán bajo /personas.

    • @GetMapping, @PostMapping, @DeleteMapping: Indican qué método HTTP debe usarse para cada operación.

    • @RequestBody:  Indica que los datos del cuerpo del request (formato JSON) deben ser convertidos a un objeto Java.

    • @PathVariable: Extrae una variable de la URL, por ejemplo /personas/1 extrae el id = 1.


Archivo application.properties

spring.datasource.url=jdbc:h2:mem:testdb

spring.jpa.hibernate.ddl-auto=update

spring.h2.console.enabled=true


spring.jpa.hibernate.ddl-auto=update:  Le indica a JPA que genere automáticamente las tablas según nuestras entidades. Si la tabla no existe, la crea.


martes, 1 de julio de 2025

Forms en Elm


Ahora crearemos un formulario básico. Tiene un campo para tu nombre, un campo para tu contraseña y un campo para verificarla. También realizaremos una validación muy sencilla para comprobar si las contraseñas coinciden.

A continuación, incluí el programa completo:


-- Input a user name and password. Make sure the password matches.

--

-- Read how it works:

--   https://guide.elm-lang.org/architecture/forms.html

--


import Browser

import Html exposing (..)

import Html.Attributes exposing (..)

import Html.Events exposing (onInput)


-- MAIN

main =

  Browser.sandbox { init = init, update = update, view = view }


-- MODEL

type alias Model =

  { name : String

  , password : String

  , passwordAgain : String

  }


init : Model

init =

  Model "" "" ""


-- UPDATE

type Msg

  = Name String

  | Password String

  | PasswordAgain String


update : Msg -> Model -> Model

update msg model =

  case msg of

    Name name ->

      { model | name = name }

    Password password ->

      { model | password = password }

    PasswordAgain password ->

      { model | passwordAgain = password }


-- VIEW

view : Model -> Html Msg

view model =

  div []

    [ viewInput "text" "Name" model.name Name

    , viewInput "password" "Password" model.password Password

    , viewInput "password" "Re-enter Password" model.passwordAgain PasswordAgain

    , viewValidation model

    ]


viewInput : String -> String -> String -> (String -> msg) -> Html msg

viewInput t p v toMsg =

  input [ type_ t, placeholder p, value v, onInput toMsg ] []


viewValidation : Model -> Html msg

viewValidation model =

  if model.password == model.passwordAgain then

    div [ style "color" "green" ] [ text "OK" ]

  else

    div [ style "color" "red" ] [ text "Passwords do not match!" ]



Esto es bastante similar a nuestro ejemplo de campo de texto, pero con más campos.

Siempre empiezo calculando el modelo. Sabemos que habrá tres campos de texto, así que sigamos con eso:


type alias Model =

  { name : String

  , password : String

  , passwordAgain : String

  }

Normalmente intento empezar con un modelo mínimo, quizás con un solo campo. Luego intento escribir las funciones de vista y actualización. Esto suele revelar que necesito añadir más elementos a mi modelo. Construir el modelo gradualmente de esta manera significa que puedo tener un programa funcional durante el proceso de desarrollo. Puede que aún no tenga todas las funciones, ¡pero casi las tiene!

A veces se tiene una idea bastante clara de cómo se verá el código básico de actualización. Sabemos que necesitamos poder cambiar nuestros tres campos, por lo que necesitamos mensajes para cada caso.


type Msg

  = Name String

  | Password String

  | PasswordAgain String


Esto significa que nuestra actualización necesita un caso para las tres variantes:


update : Msg -> Model -> Model

update msg model =

  case msg of

    Name name ->

      { model | name = name }

    Password password ->

      { model | password = password }

    PasswordAgain password ->

      { model | passwordAgain = password }


Cada caso utiliza la sintaxis de actualización de registros para garantizar que se transforme el campo correspondiente. Esto es similar al ejemplo anterior, pero con más casos.

Esta función de vista utiliza funciones auxiliares para hacer las cosas un poco más organizadas:


view : Model -> Html Msg

view model =

  div []

    [ viewInput "text" "Name" model.name Name

    , viewInput "password" "Password" model.password Password

    , viewInput "password" "Re-enter Password" model.passwordAgain PasswordAgain

    , viewValidation model

    ]


En ejemplos anteriores, usábamos input y div directamente. ¿Por qué dejamos de hacerlo?

Lo bueno de HTML en Elm es que input y div son funciones normales. Toman una lista de atributos y  una lista de nodos secundarios. Como usamos funciones normales de Elm, ¡tenemos todo el poder de Elm para ayudarnos a crear nuestras vistas! Podemos refactorizar código repetitivo y convertirlo en funciones auxiliares personalizadas. ¡Eso es exactamente lo que estamos haciendo aquí!


Así que nuestra función de vista tiene tres llamadas a viewInput:

viewInput : String -> String -> String -> (String -> msg) -> Html msg

viewInput t p v toMsg =

  input [ type_ t, placeholder p, value v, onInput toMsg ] []


Esto significa que escribir viewInput "text" "Name" "Bill" Name en Elm se convertiría en un valor HTML como <input type="text" placeholder="Name" value="Bill"> al mostrarse en pantalla.


La cuarta entrada es más interesante. Es una llamada a viewValidation:


viewValidation : Model -> Html msg

viewValidation model =

  if model.password == model.passwordAgain then

    div [ style "color" "green" ] [ text "OK" ]

  else

    div [ style "color" "red" ] [ text "Passwords do not match!" ]


Esta función primero compara las dos contraseñas. Si coinciden, se muestra texto verde y un mensaje positivo. Si no coinciden, se muestra texto rojo y un mensaje útil.

Estas funciones auxiliares empiezan a mostrar las ventajas de que nuestra biblioteca HTML sea código Elm normal. Podríamos incluir todo ese código en nuestra vista, pero crear funciones auxiliares es totalmente normal en Elm, incluso en el código de la vista. 

Records en Elm


Un registro puede contener muchos valores, y cada valor está asociado a un nombre.

Aquí se muestra un registro que representa al economista británico John A. Hobson:


> john =

|   { first = "John"

|   , last = "Hobson"

|   , age = 81

|   }

{ age = 81, first = "John", last = "Hobson" }


> john.last

"Hobson"


Hemos definido un registro con tres campos que contienen información sobre el nombre y la edad de John.

También puedes acceder a los campos del registro mediante una función de acceso a campos como esta:


> john = { first = "John", last = "Hobson", age = 81 }

{ age = 81, first = "John", last = "Hobson" }

> .last john

"Hobson"

> List.map .last [john,john,john]

["Hobson","Hobson","Hobson"]

>  


A menudo es útil actualizar valores en un registro:

> john = { first = "John", last = "Hobson", age = 81 }

{ age = 81, first = "John", last = "Hobson" }


> { john | last = "Adams" }

{ age = 81, first = "John", last = "Adams" }


> { john | age = 22 }

{ age = 22, first = "John", last = "Hobson" }


Si quisieras decir estas expresiones en voz alta, dirías algo como: "Quiero una nueva versión de John cuyo apellido sea Adams" o "John cuya edad sea 22".

Ten en cuenta que al actualizar algunos campos de John, creamos un registro completamente nuevo. No sobrescribe el existente. Elm optimiza este proceso compartiendo la mayor cantidad de contenido posible. Si actualizas uno de los diez campos, el nuevo registro compartirá los nueve valores sin cambios.

Una función para actualizar las edades podría verse así:


> celebrateBirthday person =

|   { person | age = person.age + 1 }

<function>

> john = { first = "John", last = "Hobson", age = 81 }

{ age = 81, first = "John", last = "Hobson" }

> celebrateBirthday john

{ age = 82, first = "John", last = "Hobson" }


¿Cuándo usar record en C#?


Desde C# 9, el lenguaje introdujo una nueva palabra clave: record. A simple vista parece una forma alternativa de declarar clases, pero su objetivo es mucho más específico: modelar datos inmutables que se comparan por valor.

Un record es un tipo de referencia que, a diferencia de las clases tradicionales, se compara por valor y está orientado a la inmutabilidad.


public record Person(string Name, int Age);


var p1 = new Person("Ada", 30);

var p2 = new Person("Ada", 30);


Console.WriteLine(p1 == p2); // True


Con clases normales (class), esta comparación daría false, porque se comparan las referencias.


Usá record cuando:

  • Te interesa la comparación por valor: Por ejemplo, para representar coordenadas, dinero, personas, productos, etc.
  • No necesitás mutabilidad. Los record son inmutables por defecto (aunque podés crear record class mutables, no es lo ideal).
  • Estás trabajando con código funcional o DDD. Es perfecto para modelar objetos de valor (value objects).
  • Querés usar with para copiar fácilmente con cambios:


var p3 = p1 with { Age = 31 };


 ¿Cuándo no usar record?

  • Cuando necesitás una identidad mutable (por ejemplo, una entidad con un Id que cambia de estado).
  • Cuando estás trabajando con frameworks como EF Core que esperan clases tradicionales para el mapeo.
  • Cuando necesitás herencia compleja (los record funcionan bien, pero pueden agregar confusión si no se usan con cuidado).


Desde C# 10, también existe record struct: un tipo por valor que se comporta como record, pero como struct.


public readonly record struct Coordinates(int X, int Y);


Ideal para representar datos livianos inmutables, como vectores o puntos.


Si estás modelando datos que no cambian, donde la igualdad semántica importa más que la identidad, y querés un código limpio, expresivo y seguro, record es la herramienta perfecta.


¿Qué es Spring Data JPA?


JPA (Java Persistence API) es una especificación estándar de Java para el mapeo objeto-relacional (ORM), que permite interactuar con bases de datos relacionales utilizando objetos Java.


Con JPA se pueden:

    • Definir clases Java como entidades persistentes (@Entity).

    • Especificar relaciones entre entidades (uno a muchos, muchos a muchos, etc.).

    • Realizar operaciones como insertar, actualizar, eliminar y consultar datos sin escribir SQL explícito.


JPA no es una implementación, sino una interfaz estándar. Las implementaciones más comunes son:

    • Hibernate (la más usada)

    • EclipseLink

    • OpenJPA


Spring Data JPA es un módulo de Spring que simplifica el uso de JPA:

    • Permite definir interfaces de repositorio sin necesidad de implementar los métodos CRUD.

    • Integra la capa de persistencia con el resto del ecosistema de Spring (inyección de dependencias, control transaccional, validaciones, etc.).

    • Facilita la creación de consultas con nombres de método (findByNombre, findByEmailContaining, etc.).

    • Soporta JPQL, SQL nativo y consultas dinámicas.


Spring Data JPA es una abstracción sobre JPA que reduce drásticamente el código necesario para acceder a la base de datos.


Ventajas de usar JPA con Spring

    • Menos código repetitivo: no es necesario implementar manualmente métodos CRUD.

    • Integración total con Spring: todo se gestiona mediante inyección de dependencias y configuración automática.

    • Consultas declarativas: se pueden crear consultas complejas usando el nombre del método.

    • Abstracción del SQL: permite trabajar con objetos sin escribir sentencias SQL directamente.

    • Gestión automática del contexto de persistencia: Spring gestiona transacciones, apertura y cierre de conexiones.

    • Compatible con múltiples bases de datos: MySQL, PostgreSQL, H2, Oracle, SQL Server, etc.


En el siguiente post vamos con algo más práctico. 


sábado, 28 de junio de 2025

El patrón Value Object… y cómo Ruby se lo salta cuando quiere


En el diseño orientado a objetos, un Value Object representa una entidad inmutable, comparada por valor y sin identidad propia (es decir, su identidad es el valor). Es un patrón común en lenguajes como Java, C# y Kotlin. 

Por ejemplo si necesitamos representar una fecha, dinero, fracciones, números complejos, etc. usaremos este patrón. 


Un Value Object tiene tres características esenciales:

  • Inmutabilidad: su estado no cambia después de ser creado.
  • Comparación por valor: dos objetos con los mismos atributos se consideran iguales.
  • Ausencia de identidad propia: no importa quién lo creó ni cuándo, solo importa su valor.


En C# con record, tenés inmutabilidad y comparación por valor automáticamente:


public record Money(decimal Amount, string Currency);


var a = new Money(10, "USD");

var b = new Money(10, "USD");


Console.WriteLine(a == b); // True


Y no podés modificar sus valores.


En Ruby, podés definir una clase que parezca un Value Object:


class Money

  attr_reader :amount, :currency


  def initialize(amount, currency)

    @amount = amount

    @currency = currency

  end


  def ==(other)

    amount == other.amount && currency == other.currency

  end

end


Hasta ahí todo bien, pero... todo es mutable


usd = Money.new(100, "USD")

usd.instance_variable_set(:@amount, 999) # ¡Booom!


Ruby no impide modificar los atributos internos con metaprogramación. Incluso podés cambiar el comportamiento de un único objeto:


usd.define_singleton_method(:amount) { 0 }


Este tipo de cosas rompen completamente la idea de inmutabilidad.

Entonces, ¿Cómo hacer que un VO en Ruby sea más seguro? No hay garantía total, pero hay algunas medidas:


class SafeMoney

  attr_reader :amount, :currency

  def initialize(amount, currency)

    @amount = amount.freeze

    @currency = currency.freeze

    freeze

  end


  def ==(other)

    amount == other.amount && currency == other.currency

  end

end


  • freeze impide modificaciones.
  • freeze también se aplica a los valores internos.
  • El objeto completo se congela con freeze.


Aun así... no es 100% a prueba de balas. Ruby confía en vos.

Ruby es expresivo, flexible y poderoso. Pero esa flexibilidad puede ser peligrosa cuando aplicás patrones pensados para lenguajes más rígidos.

¿Querés un Value Object en Ruby? Podés tener algo parecido, pero tené en cuenta que, la inmutabilidad en Ruby es un acto de fe... y freeze es tu mejor aliado.

Y por ultimo un recuerdo de cuando aprendi Ruby: Antes de Ruby 2.4, existían dos clases distintas para representar enteros: Fixnum (para enteros pequeños) y Bignum (para enteros grandes). Aunque eran objetos y permitían monkey patching, seguían siendo inmutables, y si se intentaba forzar una mutación real, el programa fallaba.


class Fixnum

  def mutate!

    self.replace(99)  # Esto explota

  end

end


5.mutate!

# => Error: can't modify frozen Integer (TypeError)


Incluso si se intentaba hacer algo como modificar self o reemplazar el contenido del número, Ruby lo impedía, ya que los enteros eran internamente inmutables y congelados. Esto confundía a quienes veían métodos con ! en otros objetos como String, donde sí había mutabilidad real.

Desde Ruby 2.4, Fixnum y Bignum se unificaron en Integer, y aunque el comportamiento de inmutabilidad se mantiene, ahora es más claro que los enteros no pueden ni deben ser mutados.

jueves, 26 de junio de 2025

Asignaciones a this en structs en C#


En C#, solemos pensar que this es una entidad inmutable dentro de un objeto: podés usarlo para leer, pero jamás para asignar. Eso es cierto… salvo que estés trabajando con un struct. Y ahí es donde empieza lo interesante.

En clases, intentar escribir this = otraInstancia; es un error de compilación. Pero en structs, esa línea es válida, y en algunos casos, incluso útil.

¿Por qué? Porque un struct es un tipo de valor. Cuando estás dentro de un método de instancia de un struct, this es una referencia a una copia de la instancia, y podés reasignarla.

Veamos un ejemplo básico


struct Punto

{

    public int X;

    public int Y;


    public void Reiniciar()

    {

        this = new Punto(0, 0); // reemplaza toda la instancia

    }

}


Este método Reiniciar reemplaza completamente el contenido del struct por uno nuevo.


¿Y esto para qué sirve? A veces, en vez de ir campo por campo, simplemente querés decir:

Ya fue, esta instancia no me sirve, la reescribo entera.


Por ejemplo:


struct Usuario

{

    public string Nombre;

    public string Email;


    public void Vaciar()

    {

        this = default; // equivalente a new Usuario()

    }

}


C# 9 introdujo record struct, y con eso también podemos usar el patrón with, lo que abre otras posibilidades:


public readonly record struct Coordenada(int X, int Y)

{

    public Coordenada Mover(int dx, int dy)

    {

        return this with { X = this.X + dx, Y = this.Y + dy };

    }

}


¿Y si no querés devolver una nueva instancia? Podés mutar así:


public record struct Contador(int Valor)

{

    public void Incrementar()

    {

        this = this with { Valor = Valor + 1 };

    }

}


  • Esto solo funciona en structs, no en clases.
  • Al reasignar this, perdés todo el estado anterior.
  • No es común en código idiomático de C#, así que usalo con intención y claridad.


La asignación a this en structs es uno de esos detalles de C# que sorprenden. No es algo que debas usar todo el tiempo, pero saber que existe puede ayudarte a escribir código más claro en ciertos contextos.

La próxima vez que necesites reiniciar por completo una instancia de un struct, acordate:

Sí, podés decir this = ... y nadie te lo va a impedir.

martes, 24 de junio de 2025

Text Fields en Elm


Vamos a crear una aplicación sencilla que invierte el contenido de un campo de texto.


import Browser

import Html exposing (Html, Attribute, div, input, text)

import Html.Attributes exposing (..)

import Html.Events exposing (onInput)


-- MAIN

main =

  Browser.sandbox { init = init, update = update, view = view }


-- MODEL

type alias Model =

  { content : String

  }


init : Model

init =

  { content = "" }


-- UPDATE

type Msg

  = Change String


update : Msg -> Model -> Model

update msg model =

  case msg of

    Change newContent ->

      { model | content = newContent }


-- VIEW

view : Model -> Html Msg

view model =

  div []

    [ input [ placeholder "Text to reverse", value model.content, onInput Change ] []

    , div [] [ text (String.reverse model.content) ]

    ]


Este código es una ligera variación del ejemplo anterior. Se configura un modelo. Se definen algunos mensajes. Se indica cómo actualizar. Se crea la vista. La diferencia radica simplemente en cómo se completó este esqueleto. 

Siempre empiezo por adivinar cuál debería ser mi modelo. Sabemos que debemos registrar lo que el usuario escribe en el campo de texto. Necesitamos esa información para saber cómo representar el texto invertido. Así que lo hacemos así:


type alias Model =

  { content : String

  }

Esta vez representamos el modelo como un registro. El registro almacena la entrada del usuario en el campo de contenido.

¿para qué molestarse en tener un registro si solo contiene una entrada? ¿No se podría usar la cadena directamente? ¡Claro! Pero empezar con un registro facilita la adición de más campos a medida que nuestra aplicación se vuelve más compleja. Cuando llegue el momento en que necesitemos dos entradas de texto, tendremos que hacer muchos más ajustes.

Ya tenemos nuestro modelo, así que normalmente procedemos a crear una función de vista:


view : Model -> Html Msg

view model =

  div []

    [ input [ placeholder "Text to reverse", value model.content, onInput Change ] []

    , div [] [ text (String.reverse model.content) ]

    ]


Creamos un <div> con dos hijos. El hijo interesante es el nodo <input>, que tiene tres atributos:

  • placeholder es el texto que se muestra cuando no hay contenido.
  • value es el contenido actual de este <input>.
  • onInput envía mensajes cuando el usuario escribe en este nodo <input>.

Al escribir "bard", se generarían cuatro mensajes:

  1. Change "b"
  2. Change "ba"
  3. Change "bar"
  4. Change "bard"

Estos se incorporarían a nuestra función de actualización.

Solo hay un tipo de mensaje en este programa, por lo que nuestra actualización solo tiene que manejar un caso:


type Msg

  = Change String


update : Msg -> Model -> Model

update msg model =

  case msg of

    Change newContent ->

      { model | content = newContent }


Cuando recibimos un mensaje indicando que el nodo <input> ha cambiado, actualizamos el contenido de nuestro modelo. Por lo tanto, si escribiera "bard", los mensajes resultantes generarían los siguientes modelos:


  1. { content = "b" }
  2. { content = "ba" }
  3. { content = "bar" }
  4. { content = "bard" }

Necesitamos registrar esta información explícitamente en nuestro modelo; de lo contrario, no habría forma de mostrar el texto invertido en nuestra función de vista.