Translate

sábado, 14 de noviembre de 2026

Records en Java: qué podemos hacer y cuáles son sus limitaciones


Los record llegaron en Java 16 para simplificar la creación de clases cuyo objetivo principal es representar datos. Evitan tener que escribir constructores, getters, equals(), hashCode() y toString() manualmente.


Pero ¿son simplemente clases más cortas? No exactamente. Tienen algunas restricciones importantes, especialmente cuando hablamos de herencia.

Supongamos que queremos representar un usuario:


public record Usuario(String nombre, int edad) {

}


Java genera automáticamente:

  • Un constructor con los componentes declarados.
  • Los métodos de acceso nombre() y edad().
  • Las implementaciones de equals(), hashCode() y toString().


Podemos utilizarlo así:


Usuario usuario = new Usuario("Ana", 30);

System.out.println(usuario.nombre());

System.out.println(usuario.edad());

System.out.println(usuario);


Observá que los métodos de acceso no siguen la convención tradicional de JavaBeans: se llaman nombre() y edad(), no getNombre() y getEdad().

Además, los componentes de un record son implícitamente final: no podemos reasignar sus valores después de construir la instancia.

¿Un record puede heredar de otra clase? No. Un record no puede extender una clase definida por nosotros.

En Java, los records heredan implícitamente de java.lang.Record. Por eso, esta declaración no es válida:


class Persona {

    String nombre;

}


record Usuario(String nombre, int edad)

        extends Persona { // Error de compilación

}


Tampoco podemos declarar explícitamente extends Record, porque esa herencia ya está definida por el lenguaje.

Esta restricción permite que los records tengan una estructura de datos bien definida y evita que se introduzca una jerarquía de clases que contradiga su modelo.

Por lo tanto, si necesitamos herencia de implementación entre clases, un record probablemente no sea la herramienta adecuada.

¿Un record puede implementar una interfaz? Sí. Un record puede implementar una o varias interfaces, igual que una clase convencional.


Por ejemplo:


interface Identificable {

    Long id();

}


public record Usuario(Long id, String nombre)

        implements Identificable {

}


El método id() exigido por la interfaz ya existe porque es un accesor generado automáticamente para el componente id.


También podemos implementar interfaces que requieran comportamiento:


interface Saludable {

    boolean estaActivo();

}


public record Usuario(String nombre, boolean activo)

        implements Saludable {


    @Override

    public boolean estaActivo() {

        return activo;

    }

}


Esto resulta especialmente útil cuando queremos aprovechar el polimorfismo sin necesitar herencia de clases.


¿Podemos agregar métodos a un record? Sí. Un record no se limita a almacenar datos: también puede contener lógica de negocio.


public record Rectangulo(double ancho, double alto) {


    public double area() {

        return ancho * alto;

    }

}

Podemos utilizarlo así:


Rectangulo rectangulo = new Rectangulo(5, 3);


System.out.println(rectangulo.area()); // 15.0


También podemos declarar métodos estáticos, constructores adicionales y constructores compactos para validar los datos.


Por ejemplo:


public record Usuario(String nombre, int edad) {


    public Usuario {

        if (nombre == null || nombre.isBlank()) {

            throw new IllegalArgumentException(

                "El nombre es obligatorio"

            );

        }


        if (edad < 0) {

            throw new IllegalArgumentException(

                "La edad no puede ser negativa"

            );

        }

    }

}


El constructor compacto permite validar los componentes sin tener que escribir las asignaciones correspondientes: Java las realiza automáticamente después de ejecutar su cuerpo.


¿Podemos modificar los componentes de un record?

No podemos reasignar directamente sus componentes una vez construida la instancia:


Usuario usuario = new Usuario("Ana", 30);

usuario.nombre = "María"; // Error


Sin embargo, hay una distinción importante: que los componentes sean final no significa que todos los objetos referenciados sean inmutables.


public record Grupo(List<String> integrantes) {

}


En este caso no podemos reasignar integrantes, pero sí podríamos modificar la lista si es mutable:


Grupo grupo = new Grupo(new ArrayList<>());

grupo.integrantes().add("Ana");


Si queremos proteger la colección, podemos realizar una copia defensiva:


public record Grupo(List<String> integrantes) {


    public Grupo {

        integrantes = List.copyOf(integrantes);

    }

}


Así evitamos que modificaciones posteriores sobre la lista original o sobre la lista devuelta alteren el contenido del grupo.


¿Podemos redefinir equals() y otros métodos?

Sí, aunque generalmente no es necesario. Java genera automáticamente equals(), hashCode() y toString() utilizando los componentes del record.

Podemos redefinir estos métodos si tenemos un motivo concreto. También podemos declarar constructores adicionales, siempre que respeten las reglas del lenguaje.

Lo que no podemos hacer es declarar instancias adicionales de campos de estado arbitrarios: los campos de instancia de un record corresponden a sus componentes. Sí podemos declarar campos estáticos.


¿Cuándo conviene usar records? 

Los records son una buena opción para:

  • DTOs utilizados en APIs REST.
  • Valores que se pasan entre capas de una aplicación.
  • Resultados de consultas.
  • Representar coordenadas, importes u otros valores.
  • Modelar datos que deben tener una estructura explícita y estable.


En cambio, una clase convencional puede ser más apropiada cuando necesitamos herencia de implementación, estado mutable o un ciclo de vida complejo.


Los records no son simplemente una forma abreviada de escribir clases. Son una característica del lenguaje diseñada para representar datos con una estructura explícita.

Podemos agregarles comportamiento e implementar interfaces, pero no podemos extender otras clases ni modificar la referencia de sus componentes.

lunes, 9 de noviembre de 2026

El 93% de los profesionales latinos afirman conocer o trabajar con Agentes de IA

Más resultados, menos instrucciones con la IA

miércoles, 4 de noviembre de 2026

Scala 3: entendiendo los implícitos desde cero parte 2


Ya sabemos que es implícitos ahora veamos un ejemplo más realista: dar formato a los precios.

Utilizar un String como idioma sirve para entender el mecanismo, pero no es una buena práctica para representar una configuración importante. Podríamos confundir el idioma con cualquier otra cadena de texto.


Vamos a crear un tipo específico.


case class Moneda(simbolo: String)

given monedaArgentina: Moneda = Moneda("$")


def mostrarPrecio(precio: Double)(using moneda: Moneda): String =

  s"${moneda.simbolo}$precio"


¿Qué significa cada parte?

case class Moneda define un tipo de dato para representar una moneda.

simbolo es el símbolo que queremos mostrar.

given monedaArgentina declara una moneda disponible para el compilador.

using moneda: Moneda indica que la función necesita una moneda para trabajar.


Ahora podemos ejecutar:


mostrarPrecio(1500)

mostrarPrecio(2500)


El resultado será:

$1500.0

$2500.0


La función no necesita recibir la moneda en cada llamada. Scala la encuentra en el contexto.

Este ejemplo es deliberadamente sencillo: en una aplicación real, el formato de los precios también debería contemplar separadores decimales, miles y otras reglas regionales.


¿Qué ocurre si tenemos dos monedas?

Supongamos que nuestra aplicación necesita trabajar tanto con pesos argentinos como con euros.


Podríamos declarar:

given pesos: Moneda = Moneda("$")

given euros: Moneda = Moneda("€")


Ahora tenemos dos valores del mismo tipo.


Si llamamos a:

mostrarPrecio(1500)


el compilador no tiene una única moneda que pueda elegir. Ambas coinciden con el tipo que necesita la función.

En este caso, la resolución del parámetro contextual es ambigua y el programa no compila.

¿Cómo lo resolvemos?

Podemos indicar explícitamente cuál queremos utilizar:


mostrarPrecio(1500)(using pesos)

mostrarPrecio(1500)(using euros)


También podemos organizar el código para que cada contexto tenga disponible solamente la moneda que corresponde.

La lección es importante: given no significa que Scala elige cualquier valor. El compilador debe encontrar un valor adecuado y una resolución válida.


A veces queremos pedirle al compilador que encuentre un valor contextual, sin estar dentro de una función que tenga un parámetro using.


Para eso existe summon.


case class Moneda(simbolo: String)

given monedaArgentina: Moneda = Moneda("$")

val monedaActual: Moneda = summon[Moneda]


println(monedaActual.simbolo)


El resultado es: $

La expresión: summon[Moneda] significa, aproximadamente: «Compilador, encontrá un valor contextual de tipo Moneda».


Si no existe un valor apropiado, el programa no compila.

No hay que confundir summon con una búsqueda que ocurre en cualquier momento mientras el programa se ejecuta. La resolución de estos valores es principalmente una tarea del compilador.


Hasta ahora vimos cómo proporcionar una configuración, pero los parámetros contextuales también permiten construir abstracciones más generales.

Imaginemos que queremos imprimir distintos tipos de valores. Para algunos tipos sabemos cómo convertirlos en texto; para otros, todavía no.


Podemos definir una abstracción:


trait Mostrar[A]:

  def mostrar(valor: A): String


Esta definición dice que Mostrar[A] representa una forma de mostrar valores de tipo A como texto.

Por ejemplo, podemos proporcionar una implementación para los enteros:


given mostrarEnteros: Mostrar[Int] with

  def mostrar(valor: Int): String =

    s"El número es $valor"


Y otra para las cadenas:


given mostrarTextos: Mostrar[String] with

  def mostrar(valor: String): String =

    s"El texto es '$valor'"


Ahora podemos definir una función genérica:


def imprimir[A](valor: A)(using m: Mostrar[A]): Unit =

  println(m.mostrar(valor))


¿Qué sucede cuando llamamos a la función?


imprimir(42)

imprimir("Hola")


En el primer caso, Scala busca un Mostrar[Int].

En el segundo, busca un Mostrar[String].


Como hemos declarado ambas implementaciones mediante given, el compilador puede seleccionar la correspondiente al tipo del argumento.


El resultado es:

El número es 42

El texto es 'Hola'


Este ejemplo muestra una de las ventajas más importantes del mecanismo: podemos definir una función general sin conocer de antemano cómo se representa cada tipo.

Las implementaciones concretas se proporcionan por separado y el compilador las conecta.

El ejemplo anterior es una forma de implementar una type class.


Una type class permite definir una capacidad que puede tener un tipo sin necesidad de modificar la definición original de ese tipo.


Por ejemplo, podemos definir cómo mostrar un entero y cómo mostrar una cadena, aunque Int y String sean tipos que no hemos creado nosotros.


La estructura es:

1. Definimos una abstracción, como Mostrar[A].

2. Proporcionamos implementaciones para los tipos que nos interesan mediante given.

3. Escribimos funciones que solicitan esa capacidad mediante using.

4. El compilador encuentra la implementación apropiada.


Esta idea es muy utilizada en Scala para implementar operaciones genéricas, comparaciones, serialización, conversión de datos y muchas otras abstracciones.


Scala 3 introdujo una sintaxis más específica para implicitos :

  • given declara instancias o valores contextuales.
  • using declara parámetros contextuales.
  • summon permite solicitar un valor contextual.
  • Conversion permite definir conversiones entre tipos.
  • extension permite agregar métodos de extensión a tipos existentes.


Los implícitos de Scala permiten que el compilador encuentre valores y comportamientos que una función necesita, sin que debamos pasarlos explícitamente en cada llamada.

domingo, 1 de noviembre de 2026

Scala 3: entendiendo los implícitos desde cero


Scala tiene una característica muy interesante: permite que el compilador encuentre ciertos valores o funcionalidades sin que tengamos que escribirlos explícitamente cada vez que los necesitamos.

Esta característica se conoce tradicionalmente como implicits (implícitos).

En Scala 3, los mecanismos principales para trabajar con implícitos son given y using. También existen otras herramientas relacionadas, como las conversiones automáticas mediante Conversion y las extensiones mediante extension.

Pero antes de hablar de palabras reservadas, vamos a entender el problema que intentan resolver.

Imaginemos que estamos desarrollando un programa que saluda a las personas.

Podríamos escribir una función como esta:


def saludar(nombre: String, idioma: String): String =

  if idioma == "es"

    then s"Hola, $nombre"

    else s"Hello, $nombre"


Para saludar a una persona, debemos indicar su nombre y el idioma:


saludar("Ana", "es")

saludar("Juan", "en")


Funciona perfectamente.

Sin embargo, imaginemos que toda nuestra aplicación está en español. Cada vez que llamamos a saludar, debemos repetir "es".


¿Qué pasaría si pudiéramos indicarle al programa que, por defecto, el idioma es español y que la función lo utilice automáticamente?

En Scala 3 podemos hacerlo mediante un parámetro contextual.


Veamos una implementación:


given idioma: String = "es"


def saludar(nombre: String)(using idioma: String): String =

  if idioma == "es"

    then s"Hola, $nombre"

    else s"Hello, $nombre"


Hay dos conceptos nuevos.

given: declarar un valor disponible para ser encontrado por el compilador.


given idioma: String = "es"


Estamos declarando un valor de tipo String que el compilador puede utilizar cuando necesite encontrar un valor de ese tipo en un contexto compatible.


using: declarar un parámetro que el compilador debe buscar.


def saludar(nombre: String)(using idioma: String): String =

  ...



La función necesita dos datos: el nombre y el idioma. El nombre se pasa normalmente, mientras que el idioma es un parámetro contextual.


Ahora podemos escribir:


saludar("Ana")

saludar("Juan")


El compilador encuentra el given de tipo String y lo utiliza para completar el parámetro using.


El resultado es:

Hola, Ana

Hola, Juan


¡No necesitamos escribir el idioma en cada llamada!

Es importante entender algo: el idioma no aparece mágicamente. Lo declaramos nosotros con given. Lo que hace Scala es encontrarlo y pasarlo automáticamente cuando corresponde.


¿Qué hace realmente el compilador?

Podemos pensar que el compilador completa la llamada a la función.


Nosotros escribimos:

saludar("Ana")


Conceptualmente, el compilador resuelve esa llamada como si hubiéramos escrito:

saludar("Ana")(using idioma)


No es necesario escribir esa segunda forma cuando el compilador puede encontrar el valor contextual adecuado.


Esta característica se conoce como contextual parameters, o parámetros contextuales.


El mecanismo permite separar dos responsabilidades:

  • La función declara qué información necesita.
  • El contexto proporciona esa información.


La función no necesita saber dónde se definió el idioma. Solo necesita recibir un String.


miércoles, 28 de octubre de 2026

[Infografía] 4 prioridades de infraestructura para escalar la IA de agentes

 

Me llegó este correo y lo quiero compartir : 

miércoles, 21 de octubre de 2026

Terraform avanzado: dynamic blocks y dependencias entre recursos


En los posts anteriores empezamos con los conceptos básicos de Terraform y fuimos aumentando la complejidad.

Ahora podemos empezar a enfrentarnos con un problema que aparece constantemente en infraestructura real.


Un resource puede contener bloques de configuración repetitivos.


Por ejemplo, imaginemos una regla de firewall:

regla 1 → puerto 80

regla 2 → puerto 443

regla 3 → puerto 8080


Podríamos escribir cada bloque manualmente.

Pero si las reglas vienen de una variable, ¿cómo hacemos para generar esos bloques dinámicamente?

Ahí aparece: dynamic

Un dynamic block permite generar bloques de configuración repetidos dentro de un resource.

La idea es parecida a un for_each, pero hay una diferencia fundamental.

Con for_each podemos crear resources:


resource "..." "..." {

  for_each = ...

}


Con dynamic podemos generar nested blocks dentro de un resource.


Conceptualmente:


for_each

   │

   └── crea resources


dynamic

   │

   └── crea bloques dentro de un resource


Supongamos que un resource tuviera una estructura como:


resource "example" "server" {

  rule {

    port = 80

  }


  rule {

    port = 443

  }

}


Tenemos dos bloques:


server

 ├── rule

 └── rule


Ahora supongamos que los puertos vienen de una variable:


variable "ports" {

  default = [80, 443, 8080]

}


Queremos generar:


rule {

  port = 80

}


rule {

  port = 443

}


rule {

  port = 8080

}


Podemos utilizar dynamic.


resource "example" "server" {


  dynamic "rule" {

    for_each = var.ports


    content {

      port = rule.value

    }

  }

}


Terraform genera conceptualmente:


server

 ├── rule { port = 80 }

 ├── rule { port = 443 }

 └── rule { port = 8080 }


¿Qué significa content?

Esta parte suele resultar extraña al principio. Podemos leerlo como: Por cada elemento de var.ports, generá un bloque llamado rule cuyo contenido sea el bloque content.


Si: var.ports = [80, 443, 8080]


Terraform conceptualmente produce:


rule {

  port = 80

}


rule {

  port = 443

}


rule {

  port = 8080

}


dynamic necesita que el provider soporte ese bloque

Este detalle es importante.

No podemos inventar cualquier bloque:


dynamic "foo" {

  ...

}


El resource tiene que soportar un bloque foo.

Por eso dynamic depende de la estructura definida por el provider.


Por ejemplo, un resource puede tener:


resource "..." "..." {


  ingress {

    ...

  }


  egress {

    ...

  }

}


Entonces podemos generar dinámicamente ingress o egress.

Pero no podemos inventar:


dynamic "banana" {

  ...

}


si ese resource no tiene un bloque banana.

Los recursos de infraestructura suelen tener configuraciones como reglas de red.


Podríamos tener:


variable "ingress_rules" {

  default = [

    {

      port     = 80

      protocol = "tcp"

    },

    {

      port     = 443

      protocol = "tcp"

    }

  ]

}


Y un resource que soporte bloques ingress.


Conceptualmente:


resource "example_security_group" "app" {


  dynamic "ingress" {

    for_each = var.ingress_rules


    content {

      port     = ingress.value.port

      protocol = ingress.value.protocol

    }

  }

}


La configuración resultante tendría dos bloques:


ingress

  port = 80

  protocol = tcp


ingress

  port = 443

  protocol = tcp


Esto es mucho más flexible que escribir las reglas manualmente.

¿Por qué no utilizar simplemente for_each? Porque for_each y dynamic resuelven problemas diferentes.

Supongamos que tenemos:


resource "example_server" "server" {

  for_each = var.servers

}


Esto crea:

server["web"]

server["api"]

server["worker"]


Estamos creando tres resources.


En cambio:


resource "example_server" "server" {

  dynamic "rule" {

    for_each = var.rules

    content {

      ...

    }

  }

}


crea un solo resource pero con varios bloques:


server

 ├── rule

 ├── rule

 └── rule


La diferencia es fundamental.


Ahora pasemos a otro concepto importante.

Terraform necesita conocer las dependencias entre los recursos.


Supongamos que tenemos:


network

   │

   ▼

server


La máquina necesita que la red exista antes.

¿Cómo sabe Terraform que existe esa relación?

Muchas veces la dependencia aparece automáticamente a través de referencias.


Por ejemplo:


resource "example_network" "main" {

  name = "main"

}


resource "example_server" "app" {

  network_id = example_network.main.id

}


La expresión:

example_network.main.id


no solamente obtiene un valor.

También establece una dependencia.

Terraform entiende:


example_network.main

          │

          ▼

example_server.app


Por lo tanto, primero debe crear la red y después el servidor.


Dependencias implícitas: Este tipo de dependencia se conoce como una dependencia implícita.


Por ejemplo:

resource "A" "a" {

  ...

}


resource "B" "b" {

  resource_id = A.a.id

}


Terraform puede deducir: A → B


No necesitamos decir explícitamente: "Creá A antes que B."

La referencia ya expresa esa relación.

Esto es una de las ventajas del enfoque declarativo.


A veces existe una dependencia que Terraform no puede deducir automáticamente.

Por ejemplo:

resource "example_network" "main" {

  ...

}


resource "example_server" "app" {

  ...

}


Supongamos que sabemos que el servidor depende de la red, pero nuestra configuración no tiene ninguna referencia directa entre ellos.


Podemos indicarlo:


resource "example_server" "app" {


  depends_on = [

    example_network.main

  ]

}


Ahora estamos diciendo explícitamente:


example_network.main

          │

          ▼

example_server.app


¿Cuándo usar depends_on?

La regla general es: Utilizá dependencias implícitas cuando sea posible.

Es decir, preferimos:

network_id = example_network.main.id


antes que:

depends_on = [

  example_network.main

]


si realmente necesitamos el id de la red.

¿Por qué? Porque la referencia expresa dos cosas al mismo tiempo:

  1. qué valor necesitamos;
  2. de qué recurso dependemos.

Es mucho más descriptivo. depends_on no debería ser nuestra primera herramienta

Un error frecuente es empezar a agregar: depends_on = [...] por todas partes.


Por ejemplo:


resource "A" "a" {

  ...

}


resource "B" "b" {

  depends_on = [A.a]

}


resource "C" "c" {

  depends_on = [B.b]

}


resource "D" "d" {

  depends_on = [C.c]

}


Podemos terminar creando una enorme cadena artificial: A → B → C → D

aunque Terraform podría haber determinado parte de esas relaciones por sí mismo.


Esto reduce la capacidad de Terraform para ejecutar operaciones en paralelo.

Este punto es muy interesante.


Supongamos:


network

   │

   ├── server

   │

   └── database


server y database dependen de network, pero no dependen entre ellos.

Terraform puede conceptualizarlo como:

        network

        /     \

       /       \

   server    database


Una vez creada la red, ambos recursos pueden avanzar independientemente.

En cambio, si agregamos dependencias innecesarias:


network

   │

   ▼

server

   │

   ▼

database


estamos obligando a Terraform a realizar operaciones secuencialmente.

Por eso las dependencias correctas son importantes tanto para la correctitud como para el rendimiento.

Todo esto nos lleva a una idea muy interesante.

Terraform puede representar nuestra infraestructura como un grafo de dependencias.


Por ejemplo:

             network

            /       \

           /         \

       subnet       database

          |

          ▼

        server

          |

          ▼

    load_balancer


Los nodos representan recursos.

Las flechas representan dependencias.

Terraform utiliza esta información para determinar el orden de las operaciones.

Por eso una configuración declarativa puede funcionar sin que nosotros escribamos:

  1. crear network
  2. crear subnet
  3. crear server
  4. crear load balancer


Terraform construye ese orden a partir de las relaciones.

Ahora podemos combinar varios conceptos que vimos en los últimos artículos.

Supongamos:


variable "servers" {

  type = map(object({

    size = string

  }))


  default = {

    web = {

      size = "small"

    }


    api = {

      size = "medium"

    }

  }

}


Podemos crear múltiples servidores:


resource "example_server" "server" {

  for_each = var.servers


  name = each.key

  size = each.value.size

}


Y podríamos tener otro recurso que dependa de esos servidores.

Por ejemplo:


resource "example_load_balancer" "main" {


  backend_ids = [

    for server in example_server.server :

    server.id

  ]

}


La expresión:


[

  for server in example_server.server :

  server.id

]


construye una lista con los IDs de todos los servidores.

Terraform puede deducir que el load balancer depende de ellos.

Tenemos entonces:


                      network

                            │

                            ▼

      ┌───────┴───────┐

      ▼                                         ▼

    web                                        api

      \                                           /

        \                                       /

          ▼                              ▼

                 load balancer


Esto ya se parece bastante a una infraestructura real.

Aunque dynamic es útil, hay que tener cuidado.

Es tentador transformar cualquier cosa en:


dynamic "something" {

  for_each = ...

  content {

    ...

  }

}


Pero podemos terminar con una configuración difícil de leer.

Por ejemplo, si solamente tenemos dos reglas:


ingress {

  port = 80

}


ingress {

  port = 443

}


puede ser mucho más claro escribirlas directamente.

No todo necesita ser dinámico.

Una buena regla práctica es: Usá dynamic cuando realmente necesites generar un número variable de bloques.


Si la configuración es pequeña y fija, escribirla explícitamente puede ser mejor.


Esto:


dynamic "ingress" {

  for_each = var.ingress_rules


  content {

    port     = ingress.value.port

    protocol = ingress.value.protocol

  }

}


es muy útil cuando:


development → 2 reglas

production  → 8 reglas


Pero si siempre tenemos exactamente:


HTTP

HTTPS


quizás sea más claro:


ingress {

  port     = 80

  protocol = "tcp"

}


ingress {

  port     = 443

  protocol = "tcp"

}


Terraform es código.

Y, como cualquier código, la claridad importa.

En los primeros artículos Terraform parecía bastante sencillo: resource

Después agregamos:

  • variables
  • outputs
  • modules


Y ahora tenemos:


resources

   │

   ├── count

   ├── for_each

   ├── expressions

   └── dynamic blocks

            │

            ▼

      dependencias

            │

            ▼

       dependency graph


Esto cambia bastante la forma en que podemos modelar infraestructura.

Ya no estamos simplemente escribiendo: "Crear un servidor."

Estamos describiendo relaciones: "Quiero estos servidores, con estas configuraciones, conectados a esta infraestructura y utilizados por este balanceador."

Terraform se ocupa de convertir ese modelo en un conjunto de operaciones.