Translate

sábado, 3 de octubre de 2026

Terraform desde cero: variables, outputs y recursos


En el artículo anterior vimos qué problema intenta resolver Terraform y qué significa Infrastructure as Code.

La idea fundamental era: En lugar de configurar nuestra infraestructura manualmente, podemos describirla mediante código.

Ahora vamos a dar el siguiente paso.

En este artículo vamos a conocer tres conceptos que aparecen prácticamente en cualquier proyecto de Terraform:

  • Resources
  • Variables
  • Outputs

Y vamos a terminar viendo dónde aparece un concepto fundamental: el state.


Resources: los recursos que queremos crear

El concepto más importante de Terraform probablemente sea el de resource.

Un resource representa algo que queremos administrar.


Por ejemplo:

  • una máquina virtual;
  • una base de datos;
  • un bucket;
  • una red;
  • una regla de firewall;
  • un DNS;
  • un archivo local.


En el artículo anterior utilizamos:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


La estructura general es:


resource "TIPO" "NOMBRE" {

  ...

}


Por ejemplo:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


Tenemos dos nombres importantes:


local_file

    │

    └── tipo de recurso


hello

    │

    └── nombre del recurso


Por lo tanto, dentro de Terraform podemos referirnos a este recurso como:

local_file.hello


Es importante entender que local_file no es algo que Terraform conozca mágicamente.

Lo proporciona un provider.


Providers

Terraform utiliza providers para comunicarse con diferentes plataformas y servicios.

Por ejemplo, existen providers para trabajar con:

  • AWS
  • Azure
  • Google Cloud
  • Kubernetes
  • GitHub
  • Docker
  • bases de datos
  • recursos locales


Por ejemplo, para nuestro ejemplo local:


terraform {

  required_providers {

    local = {

      source = "hashicorp/local"

    }

  }

}


provider "local" {}


Y entonces podemos utilizar:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


Podemos pensar en el provider como el componente que sabe cómo comunicarse con el sistema que estamos administrando.


Terraform sabe interpretar nuestra configuración.

El provider sabe cómo convertir esa configuración en operaciones sobre un servicio concreto.

Supongamos ahora que queremos crear dos archivos:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


resource "local_file" "goodbye" {

  filename = "goodbye.txt"

  content  = "Chau desde Terraform!"

}


Funciona perfectamente.

Pero imaginemos que el nombre de nuestro ambiente aparece en varios lugares:


resource "local_file" "config" {
  filename = "production-config.txt"
  content  = "Configuración de production"
}


¿Qué pasa si mañana queremos utilizar exactamente la misma configuración para development?

Podríamos copiar el archivo y modificar los valores.

Pero eso rápidamente empieza a convertirse en un problema.

Queremos parametrizar nuestra infraestructura.

Ahí aparecen las variables.


Variables

Una variable permite definir un valor que puede cambiar sin modificar directamente nuestro resource.

Por ejemplo:

variable "environment" {
  type    = string
  default = "development"
}

Ahora podemos utilizarla:

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"

  content = "Configuración de ${var.environment}"
}


Terraform reemplazará:

${var.environment}

por el valor correspondiente.


Con el valor por defecto:

development

terminaremos teniendo:

development-config.txt

y su contenido será:

Configuración de development

No siempre queremos definir un valor por defecto.

Por ejemplo:

variable "environment" {
  type = string
}

Ahora Terraform necesita que le proporcionemos un valor.

Podemos hacerlo mediante un archivo:

terraform.tfvars

con:

environment = "production"

Y nuestro resource puede seguir siendo exactamente el mismo:

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"

  content = "Configuración de ${var.environment}"
}

Esto es interesante porque podemos utilizar la misma infraestructura para diferentes ambientes.


Por ejemplo:

development
staging
production

cambiando solamente los valores de entrada.

¿Por qué no poner todo directamente en el resource?

Podríamos escribir:

resource "local_file" "config" {
  filename = "production-config.txt"
  content  = "Configuración de production"
}

Pero entonces nuestro código está acoplado a un ambiente concreto.

Con variables podemos separar:

Infraestructura
      +
Configuración


Por ejemplo:

main.tf
    ↓
define cómo funciona la infraestructura

terraform.tfvars
    ↓
define los valores para este ambiente


Esta separación se vuelve especialmente importante cuando trabajamos con infraestructura real.

Las variables pueden tener diferentes tipos.

Por ejemplo:

variable "environment" {
  type = string
}

Una cadena.

También podemos tener números:

variable "instance_count" {
  type = number
}

Booleanos:

variable "enabled" {
  type = bool
}

Y estructuras más interesantes.

Por ejemplo, una lista:

variable "availability_zones" {
  type = list(string)
}

Podemos asignarle:

availability_zones = [
  "us-east-1a",
  "us-east-1b"
]

Más adelante, cuando empecemos a trabajar con configuraciones más complejas, estos tipos van a ser muy importantes.


Hasta ahora vimos cómo introducir información en Terraform mediante variables.

Pero muchas veces queremos hacer lo contrario.

Queremos que Terraform nos muestre información sobre lo que acaba de crear.

Para eso tenemos los outputs.

Por ejemplo:

output "environment" {
  value = var.environment
}

Después de ejecutar:

terraform apply

Terraform puede mostrar:

Outputs:

environment = "development"


Los outputs sirven para exponer información que resulta útil después de crear nuestros recursos.

Los outputs se vuelven mucho más interesantes cuando apuntan a recursos.


Por ejemplo:

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"

  content = "Configuración de ${var.environment}"
}


Podemos definir:

output "config_file" {
  value = local_file.config.filename
}


Ahora Terraform conoce el nombre del archivo creado y podemos mostrarlo como output.


La referencia:

local_file.config.filename


significa:

resource
   │
   └── local_file.config
            │
            └── atributo filename


Esta forma de referenciar recursos es fundamental en Terraform.

Terraform entiende las dependencias

Supongamos que tenemos:

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"
  content  = "Configuración de ${var.environment}"
}

y:

output "config_file" {
  value = local_file.config.filename
}


El output depende del resource.

Terraform puede detectar esa relación.

Podemos imaginarlo así:

local_file.config
        │
        ▼
output.config_file


En configuraciones más grandes tendremos muchos recursos relacionados:


network
   │
   ├── subnet
   │      │
   │      └── virtual machine
   │
   └── database


Terraform construye y utiliza estas dependencias para determinar el orden en el que debe realizar las operaciones.

Veamos un ejemplo completo

Nuestro main.tf puede quedar así:

terraform {
  required_providers {
    local = {
      source = "hashicorp/local"
    }
  }
}

provider "local" {}

variable "environment" {
  type    = string
  default = "development"
}

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"

  content = <<EOF
Environment: ${var.environment}
Application: MyApplication
EOF
}

output "config_file" {
  value = local_file.config.filename
}


Ahora ejecutamos:

terraform init


Después:

terraform plan


Y finalmente:

terraform apply


Terraform crea:

development-config.txt

con:

Environment: development
Application: MyApplication

Y además podemos obtener:

Outputs:

config_file = "development-config.txt"

Tenemos entonces las tres piezas:

                                       Terraform
                                             │
        ┌────────────┼────────────┐
        │                                  │                                  │
        ▼                                ▼                                 ▼
     Variable                     Resource                       Output
        │                                  │                                  │
        │                                  │                                  │
        └────────────┼────────────┘
                                             │
                                            ▼
                                     infraestructura


¿Dónde entra el State?

Hay un concepto que todavía no explicamos en profundidad, pero que ya aparece cuando ejecutamos Terraform:

terraform apply

Terraform necesita saber qué recursos administra y cuál es el estado conocido de esos recursos.

Para eso utiliza el state.

Normalmente, después de ejecutar nuestro ejemplo veremos un archivo:

terraform.tfstate

Este archivo contiene información que Terraform utiliza para relacionar nuestra configuración con los recursos que administra.

Por ahora no necesitamos entrar en todos sus detalles.

Lo importante es entender que Terraform trabaja con tres cosas:

  Configuración
       │
       │
       ▼
   Terraform
       ▲
       │
       │
     State
       │
      ▼
Infraestructura real


La configuración dice lo que queremos.

El state ayuda a Terraform a saber qué está administrando.

Y la infraestructura real es lo que existe efectivamente.

Este concepto merece otro post... 

viernes, 2 de octubre de 2026

Terraform: ¿qué problema resuelve? Una introducción a Infrastructure as Code


Cuando hablamos de Terraform, muchas veces la primera reacción es: ¿Otra herramienta más para configurar servidores?

No exactamente.

Terraform intenta resolver un problema bastante más interesante: ¿cómo podemos definir, versionar y reproducir nuestra infraestructura de la misma manera que hacemos con nuestro código?


Imaginemos que tenemos que desplegar una aplicación.

Necesitamos:

  • una máquina virtual;
  • una base de datos;
  • una red;
  • algunos permisos;
  • un almacenamiento;
  • quizás un balanceador.


Una forma de hacerlo es entrar a la consola de nuestro proveedor cloud y empezar a crear recursos.

  1. Hacemos clic.
  2. Configuramos una opción.
  3. Creamos una red.
  4. Después una máquina virtual.
  5. Después cambiamos una configuración.
  6. Y finalmente tenemos nuestra infraestructura funcionando.


El problema aparece unos meses después.

Queremos crear exactamente el mismo ambiente para testing.

¿Recordamos todos los pasos?

¿Todas las opciones?

¿Todas las reglas de firewall?

¿Todas las dependencias?

Probablemente no.


Y aunque recordemos todo, aparece otro problema: ¿cómo sabemos exactamente qué cambió entre producción y testing?


Ahí aparece una idea importante: Infrastructure as Code

Infrastructure as Code, o IaC, consiste en tratar la infraestructura como código.


En lugar de decir: Entrá a esta consola y creá una máquina con estas opciones, podemos escribir algo que describa nuestra infraestructura:


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

    ...

}


Ese archivo puede:

  • guardarse en Git;
  • revisarse mediante Pull Requests;
  • versionarse;
  • reutilizarse;
  • automatizarse;
  • compartirlo con otros desarrolladores.


Nuestra infraestructura deja de estar solamente en una consola y pasa a estar también descrita en código.

HashiCorp Terraform es una herramienta de Infrastructure as Code desarrollada originalmente por HashiCorp.


La idea fundamental es bastante sencilla: Describimos el estado que queremos y Terraform se encarga de realizar los cambios necesarios para llegar a ese estado.

Esto es importante porque Terraform utiliza un enfoque declarativo.

No necesitamos describir paso por paso cómo crear la infraestructura.

Describimos qué queremos tener.


Por ejemplo:

resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


Estamos diciendo: Quiero un recurso local_file llamado hello cuyo archivo sea hello.txt y cuyo contenido sea Hola desde Terraform!.

Terraform se ocupa del resto.


Para empezar no necesitamos AWS, Azure ni Google Cloud.

Podemos utilizar un provider que trabaja con archivos locales.

Nuestro primer ejemplo completo puede ser:


terraform {

  required_providers {

    local = {

      source = "hashicorp/local"

    }

  }

}


provider "local" {}


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


Guardamos el archivo como:

main.tf


Y ejecutamos:

terraform init


Terraform descarga y configura el provider que necesitamos.

Después podemos preguntarle qué va a hacer:


terraform plan


Terraform nos muestra un plan de ejecución.


Finalmente:

terraform apply


Terraform aplica los cambios.


Y ahora tenemos:

hello.txt


con el contenido:


Hola desde Terraform!


¿Por qué plan es interesante?

Este es uno de los conceptos más importantes de Terraform.

Antes de modificar nuestra infraestructura podemos ejecutar:


terraform plan


Terraform analiza la configuración y el estado actual y nos muestra qué cambios pretende realizar.


Por ejemplo:

Plan: 1 to add, 0 to change, 0 to destroy.


Esto nos permite revisar los cambios antes de aplicarlos.

En una infraestructura real esto es extremadamente importante.


Imaginemos que alguien modifica un archivo Terraform y, sin revisar nada, ejecuta directamente la aplicación de los cambios.

Podría terminar eliminando un recurso que no debía eliminarse.

Con plan, podemos ver qué piensa hacer Terraform antes de hacerlo.


Terraform no es un script. Esta diferencia merece una explicación.


jueves, 1 de octubre de 2026

Fundamentos de lenguajes de programación cuánticos parte 2

 

Seguimos con : https://emanuelpeg.blogspot.com/2026/10/fundamentos-de-lenguajes-de.html

 ¿Y si el sistema de tipos controlara los qubits?

Acá aparece una conexión muy interesante con un área clásica de los lenguajes de programación: los sistemas de tipos.

Supongamos que tenemos:

qubit q

y hacemos:

measure(q)


Ahora podríamos preguntarnos:

xt

use(q)


¿Es válido?

¿Y qué pasa con:

copy(q)


¿Debería permitirlo el compilador?

Estas preguntas nos llevan a una idea conocida en lenguajes funcionales y sistemas de tipos: los tipos pueden utilizarse para controlar recursos.


Un tipo lineal introduce una restricción interesante: un recurso debe utilizarse exactamente de acuerdo con las reglas establecidas por el sistema de tipos.


Una forma simplificada de imaginarlo sería:

q : Qubit

y determinadas operaciones consumen o transforman ese recurso.


En vez de pensar solamente: variable → valor

podemos pensar: variable → recurso


Esto resulta especialmente interesante para la computación cuántica porque los qubits no se comportan como valores clásicos que podemos copiar libremente.


Por ejemplo, conceptualmente queremos evitar situaciones como:

q = createQubit()

q1 = q

q2 = q


si eso implica que q1 y q2 son copias independientes del mismo estado cuántico.

El sistema de tipos puede ayudar a expresar estas restricciones.


Esta diferencia permite ver algo más profundo.

En muchos lenguajes pensamos principalmente en: valor

Por ejemplo:

x : Int


Pero en determinados lenguajes podemos necesitar pensar también en: t recurso

Un qubit puede considerarse un recurso cuyo uso está restringido.

Esto tiene relación conceptual con otras ideas que ya existen en lenguajes modernos:

  • tipos lineales
  • ownership
  • borrowing
  • affine types
  • resource management


Por supuesto, estas ideas no son idénticas entre sí ni todos los lenguajes cuánticos utilizan exactamente el mismo mecanismo.


Pero comparten una preocupación: controlar qué podemos hacer con determinados valores o recursos.


Los programas cuánticos tampoco necesariamente reemplazan a los programas clásicos.

En muchos casos tenemos una combinación:


programa clásico


       │


       ▼


preparar qubits


       │


       ▼


ejecutar circuito cuántico


       │


       ▼


    medir


       │


       ▼


resultado clásico



Podemos imaginar un programa clásico que decide qué circuito ejecutar:


if condition:

    ejecutarCircuitoA()

else:

    ejecutarCircuitoB()


y dentro de esos circuitos aparecen las operaciones cuánticas.

Esto genera una característica importante de los lenguajes cuánticos: el mundo clásico y el mundo cuántico deben coexistir.


Podemos pensar entonces en dos mundos:

                         Programa

                                │

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

       │                                                │

    Clásico                                    Cuántico

       │                                              │

     Int                                           Qubit

     Bool                                        Estado

     List                                         Circuito

     String                                     Medición



El lenguaje debe establecer reglas sobre cómo interactúan ambos.


Por ejemplo:

resultado = measure(q)

convierte información del mundo cuántico en información clásica.


Pero el camino inverso no es simplemente:

q = resultado


Hay que preparar un estado cuántico y aplicar las operaciones correspondientes.


Acá aparece otro punto de contacto con los lenguajes de programación tradicionales.

Podemos imaginar una cadena de compilación como:


Código fuente

     ↓

   AST

     ↓

Representación intermedia

     ↓

Optimización

     ↓

Circuito cuántico

     ↓

Instrucciones

     ↓

Hardware cuántico



Esto es muy parecido a un compilador tradicional.

Pero aparecen problemas nuevos.


Por ejemplo:

¿Podemos eliminar una operación?

 ¿Podemos reordenar dos operaciones?

 ¿Dos circuitos son equivalentes?

 ¿Podemos reducir la cantidad de puertas?

 ¿Cómo traducimos una operación abstracta a las operaciones que soporta un hardware específico?


El compilador cuántico no solamente traduce código.

También puede transformar y optimizar circuitos.


Fundamentos de lenguajes de programación cuánticos


Cuando pensamos en computación cuántica, normalmente aparecen palabras como qubit, superposición, entrelazamiento o puertas cuánticas.

Pero hay otra pregunta interesante:

¿Cómo se programa una computadora cuántica?

Y, más específicamente:

¿Qué cambia en un lenguaje de programación cuando la máquina que ejecuta nuestros programas es cuántica?

La respuesta es interesante porque no alcanza simplemente con agregar un nuevo tipo de dato llamado Qubit.

La computación cuántica introduce restricciones y conceptos que afectan algunas de las ideas fundamentales de los lenguajes de programación: valores, variables, tipos, copia, efectos, recursos y compilación.


En una computadora clásica, la unidad básica de información es el bit.

Un bit puede tener dos valores: 0 o 1

Podemos imaginar entonces una variable:


x = 0


y posteriormente:


x = 1


Un qubit es diferente.

Su estado puede representarse como:


α|0⟩ + β|1⟩


donde α y β son números complejos llamados amplitudes.


No significa simplemente que el qubit contiene un 0 y un 1.

Significa que su estado cuántico es una combinación de ambos estados posibles.


Las amplitudes deben cumplir:


|α|² + |β|² = 1


Cuando medimos el qubit obtenemos un resultado clásico: 0 o 1

La probabilidad de cada resultado depende de las amplitudes.


Por ejemplo:

1/√2 |0⟩ + 1/√2 |1⟩


produce un 0 o un 1 con igual probabilidad.

Esto ya introduce una diferencia importante para un lenguaje de programación.

Una variable clásica tiene un valor.

Un qubit representa un estado cuántico que puede transformarse y posteriormente medirse.


En un lenguaje clásico estamos acostumbrados a operaciones como:

x = x + 1


o:


x = !x


En un lenguaje cuántico aparecen operaciones como las puertas cuánticas.


Por ejemplo:

X(q)


La puerta X tiene un comportamiento similar al NOT clásico sobre los estados básicos:


|0⟩ → |1⟩

|1⟩ → |0⟩


Otra puerta fundamental es H, la puerta de Hadamard.


Aplicada a |0⟩, produce una superposición:


H(|0⟩) = 1/√2 |0⟩ + 1/√2 |1⟩


Es decir, una operación que en un lenguaje clásico podríamos imaginar como una transformación de un valor, en el mundo cuántico transforma un estado.


Una forma habitual de representar un programa cuántico es mediante un circuito.


Por ejemplo:


q ── H ─────●──── M

             │

q ───────────X──── M


Aquí tenemos dos qubits.

Al primero le aplicamos H, después utilizamos ambos en una operación CNOT y finalmente medimos los qubits.

Desde el punto de vista de un lenguaje de programación, esto es bastante interesante.

El programa no solamente describe valores y operaciones. También describe una secuencia de transformaciones sobre estados cuánticos.

Por eso muchos lenguajes y frameworks cuánticos tienen una fuerte relación con la representación de circuitos.


La medición es uno de los conceptos que más claramente rompe nuestras intuiciones clásicas.

Podemos imaginar algo como:

q = |+⟩

donde:

|+⟩ = 1/√2 |0⟩ + 1/√2 |1⟩


Si hacemos: measure(q) obtenemos un resultado clásico: 0 o 1

Pero el proceso de medición también modifica el estado cuántico.


Esto genera una pregunta interesante para el diseño de lenguajes: ¿Qué significa exactamente utilizar nuevamente q después de medirlo?


En un lenguaje clásico, leer una variable normalmente no destruye su valor:


x = 10

print(x)

print(x)

print(x)


En computación cuántica no podemos asumir automáticamente ese comportamiento.

La medición es un efecto que debemos tener en cuenta.


Esta es probablemente una de las diferencias más interesantes desde el punto de vista de los lenguajes de programación.


En un lenguaje clásico podemos hacer:

a = 10

b = a


Tenemos dos valores.

Con un qubit no existe una operación general equivalente a:

q2 = copy(q1)


para copiar arbitrariamente el estado cuántico.


Esto se conoce como el teorema de no clonación cuántica.

No se trata simplemente de que la API de un lenguaje haya decidido prohibir una operación.

Es una propiedad fundamental de la mecánica cuántica.

Y esto tiene consecuencias importantes para el diseño de lenguajes.


Me quedo relargo el post vamos a tener que tener parte 2. 

miércoles, 30 de septiembre de 2026

De C# a Q#: buscando un elemento con Grover


Cuando hablamos de computación cuántica es fácil caer en ejemplos demasiado artificiales. Podemos hablar de superposición, entrelazamiento, qubits e interferencia, pero queda una pregunta bastante más interesante: ¿Cómo se programa realmente un algoritmo cuántico?

Para verlo, vamos a resolver un problema muy sencillo de dos maneras:

Tenemos una colección de elementos y queremos encontrar uno que cumple una determinada condición.


Primero lo hacemos de la manera clásica, con C#. Después veremos cómo abordar el mismo problema con Q# utilizando el algoritmo de Grover.


Supongamos que tenemos:

int[] values = { 3, 8, 12, 17, 21, 42, 55, 61 };


y queremos encontrar el 42.


La solución más sencilla es una búsqueda lineal:


int Search(int[] values, int target)

{

    for (int i = 0; i < values.Length; i++)

    {

        if (values[i] == target)

            return i;

    }


    return -1;

}


No hay ningún misterio.


El algoritmo va recorriendo los elementos:


3  → no

8  → no

12 → no

17 → no

21 → no

42 → sí


En el peor caso tendremos que consultar todos los elementos.

Para una colección de N elementos, esto requiere:


O(N)


consultas.


Acá aparece una diferencia fundamental.


No podemos simplemente escribir:


for cada qubit

    buscar()


y esperar que la computadora cuántica haga lo mismo más rápido.


La programación cuántica utiliza otro modelo.

Uno de los algoritmos más conocidos para búsquedas no estructuradas es Grover's algorithm.


Su idea fundamental es utilizar:

  • superposición
  • un oráculo cuántico
  • interferencia
  • amplificación de amplitud
  • medición


para aumentar la probabilidad de obtener la respuesta que estamos buscando.


La cantidad de consultas necesarias pasa de: O(N)

a aproximadamente: O(√N)


Para utilizar Grover tenemos que formular la búsqueda de otra manera.

En lugar de preguntarnos: ¿Es este elemento igual a 42?

con cada elemento individualmente, definimos una función que identifica la solución.


Por ejemplo:

f(x) = 1  si x es la solución

f(x) = 0  en otro caso


Esta función se implementa como un oráculo cuántico.

Conceptualmente:


                     ┌─────────────┐

x ─────►│          ORACLE           │─────► marca x

                     └─────────────┘


El oráculo no nos devuelve directamente la solución.

Lo que hace es marcarla.

Y después Grover utiliza interferencia para aumentar su amplitud.


Primero necesitamos una superposición

Supongamos que tenemos 8 posibilidades:


000

001

010

011

100

101

110

111


Con tres qubits podemos representar esas ocho posibilidades.

Inicialmente tenemos un estado:

|000>


Aplicando una compuerta Hadamard a cada qubit obtenemos una superposición de todos ellos:


|000> + |001> + |010> + |011>

+ |100> + |101> + |110> + |111>


No significa que tengamos ocho computadoras ejecutándose independientemente.

Significa que nuestro estado cuántico es una combinación de esas posibilidades.


El oracle. Ahora necesitamos marcar nuestra solución.

Supongamos que queremos encontrar: 101


El oracle modifica la fase asociada a ese estado.


Conceptualmente:

000   +

001   +

010   +

011   +

100   +

101   -

110   +

111   +


La solución no apareció mágicamente.

Simplemente fue marcada mediante su fase.

Y esto es importante porque todavía no podemos medir y decir: ¡101!


Si medimos ahora, seguimos teniendo una probabilidad distribuida entre los estados.


Grover aplica una operación llamada difusión o inversión sobre la media.

Su objetivo es modificar las amplitudes de los estados.

Después de una iteración:


solución      ↑↑↑

otros estados ↓


La amplitud de la solución aumenta mientras las amplitudes de los estados incorrectos disminuyen.


Repetimos este proceso aproximadamente: π/4 × √N veces.


Finalmente medimos.

La probabilidad de obtener la solución es mucho mayor.


¿Y dónde está Q#?

Ahora viene la parte que más nos interesa.

Q# no intenta esconder que estamos trabajando con un modelo diferente.


Un programa cuántico puede trabajar con Qubit: use qs = Qubit[3];

Tenemos tres qubits.

Podemos ponerlos en superposición:

ApplyToEach(H, qs);


H es la compuerta Hadamard.

Después podemos aplicar nuestro oracle y la operación de difusión.

Una versión conceptual de Grover puede verse así:


operation Grover(qs : Qubit[]) : Unit {

    ApplyToEach(H, qs);


    // Oracle

    Oracle(qs);


    // Difusión

    Diffusion(qs);

}


En una implementación real necesitamos definir cuidadosamente el oracle y la difusión, pero la estructura fundamental ya está ahí:


superposición

      ↓

   oracle

      ↓

  difusión

      ↓

   medir


Comparemos las dos soluciones.

En C# escribimos:


for (int i = 0; i < values.Length; i++)

{

    if (values[i] == target)

        return i;

}


El programa piensa en términos de:


elemento

elemento

elemento

elemento

...

Es una búsqueda secuencial.


En Q# pensamos en:


qubits

  ↓

superposición

  ↓

oracle

  ↓

interferencia

  ↓

medición


No estamos escribiendo una versión más complicada del mismo for.

Estamos utilizando un modelo computacional diferente.

Sería muy fácil concluir: Entonces una computadora cuántica puede buscar en una lista de un millón de elementos en √1.000.000 = 1.000 pasos.


No es tan sencillo.

Grover proporciona una ventaja en el número de consultas al oracle.

Eso no significa que toda la aplicación se ejecute en O(√N).


Tenemos que:

  • preparar el estado cuántico;
  • construir el oracle;
  • ejecutar las operaciones cuánticas;
  • repetir Grover el número adecuado de veces;
  • realizar la medición.


Además, las computadoras cuánticas reales tienen limitaciones físicas y de hardware.

Por eso es más correcto decir: Grover reduce el número de consultas necesarias para una búsqueda no estructurada de O(N) a O(√N).

No significa que cualquier programa de búsqueda pueda reemplazarse automáticamente por Grover.


Lo interesante de Q# no es simplemente que tenga un tipo llamado Qubit.

Lo interesante es que el lenguaje permite expresar conceptos que no aparecen en un lenguaje clásico tradicional:

  • Qubit
  • Hadamard
  • superposición
  • medición
  • operaciones unitarias
  • oracle


Y eso cambia la forma en que pensamos el algoritmo.

En C# pensamos:

datos → operaciones → resultado


En Q# podemos pensar:


estado cuántico

      ↓

transformación

      ↓

interferencia

      ↓

medición

      ↓

resultado



No podemos decir simplemente eso.

Estamos comparando dos modelos de computación diferentes.


Para nuestra búsqueda:

Clásico: O(N)


frente a Grover: O(√N) consultas al oracle


Pero para aprovechar esa ventaja necesitamos una computadora cuántica y un problema que pueda expresarse adecuadamente mediante un oracle.

Además, si los datos están almacenados en una memoria clásica y tenemos que cargarlos en la computadora cuántica, esa carga también forma parte del problema.


Lo verdaderamente interesante

Quizás la parte más interesante de este ejemplo no sea que:

O(N) → O(√N)


sino por qué es posible hacerlo.


Una computadora clásica trabaja con bits:

0

1


Una computadora cuántica trabaja con qubits, que pueden encontrarse en superposición:

α|0> + β|1>


y las operaciones cuánticas pueden modificar las amplitudes de esos estados.

Grover aprovecha precisamente esto.

No estamos ejecutando un for` más rápido.


Estamos diseñando un algoritmo alrededor de las propiedades de la mecánica cuántica.

Y acá aparece una cuestión interesante para los lenguajes de programación.

En C# podemos abstraernos completamente del hardware:

int result = Search(values, 42);


En Q#, en cambio, muchas de las abstracciones del lenguaje están relacionadas directamente con el modelo cuántico:

use qs = Qubit[3];


ApplyToEach(H, qs);


Oracle(qs);

Diffusion(qs);


El programador tiene que pensar en:

¿Qué estado tengo?

¿Qué transformación estoy aplicando?

¿Qué amplitudes quiero aumentar?

¿Qué amplitudes quiero cancelar?

¿Cuándo puedo medir?


Es decir, el paradigma de programación cambia.


Grover es un buen ejemplo para entender que la computación cuántica no consiste simplemente en ejecutar programas clásicos más rápido.


Tenemos el mismo problema: encontrar un elemento que cumple una condición.


Pero utilizamos dos modelos diferentes.

En el modelo clásico:


buscar

   ↓

comparar

   ↓

buscar

   ↓

comparar

   ↓

...



En el modelo cuántico:


superposición

      ↓

    oracle

      ↓

 interferencia

      ↓

amplificación

      ↓

   medición


Y ahí está, quizás, la diferencia más importante entre programar para una computadora clásica y programar para una computadora cuántica: No se trata solamente de aprender nuevas instrucciones. Hay que aprender a pensar en términos de estados cuánticos y transformaciones sobre esos estados.


martes, 29 de septiembre de 2026

Quicksort en Q#


Un algoritmo que me gusta mucho es el quicksort, porque es un algoritmo por demás claro. Ya he escrito lo fácil que es implementarlo en Erlang, Rust, haskell y lisp


Ahora le toca a Q#, el lenguaje de programación lógica/relacional. Básicamente, el algoritmo toma un pivote y agrupa los menores que el pivote al principio y los mayores al final y aplica quicksort a estos dos grupos. Y si la lista es vacía o tiene un elemento, ya está ordenada. 


Vamos al código: 


namespace QuantumQuickSort {


    function QuickSort(xs : Int[]) : Int[] {

        if Length(xs) <= 1 {

            return xs;

        }


        let pivot = xs[0];


        let smaller = Filter(x -> x <= pivot, xs[1...]);

        let greater = Filter(x -> x > pivot, xs[1...]);


        return QuickSort(smaller)

               + [pivot]

               + QuickSort(greater);

    }


    function Filter(predicate : (Int -> Bool), xs : Int[]) : Int[] {

        mutable result = [];


        for x in xs {

            if predicate(x) {

                set result += [x];

            }

        }


        return result;

    }

}


sábado, 26 de septiembre de 2026

Quicksort en MiniKanren.



Un algoritmo que me gusta mucho es el quicksort, porque es un algoritmo por demás claro. Ya he escrito lo fácil que es implementarlo en Erlang, Rust, haskell y lisp


Ahora le toca a MiniKanren, el lenguaje de programación lógica/relacional. Básicamente, el algoritmo toma un pivote y agrupa los menores que el pivote al principio y los mayores al final y aplica quicksort a estos dos grupos. Y si la lista es vacía o tiene un elemento, ya está ordenada. 


Vamos al código: 

(define (quicksorto xs ys)

  (conde

    [(== xs '())

     (== ys '())]


    [(fresh (pivot rest smaller larger

                    sorted-smaller sorted-larger)

       (== xs (cons pivot rest))


       (partitiono pivot rest smaller larger)


       (quicksorto smaller sorted-smaller)

       (quicksorto larger sorted-larger)


       (appendo sorted-smaller

                (cons pivot sorted-larger)

                ys))]))

jueves, 24 de septiembre de 2026

S#: un lenguaje para enseñar programación funcional


Cuando enseñamos programación funcional, aparece rápidamente una pregunta: ¿Qué lenguaje usamos?

Una opción bastante obvia es Haskell. Es uno de los lenguajes funcionales más importantes y tiene prácticamente todo lo que queremos enseñar.

Pero aparece un problema: Haskell no siempre resulta sencillo para alguien que está aprendiendo programación funcional por primera vez. Básicamente, en la universidad se enseña C#, Java o C++, que están lejos de Haskell. 

La sintaxis, el sistema de tipos, las funciones de orden superior, las expresiones, las mónadas y muchas otras características hacen que el alumno pueda terminar concentrándose más en entender el lenguaje que en entender el paradigma.

Entonces aparece otra posibilidad: Scala.

Scala tiene una enorme ventaja: permite enseñar programación funcional dentro de un lenguaje que también tiene programación orientada a objetos. Y justamente ahí aparece otro problema.

Si el objetivo de una materia es enseñar programación funcional, no necesariamente queremos que el alumno pueda resolver el ejercicio utilizando:


var x = 10

x = x + 1


o utilizando clases, objetos mutables y técnicas propias de la programación imperativa.


Scala permite hacerlo porque es un lenguaje multiparadigma.

Pero para enseñar un paradigma, a veces queremos algo diferente: Un lenguaje que no permita escapar del paradigma que estamos intentando aprender.


La pregunta entonces fue: ¿Podemos diseñar un lenguaje funcional con una sintaxis más sencilla?

S# es funcional por diseño.

No hay una versión funcional y otra imperativa del lenguaje.

La idea es que el lenguaje ayude al alumno a pensar en términos funcionales.


S# es un pequeño lenguaje de programación funcional, estáticamente tipado, pensado principalmente como herramienta para enseñar programación funcional.

El proyecto actualmente transpila código S# a C# y utiliza .NET como plataforma de ejecución.


Por ejemplo, una función puede escribirse simplemente como:

def add(a: Int, b: Int): Int = a + b


Las funciones son valores:

val double = (x: Int) => x * 2


Y podemos trabajar con listas:

val numbers = List(1, 2, 3)


También podemos utilizar pattern matching:

def length[A](list: List[A]): Int = list match {

    case Nil        => 0

    case head::tail => 1 + length(tail)

}


La sintaxis intenta ser suficientemente familiar para que el alumno pueda concentrarse en la idea funcional que hay detrás del código.


Una de las decisiones importantes de S# es utilizar val para las vinculaciones:

val x = 10


La idea no es tener una alternativa como "var" para modificar posteriormente ese valor.

Esto es deliberado.

Si estamos enseñando programación funcional, queremos que el alumno se acostumbre desde el principio a trabajar con valores inmutables.


El lenguaje intenta acompañar esa forma de pensar. Funcional, pero no necesariamente extraño

Otra decisión importante es no intentar inventar una sintaxis completamente nueva.

S# toma inspiración de lenguajes como Scala y utiliza conceptos conocidos en programación funcional:

val`

funciones y lambdas;

tipos genéricos;

  • List;
  • Option;
  • match;
  • case;
  • sealed trait;
  • case class;
  • recursión;
  • currying;
  • pattern matching.


Por ejemplo:


sealed trait Shape

case class Circle(radius: Double) extends Shape

case class Rectangle(width: Double, height: Double) extends Shape


Y después:


def area(s: Shape): Double = s match {

    case Circle(r)       => 3.14159 * r * r

    case Rectangle(w, h) => w * h

}


Son conceptos que posteriormente el alumno puede encontrar en otros lenguajes funcionales.

La idea no es crear un lenguaje aislado, sino crear una puerta de entrada.


¿Y por qué transpilar a C#?

S# no intenta competir con C#.

C# es simplemente el backend.


El compilador analiza el código S#, realiza el chequeo de tipos y genera código C# que después puede ser compilado y ejecutado sobre .NET.


Esto también tiene una ventaja interesante desde el punto de vista educativo.

Podemos escribir:


def factorial(n: Int): Int =

    if (n <= 1) 1

    else n * factorial(n - 1)


y terminar generando código C# equivalente.


El alumno puede concentrarse primero en qué expresa el programa y, posteriormente, analizar cómo ese código funcional puede representarse en otro lenguaje.


El proyecto fue creciendo más allá de la idea inicial de una sintaxis funcional.


Actualmente S# cuenta con:

  • compilador;
  • lexer y parser;
  • chequeo de tipos;
  • generación de código C#;
  • runtime propio;
  • listas y Option;
  • pattern matching;
  • ADTs;
  • funciones de orden superior;
  • currying y partial application;
  • optimización de tail recursion;
  • REPL;
  • CLI;
  • API REST y gRPC;
  • diagnósticos de compilación;
  • tests de especificación;
  • extensión para VS Code.


Pero el objetivo principal sigue siendo el mismo: hacer que aprender programación funcional sea sencillo.


El proyecto está disponible en GitHub: https://github.com/emanuelpeg/SSharp


sábado, 19 de septiembre de 2026

¿El modelo de actores está más cerca de la OOP de Alan Kay?


En los posts anteriores cuestionamos algunas ideas bastante instaladas sobre la Programación Orientada a Objetos.

¿Son realmente necesarios las clases y la herencia?

¿Los famosos cuatro pilares representan la esencia de OOP?


Y llegamos a una idea particularmente interesante al recuperar la visión de Alan Kay.

Para Kay, la esencia de OOP no estaba en las clases ni en la herencia.


En 2003 lo resumió así: “OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.”


Es decir:

  • mensajes;
  • estado y comportamiento mantenidos localmente;
  • protección y ocultamiento de ese estado;
  • late binding extremo.


Y entonces aparece una pregunta sorprendente: ¿No se parece muchísimo esto al modelo de actores?


Kay imaginaba los objetos como entidades independientes, similares a células biológicas o pequeñas computadoras conectadas en una red.

Cada una tiene su propio estado y comportamiento y se comunica con las demás mediante mensajes.

Lo importante no es acceder al estado de otro objeto.

Lo importante es comunicarse con él.

Esto es muy diferente del modelo mental habitual de:


account.deposit(100);


donde tendemos a pensar en una llamada directa a un método.


En el modelo de mensajes, la idea fundamental es:

account ← deposit(100)


El receptor decide qué hacer con ese mensaje.

Y entonces aparecen los actores


Un actor tiene:

  • estado;
  • comportamiento;
  • una dirección/referencia;
  • un mailbox;
  • capacidad para enviar mensajes;
  • capacidad para cambiar su propio estado.


El actor recibe mensajes y, al procesarlos, puede modificar su estado y enviar nuevos mensajes.

Esto es exactamente el tipo de aislamiento que describe la idea de local retention de Kay.


En el modelo de actores de Akka, por ejemplo, el estado del actor está encapsulado detrás de una referencia: desde afuera no se puede acceder directamente a ese estado. Los demás actores solamente pueden interactuar enviándole mensajes.


En Erlang, la comunicación entre procesos se realiza mediante envío de mensajes, y el message passing es central en el modelo de concurrencia del lenguaje.


Podemos imaginar un proceso que mantiene su propio estado:


loop(State) ->

    receive

        {deposit, Amount} ->

            loop(State + Amount);


        {balance, From} ->

            From ! {balance, State},

            loop(State)

    end.


El estado pertenece al proceso.


Otro proceso no hace:

process.state = 100


Tiene que enviarle un mensaje.

Esto produce una forma de encapsulamiento particularmente fuerte: No solamente ocultamos el estado; aislamos al propietario del estado.


Comparemos los dos modelos.


OOP tradicional


Object

 ├── state

 └── methods

       ↑

       │

  otro objeto

       │

   method call


 Modelo de actores


     Actor A                                                                        Actor B

┌───────────┐                                         ┌───────────┐

│   state                     │                                         │   state                    │

│ behavior                │                                         │ behavior                │

└─────┬─────┘                                         └─────┬─────┘

                 │                                                                           │                 

                 └──────── message ─────────────┘


En el segundo modelo no compartimos directamente el estado ni transferimos el flujo de ejecución al receptor.

Enviamos una señal y continuamos.


En Akka, por ejemplo, el envío de mensajes entre actores es asíncrono: el actor emisor no entrega su hilo de ejecución al receptor.

Esto hace que la metáfora de las “pequeñas computadoras que se comunican” resulte especialmente natural.


El encapsulamiento cambia de escala

En OOP tradicional podemos escribir:


class Account {

    private BigDecimal balance;


    public void deposit(BigDecimal amount) {

        // ...

    }

}


private protege el estado.


Pero el objeto sigue viviendo dentro del mismo espacio de ejecución y comparte muchas de las mismas estructuras que el resto del programa.


En un sistema de actores:


Actor A

  │

  │ message

  ▼

Actor B


B mantiene su estado aislado.

A no puede modificarlo directamente.


No hay:

B.balance = ...


La única forma de interactuar es mediante el protocolo de mensajes.

Y esto se acerca mucho a la idea de Kay: El objeto no expone su implementación; expone una forma de comunicarse.


¿Entonces Actor = Object?

No exactamente.

Esta es una distinción importante.

El modelo de actores es un modelo de computación concurrente con una semántica específica.


Los actores:

  • procesan mensajes;
  • tienen mailboxes;
  • pueden ejecutarse concurrentemente;
  • mantienen estado aislado;
  • se comunican mediante mensajes.


Un objeto tradicional no necesita tener ninguna de esas propiedades.


Por ejemplo:

account.deposit(100);


puede ser una llamada síncrona y ejecutarse en el mismo hilo.


En cambio:

accountActor ! Deposit(100)


representa el envío de un mensaje a una entidad concurrente.


Por lo tanto: Actor y objeto no son sinónimos.

Pero hay una coincidencia conceptual extraordinariamente fuerte.


Para la OOP tradicional:

objeto → método


suele ser el centro del modelo mental.


Para Alan Kay:

objeto → mensaje → objeto

es mucho más importante.


Para los actores:

actor → mensaje → actor

es literalmente el mecanismo fundamental de comunicación.

El modelo de actores no es una implementación de la OOP de Kay.

Pero comparte con ella una idea que resulta mucho más fundamental que las clases y la herencia: las entidades encapsuladas se relacionan mediante mensajes.


En los lenguajes OO tradicionales solemos enseñar:

  • Encapsulamiento
  • Abstracción
  • Herencia
  • Polimorfismo


Pero el modelo de actores nos permite pensar otra lista:

  • Estado privado
  • Comportamiento
  • Mensajes
  • Aislamiento
  • Concurrencia


Y sorprendentemente, esta segunda lista está mucho más cerca de la definición que Kay daba de OOP.

De hecho, la documentación de Akka llega a describir a los actores como objetos que encapsulan estado y comportamiento y se comunican exclusivamente mediante mensajes.


Podemos plantear una hipótesis interesante: El modelo de actores puede ser una realización especialmente cercana a la visión original de Alan Kay de entidades autónomas que mantienen su propio estado y se comunican mediante mensajes.


Incluso podemos decir que lleva algunas de esas ideas más lejos.

En OOP tradicional:


objeto

    │

    └── mensaje


En actores:


actor

    │

    ├── mailbox

    ├── estado privado

    ├── comportamiento

    └── mensajes


Y además aparece algo que Kay consideraba fundamental: el desacoplamiento.

El emisor no necesita conocer cómo funciona internamente el receptor.

Solo necesita conocer cómo comunicarse con él.


Después de todo este recorrido, tal vez ya no tenga demasiado sentido preguntar: “¿Los actores son orientación a objetos?”


La pregunta más interesante sería: ¿Qué pasa si tomamos en serio la idea de que OOP es fundamentalmente comunicación entre entidades encapsuladas?


Ahí Erlang y los sistemas de actores dejan de parecer una idea completamente separada de OOP.

Y aparecen como otra manera de explorar una intuición muy antigua:


        Entidad

           │

           │

        mensaje

           │

           ▼

        Entidad


No clases.

No necesariamente herencia.

No necesariamente jerarquías.

Comunicación.


Quizás eso sea justamente lo que Alan Kay quería decir cuando afirmó que el núcleo de Smalltalk no eran las clases, sino messaging.


Y entonces queda una pregunta fascinante: ¿Y si el modelo de actores no estuviera tan lejos de la Orientación a Objetos como creemos, sino que estuviera recuperando precisamente una parte de OOP que el modelo tradicional de clases terminó dejando en segundo plano?


Java 27: hacia dónde está evolucionando Java

 


Java cambió hace tiempo su ritmo de evolución.

Ya no tenemos que esperar varios años para ver cambios importantes en el lenguaje. Desde Java 10, el JDK recibe una nueva versión cada seis meses.

Y esto produjo algo interesante: las grandes funcionalidades dejaron de aparecer todas juntas y comenzaron a evolucionar durante varias versiones.


Un buen ejemplo son los patrones, la concurrencia estructurada, los valores lazy o las mejoras de rendimiento de la JVM.

Java 25, Java 26 y Java 27 muestran bastante bien hacia dónde está avanzando el lenguaje.


Java 25: un nuevo LTS

Java 25 llegó en septiembre de 2025 y es una versión LTS.

Entre sus novedades hay cambios tanto en el lenguaje como en la JVM y las bibliotecas.


Archivos fuente más simples: Java 25 incorporó los Compact Source Files and Instance Main Methods.

Ahora un programa pequeño puede escribirse de una manera mucho más sencilla:


void main() {

    System.out.println("Hello Java!");

}


Ya no es necesario comenzar con:


public class Main {

    public static void main(String[] args) {

        System.out.println("Hello Java!");

    }

}


La idea no es crear un nuevo lenguaje para principiantes, sino permitir que Java pueda comenzar siendo pequeño y crecer posteriormente hacia el modelo tradicional.


Module Import Declarations


También apareció:

import module java.base;


En lugar de importar individualmente paquetes como:


import java.util.*;

import java.io.*;

import java.nio.*;


se puede importar todo lo exportado por un módulo.


Es especialmente interesante para programas pequeños y ejemplos, donde el ruido de los imports puede ser considerable.


Flexible Constructor Bodies


Java también flexibilizó los constructores.


Antes:

class Person extends Entity {


    Person(String name) {

        super(name);

    }

}


El constructor tenía que comenzar con super(...) o this(...).


Java 25 permite realizar ciertas operaciones antes de esa invocación explícita:


class Person extends Entity {


    Person(String name) {

        name = name.trim();


        super(name);

    }

}



Esto permite validar o preparar datos antes de construir la parte correspondiente a la superclase.


Scoped Values


Otra característica importante de Java 25 fue Scoped Values.

Son una alternativa a ThreadLocal para compartir datos inmutables dentro de un contexto de ejecución.

Esto resulta especialmente interesante cuando se combinan con Virtual Threads y Structured Concurrency.

Es decir, Java empieza a construir un modelo de concurrencia más coherente alrededor de estas nuevas abstracciones.


Y la JVM también sigue cambiando


Java 25 no fue solamente lenguaje.

Por ejemplo, incorporó Compact Object Headers, que permiten reducir el tamaño de las cabeceras de los objetos en arquitecturas de 64 bits.

También continuó el trabajo de Project Leyden, orientado a mejorar startup y warmup mediante mecanismos AOT.


Java 26: menos sintaxis, más JVM

Java 26 llegó en marzo de 2026.

A diferencia de Java 25, no es una versión LTS, pero continuó varias de las líneas iniciadas en versiones anteriores.

Una de las novedades más interesantes fue HTTP/3 en el HTTP Client API.

El cliente HTTP de Java ahora puede trabajar con HTTP/3 sin necesidad de utilizar una biblioteca externa específica para ese protocolo.


Por ejemplo, seguimos teniendo una API similar a:


HttpClient client = HttpClient.newHttpClient();


HttpRequest request =

    HttpRequest.newBuilder(uri)

        .build();


HttpResponse<String> response =

    client.send(request, BodyHandlers.ofString());


pero el JDK amplía las posibilidades de transporte disponibles.

Esto es especialmente interesante para aplicaciones Java que funcionan como clientes de APIs y microservicios.


Project Leyden sigue avanzando

Java 26 continuó el trabajo de AOT Object Caching.

La JVM puede almacenar objetos previamente inicializados en una caché AOT y reutilizarlos durante el arranque.

La diferencia importante es que Java 26 permite hacerlo independientemente del Garbage Collector utilizado, incluyendo ZGC.


La dirección es clara: Java quiere arrancar más rápido sin abandonar el modelo dinámico de la JVM.


Java 26 también incorporó mejoras al Garbage Collector G1 para reducir sincronización y aumentar el throughput.

Y esto prepara el terreno para Java 27, donde G1 pasa a ser el GC predeterminado en todos los entornos.


Java 26 también comenzó a preparar un cambio importante relacionado con los campos final.

El JDK empezó a emitir advertencias cuando se utiliza reflexión profunda para modificar campos final. La intención es avanzar hacia un Java donde final signifique realmente que el campo no puede modificarse mediante estos mecanismos.

Es otro ejemplo de una tendencia importante de integridad y optimización por defecto.


Y llegamos a Java 27

Java 27 continúa muchas de estas ideas.

No es una revolución sintáctica.

De hecho, es bastante más interesante observar qué problemas está intentando resolver.


Entre las principales novedades de Java 27 encontramos nueve JEPs, incluyendo características definitivas y varias que continúan como preview o incubator.


Compact Object Headers por defecto

Java 25 introdujo los Compact Object Headers como una funcionalidad que había que activar explícitamente.

Java 27 da el siguiente paso:pasan a estar habilitados por defecto.

La cabecera tradicional de un objeto podía ocupar 96 bits en determinadas configuraciones.

Con Compact Object Headers puede reducirse a 64 bits.

Esto significa menos memoria por objeto y potencialmente mejor densidad de datos en el heap.

Y lo interesante es que:no necesitamos modificar nuestro código.

Es una mejora de la JVM.


G1 pasa a ser el Garbage Collector por defecto en todos los entornos

G1 ya era el Garbage Collector predeterminado desde Java 9 en la mayoría de los casos.

Java 27 elimina las excepciones para máquinas pequeñas.

Ahora G1 pasa a ser el GC por defecto independientemente de la cantidad de CPU o memoria disponible.


Esto muestra otra tendencia: La JVM cada vez necesita menos configuración manual para obtener un comportamiento razonable.


Structured Concurrency sigue evolucionando

Structured Concurrency aparece nuevamente en Java 27 como séptimo preview.

La idea es tratar un conjunto de tareas concurrentes como una única unidad de trabajo.

Por ejemplo, imaginemos que para construir una respuesta necesitamos consultar:


              ┌── usuario

              │

Request ┼── pedidos

              │

              └── recomendaciones


Podemos ejecutar las tres operaciones concurrentemente.

Pero queremos que tengan una relación estructurada:

  • si una falla, podemos cancelar las demás;
  • esperamos a que terminen;
  • propagamos correctamente los errores;
  • no dejamos tareas ejecutándose accidentalmente.


Ese es precisamente el tipo de problema que Structured Concurrency intenta resolver.

Y tiene una relación natural con Virtual Threads.

Java no está solamente haciendo que crear threads sea barato.

Está intentando mejorar la forma en que pensamos la concurrencia.


Lazy Constants

Los Stable Values de Java 25 evolucionaron en Java 26 hacia Lazy Constants y continúan evolucionando en Java 27.


La idea es interesante: queremos un valor que sea inmutable después de inicializarse, pero cuya inicialización pueda retrasarse hasta que realmente sea necesario.


Conceptualmente:


LazyConstant<Connection> connection =

    LazyConstant.of(() -> createConnection());


La conexión no se crea necesariamente al construir el objeto.

Se crea cuando realmente se necesita.

Y una vez inicializada, permanece estable.

Es una forma más limpia de resolver ciertos casos que tradicionalmente terminábamos implementando con inicialización lazy, sincronización o double-checked locking.


Primitive Types + Pattern Matching


El pattern matching continúa su evolución.

Java 27 vuelve a presentar como preview la posibilidad de trabajar con tipos primitivos dentro de instanceof, switch y patrones.

Es la continuación de una evolución que comenzó varias versiones atrás.

La dirección es bastante clara:


switch (value) {

    case int i -> ...

    case long l -> ...

}


La intención es que el sistema de pattern matching sea cada vez más uniforme y no tenga una separación artificial entre tipos referencia y tipos primitivos.


Criptografía preparada para el mundo post-cuántico

Java 27 también incorpora soporte para Post-Quantum Hybrid Key Exchange en TLS 1.3.

La idea es combinar algoritmos tradicionales con algoritmos resistentes a ataques cuánticos.

Esto apunta a un problema que hoy parece lejano, pero que en seguridad ya se considera relevante: harvest now, decrypt later.


Un atacante puede almacenar tráfico cifrado hoy y tratar de descifrarlo en el futuro cuando disponga de tecnología suficiente.

Java empieza a preparar la infraestructura criptográfica para ese escenario.


JFR puede ocultar información sensible

Java Flight Recorder también recibe una mejora importante.

Java 27 incorpora JFR In-Process Data Redaction.

La idea es evitar que información sensible presente en argumentos, propiedades o variables de entorno termine accidentalmente en una grabación de JFR.


Por ejemplo:

PASSWORD=...

TOKEN=...

API_KEY=...


El objetivo es mejorar la observabilidad sin convertir los mecanismos de diagnóstico en una nueva fuente de filtraciones.


Entonces, ¿hacia dónde está evolucionando Java?

Si miramos Java 25, 26 y 27 en conjunto, aparece una imagen bastante interesante.

Java no está intentando convertirse en otro lenguaje.

La JVM intenta hacerse más inteligente para que el código Java pueda permanecer relativamente simple.

También consiste en conseguir que el mismo código Java sea más rápido, consuma menos memoria, arranque antes, sea más seguro y sea más fácil de escribir.


Y Java 27 continúa exactamente en esa dirección.


jueves, 17 de septiembre de 2026

Quickysort en Joy


Un algoritmo que me gusta mucho es el quicksort, porque es un algoritmo por demás claro. Ya he escrito lo fácil que es implementarlo en Erlang, Rust, haskell y lisp

Ahora le toca a Joy. Básicamente, el algoritmo toma un pivote y agrupa los menores que el pivote al principio y los mayores al final y aplica quicksort a estos dos grupos. Y si la lista es vacía o tiene un elemento, ya está ordenada. 

Vamos al código: 


DEFINE qsort ==

    [small]

    []

    [uncons [>] split]

    [swapd cons concat]

    binrec.


La clave es binrec: recibe cuatro programas entre corchetes:

  1. [small] → condición de terminación.
  2. [] → qué hacer cuando la lista ya es pequeña.
  3. [uncons [>] split] → separar usando el primer elemento como pivot.
  4. [swapd cons concat] → recombinar los resultados.

domingo, 13 de septiembre de 2026

¿Y si en el futuro no escribimos código?

 En un post anterior planteaba una pregunta: Si una IA pudiera construir software completamente sola, ¿en qué lenguaje programaría?


Pero después apareció una pregunta todavía más interesante.

Quizás estamos suponiendo algo que no tiene por qué ser cierto.

Quizás una IA ni siquiera necesitaría un lenguaje de programación.


¿Y si pudiera tomar directamente una historia de usuario, comprender su intención y transformarla en software?


Algo así:

Historia de usuario

            ↓

           IA

            ↓

          ???

            ↓

Código máquina


La pregunta entonces es: ¿Qué debería existir en ese espacio entre la intención humana y el código máquina?

¿Por qué no programar directamente en ensamblador?


Si una IA no tiene las limitaciones de un humano, podríamos pensar: Bueno, que programe directamente en ensamblador.


Después de todo, para una IA escribir esto:


mov eax, [rbp-8]

add eax, 1


no debería ser especialmente difícil.

Incluso podríamos ir un poco más arriba:

  • LLVM IR
  • JVM Bytecode
  • .NET IL
  • WebAssembly


Una IA podría trabajar directamente sobre una representación intermedia y olvidarse completamente de Java, Python o Rust. Pero hay un problema.

Estas representaciones están demasiado lejos de la intención original.


Una historia de usuario podría decir: Como usuario, quiero transferir dinero entre dos cuentas.


Y el ensamblador termina hablando de:

  1. registros
  2. direcciones de memoria
  3. saltos
  4. instrucciones
  5. calling conventions


Nada de eso tiene que ver realmente con una transferencia de dinero.

Son detalles de implementación.

Y probablemente una IA no querría tomar esas decisiones demasiado pronto.


La pregunta no es: ¿Qué registro debería usar?

La pregunta es: ¿Qué significa transferir dinero?


Hoy el proceso es relativamente conocido:


Código fuente

        ↓

Parser

        ↓

AST

        ↓

IR / Bytecode

        ↓

Optimización

        ↓

Código máquina


Y tiene una propiedad extremadamente importante: Es determinista.


Si tenemos:


Código fuente

+

Compilador

+

Configuración


esperamos obtener el mismo resultado.


Eso nos da cosas fundamentales:

  • reproducibilidad;
  • debugging;
  • auditoría;
  • control de versiones;
  • seguridad;
  • testing.


Podemos congelar una versión del software.

Podemos volver atrás.

Podemos comparar cambios.

Podemos saber exactamente qué fue desplegado.


Y si reemplazamos el código por una historia de usuario?

Imaginemos esto:


Historia de usuario

        ↓

IA generativa

        ↓

Código máquina


Suena increíble.

Pero aparece un problema.


Supongamos que la historia dice: Como usuario quiero transferir dinero entre dos cuentas.


La primera vez, la IA podría generar:


TransferService

        ↓

PostgreSQL

        ↓

Transacción ACID


La segunda vez:


Event Sourcing

        ↓

Kafka

        ↓

Consistencia eventual


Y quizás ambas soluciones sean técnicamente correctas.

Pero hay un detalle importante. No son el mismo software.

Incluso aunque la entrada sea exactamente la misma.


Un compilador tradicional no "interpreta" el código cada vez.

No vuelve a pensar qué quiso decir el programador.

Las reglas ya están definidas.


Pero una IA generativa trabaja de otra manera.

Puede existir variabilidad.

Puede cambiar el modelo.

Puede cambiar el contexto.

Puede cambiar la temperatura.

Puede cambiar la recuperación de información.

Puede aparecer una nueva versión del modelo.


Entonces imaginemos algo así:


Historia de usuario

        ↓

IA versión 1

        ↓

Software A


Un año después:


La misma historia de usuario

        ↓

IA versión 2

        ↓

Software B


Y quizás Software B sea incluso "mejor".

Pero no necesariamente sea compatible con A.


Ni tenga los mismos comportamientos.

Ni las mismas garantías.

Entonces, ¿podemos realmente llamar a eso compilación?


Otra posibilidad interesante sería:


Historia de usuario

        ↓

Embedding

        ↓

Espacio vectorial

        ↓

       IA

        ↓

Código máquina



La idea sería que el software no estuviera representado principalmente como texto.

No guardaríamos solamente:

class TransferService


Sino algo más cercano al significado: Una operación que mueve una cantidad positiva de dinero desde una cuenta hacia otra preservando determinadas invariantes.


Una IA podría trabajar sobre representaciones semánticas.

Buscar conceptos similares.

Relacionar patrones.

Encontrar soluciones previamente utilizadas.

Incluso reutilizar conocimiento sin necesidad de importar una librería de la manera tradicional.

Es una idea poderosa.

Pero también peligrosa.

Porque una base vectorial responde muy bien a una pregunta como: ¿Qué se parece a esto?


Pero el software muchas veces necesita responder: ¿Esto es exactamente esto?

Y esas son preguntas muy diferentes.


Supongamos estas dos reglas: 

Un usuario puede acceder al documento.

Un administrador puede acceder al documento.


Para un modelo semántico, ambas frases podrían ser muy similares.

Pero desde el punto de vista del software, la diferencia puede ser crítica.


O estas:

El usuario puede retirar hasta $10.000.

El usuario debe retirar exactamente $10.000.


Una palabra cambia completamente el comportamiento.

Por eso una representación vectorial no debería ser la fuente final de verdad.


Podría ayudar a la IA a:

  • encontrar conocimiento;
  • recuperar patrones;
  • relacionar conceptos;
  • reutilizar soluciones;
  • comprender contexto.


Pero en algún momento necesitamos algo más preciso.

Algo que podamos versionar.

  • Comparar.
  • Verificar.
  • Congelar.


Quizás el futuro tenga dos etapas. Y acá aparece una arquitectura que me resulta mucho más interesante.

La primera parte puede ser generativa.

La segunda debe ser determinista.


```text

┌──────────────┐

│          HUMANO             │

│                                         │

│  Historias de usuario       │

│  Conversaciones              │

│  Requisitos                      │

└────┬─────────┘

               ↓

┌─────────────┐

│   IA GENERATIVA   │

│                                   │

│  Interpreta                 │

│  Pregunta                  │

│  Propone                   │

│  Diseña                     │

│  Explora alternativas │

└────┬───────┘

               ↓

        🔒 SE CONGELA

               ↓

┌───────────────────────┐

│   REPRESENTACIÓN SEMÁNTICA   │

│                                                                 │

│  Entidades                                               │

│  Reglas                                                    │

│  Tipos                                                      │

│  Contratos                                               │

│  Invariantes                                             │

│  Propiedades                                           │

└────────┬──────────────┘

                          ↓

┌────────────────────────┐

│   COMPILACIÓN DETERMINISTA        │

└──────────┬─────────────┘

                                ↓

      ┌────────┼────────┐

      ↓                       ↓                          ↓

     x86                 ARM                WASM



Y para mí, ese punto donde se congela la intención es fundamental.

Antes de ese punto, la IA puede ser creativa.

Puede explorar.

Puede probar arquitecturas.

Puede buscar alternativas.

Puede decidir si algo debería ser:

  • una transacción;
  • un evento;
  • una cola;
  • una función;
  • una tabla;
  • un servicio distribuido.


Pero después de ese punto, ya no debería cambiar de opinión.


Quizás el futuro de la programación no sea:


Humano

   ↓

Código

   ↓

Compilador

   ↓

Máquina


Sino:


Humano

   ↓

Intención

   ↓

  IA

   ↓

Especificación

   ↓

Compilador

   ↓

Máquina


La IA sería responsable de responder: ¿Qué quiso decir el humano?

La especificación respondería: Esto es exactamente lo que el sistema debe hacer.

Y el compilador respondería: Entonces esto es lo que voy a ejecutar.

Quizás esa sea la verdadera frontera


Hoy usamos IA para generar código.

Le damos un prompt y obtenemos:


@Service

public class TransferService {

    ...

}


Pero eso quizás sea solo una etapa intermedia.

Tal vez en el futuro ni siquiera queramos revisar Java. Ni Python. Ni Rust.


Queramos revisar directamente algo como:


ENTITY Account


INVARIANT

    balance >= 0


OPERATION transfer

    FROM source

    TO destination

    AMOUNT amount


REQUIRES

    amount > 0

    source.balance >= amount


ENSURES

    source.balance decreases by amount

    destination.balance increases by amount


Eso sería mucho más cercano a la intención que al código.

Y después podríamos dejar que el sistema decida si eso termina convertido en:

  • Rust;
  • Java;
  • SQL;
  • WebAssembly;
  • bytecode;
  • ensamblador.


Entonces, ¿qué debería ser generativo y qué debería ser determinista?

Creo que esa puede ser la verdadera división.


Generativo:

  • Comprender requisitos
  • Resolver ambigüedades
  • Diseñar
  • Explorar alternativas
  • Optimizar
  • Buscar patrones


Determinista:

  • Representar la especificación
  • Verificar reglas
  • Validar invariantes
  • Compilar
  • Ejecutar


Porque si dejamos que una IA generativa vuelva a interpretar la intención cada vez que "compilamos", entonces algo extraño ocurre.

Ya no estamos compilando el software. Estamos volviendo a diseñarlo cada vez.

Y eso puede ser fantástico durante la creación.

Pero sería aterrador en producción.