Translate

martes, 6 de octubre de 2026

Terraform: módulos para reutilizar infraestructura


En los artículos anteriores fuimos construyendo las piezas fundamentales de Terraform.


Ya vimos:

  • Resources.
  • Providers.
  • Variables.
  • Outputs.
  • State.


Hasta ahora nuestros ejemplos fueron pequeños.

Pero imaginemos un proyecto real.


Podríamos tener:

  • network
  • database
  • application
  • load balancer
  • monitoring
  • security


Y cada una de esas partes puede tener varios recursos.

Nuestro main.tf podría empezar a crecer. Y además podemos terminar repitiendo configuraciones.

Por ejemplo, quizás necesitamos crear la misma infraestructura para:

  • development
  • staging
  • production



¿Cómo evitamos copiar y pegar cientos de líneas?

La respuesta son los modules.


¿Qué es un módulo?

Un módulo de Terraform es, básicamente, un conjunto de archivos Terraform que encapsula una determinada funcionalidad o parte de nuestra infraestructura.

Podemos pensar en un módulo como una función.


Una función recibe parámetros:


entrada

   ↓

 función

   ↓

 resultado


Un módulo hace algo conceptualmente parecido:


variables

    ↓

  módulo

    ↓

resources

    ↓

outputs


Por ejemplo, podríamos crear un módulo llamado: database. Que se encargue de crear una base de datos completa.

Desde afuera solamente necesitamos decir:

  • Quiero una database
  • con este nombre
  • con este tamaño
  • en este ambiente


El módulo se ocupa del resto.


Vamos a crear una estructura sencilla:


terraform/

│

├── main.tf

│

└── modules/

    │

    └── file/

        │

        ├── main.tf

        ├── variables.tf

        └── outputs.tf


Nuestro módulo estará en:

modules/file


Y tendrá tres archivos.

En:

modules/file/main.tf


podemos tener:


resource "local_file" "file" {

  filename = var.filename

  content  = var.content

}


El módulo necesita dos valores:

  • filename
  • content


Por eso los definimos como variables.


En:

modules/file/variables.tf


tenemos:


variable "filename" {

  type = string

}


variable "content" {

  type = string

}


Ahora nuestro módulo tiene una interfaz. Desde afuera no necesitamos saber cómo está implementado.

Solamente necesitamos conocer sus entradas.

Podemos devolver información desde el módulo.


En:

modules/file/outputs.tf


escribimos:


output "filename" {

  value = local_file.file.filename

}


Ahora nuestro módulo tiene:


Entradas

   │

   ├── filename

   └── content

        │

        ▼

      módulo

        │

        ▼

     resource

        │

        ▼

     Outputs

        │

        └── filename


Ya tenemos algo parecido a una función.

Ahora podemos utilizar nuestro módulo desde el main.tf principal:


terraform {

  required_providers {

    local = {

      source = "hashicorp/local"

    }

  }

}


provider "local" {}


module "hello" {

  source = "./modules/file"

  filename = "hello.txt"

  content  = "Hola desde un módulo!"

}


La parte importante es:


module "hello" {

  source = "./modules/file"

  filename = "hello.txt"

  content  = "Hola desde un módulo!"

}


Estamos diciendo: Utilizá el módulo que está en `./modules/file`.

Y le pasamos sus parámetros.

Nuestro main.tf ya no necesita conocer los detalles internos del resource. Y esto nos permite separar responsabilidades. El módulo sabe cómo crear el recurso.

El código principal sabe qué recurso quiere utilizar.

Podemos utilizar el mismo módulo varias veces.


Por ejemplo:


module "hello" {

  source = "./modules/file"


  filename = "hello.txt"

  content  = "Hola!"

}


module "goodbye" {

  source = "./modules/file"


  filename = "goodbye.txt"

  content  = "Chau!"

}


Tenemos dos instancias del mismo módulo.

No copiamos la implementación. La reutilizamos.


Recordemos que nuestro módulo tiene:


output "filename" {

  value = local_file.file.filename

}


Desde el módulo principal podemos acceder al output:


output "hello_file" {

  value = module.hello.filename

}


La sintaxis:

module.hello.filename


significa:

module

  │

  └── instancia "hello"

           │

           └── output "filename"


Esto es muy parecido a acceder al resultado de una función.

Este concepto es muy importante.

Nuestro módulo tiene una especie de API.


Entradas:

  • filename
  • content


Salidas:

  • filename


Internamente podría tener:

  • 10 resources
  • 20 resources
  • 100 resources


Pero quien lo utiliza no necesita conocer todos esos detalles. Esto permite encapsular complejidad.

Y en infraestructura real esto es extremadamente útil.


Supongamos que nuestra aplicación necesita una máquina virtual.

Podríamos tener un módulo:


modules/

└── application/

    ├── main.tf

    ├── variables.tf

    └── outputs.tf


Internamente podría crear:

  • Virtual Machine
  • Network Interface
  • Security Group
  • Disk


Desde nuestro proyecto principal podríamos utilizarlo así:


module "application" {

  source = "./modules/application"


  environment = "production"

  instance_type = "small"

}


El módulo podría encargarse de todos los detalles.

El código principal queda mucho más limpio:


main.tf


  application

      │

      ├── network

      ├── machine

      ├── security

      └── disk


En lugar de tener cientos de recursos directamente en el main.tf.


Ahora podemos combinar lo que vimos en artículos anteriores.


Supongamos que queremos:

  • development
  • staging
  • production


Podemos tener diferentes configuraciones que utilizan los mismos módulos.


Por ejemplo:


terraform/

│

├── modules/

│   └── application/

│

├── environments/

│   ├── development/

│   │   └── main.tf

│   │

│   ├── staging/

│   │   └── main.tf

│   │

│   └── production/

│       └── main.tf


En development:


module "application" {

  source = "../../modules/application"


  environment   = "development"

  instance_type = "small"

}


En producción:


module "application" {

  source = "../../modules/application"


  environment   = "production"

  instance_type = "large"

}


El módulo es el mismo.

Lo que cambia es la configuración que le pasamos.

Esto nos permite evitar una práctica peligrosa:


development/

    copia de todo Terraform


production/

    otra copia de todo Terraform


Si copiamos infraestructura, tarde o temprano las copias empiezan a divergir.

Con módulos podemos reutilizar la implementación.

Hasta ahora utilizamos:


source = "./modules/application"


Es decir, un módulo local.

Pero Terraform también permite utilizar módulos almacenados en otros lugares.


Por ejemplo, podemos obtener módulos desde:

  • Git;
  • registros de módulos;
  • repositorios privados;
  • otros sistemas compatibles con Terraform.


Un ejemplo podría ser:

module "application" {

  source = "git::https://example.com/infrastructure.git"

}


La idea es que un módulo puede convertirse en una pieza reutilizable entre diferentes proyectos.


Los módulos también pueden tener versiones. Cuando una infraestructura se vuelve importante, queremos evitar que una actualización de un módulo rompa todos nuestros ambientes.

Por eso es habitual trabajar con versiones.

Un proyecto puede utilizar una versión determinada.

Esto es especialmente importante cuando los módulos son compartidos por diferentes equipos.


Este es un error bastante común cuando empezamos a utilizar módulos.

Podemos terminar haciendo:

module "resource1"

module "resource2"

module "resource3"

module "resource4"


para absolutamente todo.

Y terminamos con una infraestructura más complicada que la original.

Un módulo tiene sentido cuando encapsula una unidad reutilizable o conceptualmente coherente.


Por ejemplo:

  • network
  • database
  • application
  • monitoring


pueden ser buenos candidatos.

Crear un módulo solamente porque tenemos un resource:

module "s3_bucket"

no necesariamente aporta valor.


Depende del proyecto y de cuánto comportamiento adicional encapsule.


Una forma interesante de pensar Terraform es como si estuviéramos construyendo una aplicación.

En una aplicación tenemos:

  • clases
  • funciones
  • componentes
  • librerías


En Terraform podemos tener:

  • resources
  • modules
  • providers


Un módulo puede encapsular una parte de nuestra infraestructura de la misma manera que una librería encapsula funcionalidad de una aplicación.


Por ejemplo:


Aplicación

│

├── UserService

├── PaymentService

└── NotificationService


Terraform:


Infraestructura

│

├── Network

├── Database

└── Application


La comparación no es perfecta, pero ayuda a entender la idea.


Un módulo no es solamente una forma de ordenar archivos. Este punto es importante.

Podríamos pensar:"Entonces los módulos sirven solamente para tener el código más ordenado."

No.

Su verdadero valor aparece cuando conseguimos:

  • reutilizar infraestructura;
  • encapsular detalles;
  • definir interfaces mediante variables;
  • devolver resultados mediante outputs;
  • versionar componentes;
  • compartir componentes entre proyectos.


Por eso un módulo puede convertirse en una verdadera unidad de diseño de infraestructura.


Terraform State: qué es y por qué es tan importante


En los artículos anteriores vimos cómo Terraform permite definir infraestructura como código.

También vimos que, después de ejecutar Terraform, aparece un archivo llamado:


terraform.tfstate



¿Qué es exactamente?

¿Por qué Terraform necesita guardar información sobre los recursos que creó?

Y quizás la pregunta más importante: ¿Por qué no alcanza con mirar nuestra configuración .tf?

La respuesta está en uno de los conceptos fundamentales de Terraform: el State.


Supongamos que tenemos:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola!"

}


Ejecutamos:

terraform apply


Terraform crea:

hello.txt


Hasta acá todo parece sencillo.


Pero imaginemos que mañana ejecutamos nuevamente:

terraform apply


Terraform tiene que decidir: "¿Tengo que crear el archivo otra vez?"

La respuesta es no.

Terraform necesita saber que ese recurso ya existe y está siendo administrado.

Ahí aparece el State.

El State es información que Terraform mantiene sobre la infraestructura que administra.

Por defecto se almacena en:

terraform.tfstate


Podemos pensar que Terraform tiene tres fuentes de información:

  • La configuración describe lo que queremos.
  • La infraestructura representa lo que existe realmente.
  • El State ayuda a Terraform a saber qué recursos está administrando y cómo se relacionan con la configuración.

Podríamos pensar: "Entonces terraform.tfstate es simplemente otra versión de main.tf."

No.


Son cosas diferentes.

Nuestro código podría decir:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola!"

}


Mientras que el State contiene información sobre el recurso que Terraform está administrando.

Por ejemplo, puede almacenar:

  • el tipo de recurso;
  • su identificador;
  • atributos conocidos;
  • información del provider;
  • relaciones entre recursos;
  • valores que Terraform necesita para gestionar ese recurso.


El State funciona como una especie de mapa entre nuestra configuración y la infraestructura real.

Podemos crear un ejemplo pequeño:


terraform {

  required_providers {

    local = {

      source = "hashicorp/local"

    }

  }

}


provider "local" {}


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola Terraform!"

}


Inicializamos:

terraform init


Y aplicamos:

terraform apply


Ahora tendremos:

hello.txt

terraform.tfstate


Podemos consultar el State mediante:

terraform show


Terraform nos muestra información sobre los recursos que administra.

También podemos listar los recursos que forman parte del State:

terraform state list


Obtendremos algo parecido a:

local_file.hello


Esto nos dice que Terraform está administrando ese recurso.

¿Qué pasa si ejecutamos apply otra vez?

Terraform no debería intentar crear otro archivo.

En lugar de eso veremos algo similar a:

No changes. Your infrastructure matches the configuration.


¿Por qué?

Porque Terraform sabe que:

  1. nuestra configuración declara local_file.hello;
  2. ese recurso ya está registrado en el State;
  3. el recurso existe;
  4. no hay cambios que aplicar.


Por eso Terraform es idempotente en este sentido:


apply

  ↓

crear recurso


apply nuevamente

  ↓

no hay cambios


¿Y si modificamos la configuración?

Supongamos que cambiamos:


content = "Hola Terraform!"


por:


content = "Hola mundo!"



Ahora ejecutamos:

terraform plan


Terraform detecta que nuestra configuración cambió.

Podríamos ver algo parecido a:

~ local_file.hello

  content: "Hola Terraform!" -> "Hola mundo!"


El símbolo: ~ indica que Terraform planea modificar un recurso existente.


Después:

terraform apply


Terraform actualiza el recurso.

¿De dónde sabe Terraform qué cambió?

Esta es justamente una de las razones por las que existe el State.

Terraform puede comparar diferentes piezas de información:


               configuración

                         │

                        ▼

                  Terraform

                         │

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

        ▼                             ▼

      State                    infraestructura


A partir de esa información Terraform puede determinar qué acciones necesita realizar.


Por ejemplo:

crear

modificar

destruir

no hacer nada


Esto se conoce como planificación de cambios.

Ahora viene una situación muy interesante.

Supongamos que Terraform creó una infraestructura y todo está funcionando correctamente.

Pero alguien entra directamente a la consola del proveedor cloud y modifica algo.


Por ejemplo:

Terraform:

instance_type = "small"


Pero alguien entra manualmente y cambia la máquina a:

instance_type = "large"


Ahora tenemos una diferencia: La infraestructura real ya no coincide con lo que Terraform espera.

A esto normalmente se lo llama drift.


Es decir: La infraestructura se alejó del estado que Terraform espera administrar.


¿Qué puede hacer Terraform?

Cuando ejecutamos:

terraform plan


Terraform consulta información del provider y puede detectar diferencias entre lo que conoce y lo que existe.


Por ejemplo, podría mostrarnos que una configuración cambió.

Entonces Terraform puede proponer una corrección.


La idea es volver a:

Configuración

      │

      ▼

   Terraform

      │

      ▼

Infraestructura deseada


En otras palabras: Terraform puede utilizarse para mantener la infraestructura alineada con una configuración declarativa.

¿Terraform detecta absolutamente todo?

No. Esto es importante.

Terraform depende de lo que el provider pueda consultar y representar.

Además, no todos los cambios externos necesariamente tienen el mismo comportamiento.

Por eso no deberíamos pensar: "Terraform mágicamente sabe todo lo que ocurrió."

Terraform trabaja con la información que obtiene de los providers y con el State que mantiene.


¿Qué ocurre si eliminamos terraform.tfstate? Esta es una prueba interesante.

Supongamos que tenemos:

terraform.tfstate

hello.txt


Si eliminamos:

terraform.tfstate


pero dejamos:

hello.txt


la infraestructura sigue existiendo.

Pero Terraform perdió su información sobre ese recurso.


Si volvemos a ejecutar:

terraform plan


Terraform puede considerar que el recurso declarado en la configuración no está registrado en el State.

Es decir, tenemos:


Configuración

      │

      ▼

local_file.hello


   State

      │

      ▼

     vacío


Infraestructura

      │

      ▼

  hello.txt


Esto muestra algo importante: El State no es la infraestructura.

Si borramos el State, no necesariamente borramos la infraestructura.

Hemos perdido la información que Terraform utilizaba para administrarla.


Entonces, ¿podemos borrar el State?

No deberíamos hacerlo arbitrariamente.

En un proyecto real, perder el State puede ser un problema serio.


Por eso es habitual:

  • almacenarlo de forma segura;
  • realizar backups;
  • restringir el acceso;
  • utilizar un backend apropiado;
  • evitar modificarlo manualmente.


Y esto nos lleva a otro problema.

¿Qué pasa cuando trabajamos en equipo?

Supongamos que tenemos:


Emanuel

   │

   └── terraform apply


Juan

   │

   └── terraform apply


Si ambos tienen una copia local de:

terraform.tfstate

tenemos un problema.


Cada uno podría tener un State diferente.


Por ejemplo:

Emanuel

terraform.tfstate

      │

      └── versión A


Juan

terraform.tfstate

      │

      └── versión B


¿Cuál es el correcto?

Por eso, en equipos, normalmente no queremos guardar el State únicamente en el disco local de cada desarrollador.


Necesitamos un backend remoto.

Terraform permite almacenar el State en diferentes backends.


Por ejemplo:


Terraform

    │

    ▼

Backend remoto

    │

    ├── State

    └── Locking


Algunos proveedores cloud ofrecen almacenamiento adecuado para esto.

Por ejemplo, un equipo que trabaja con AWS puede utilizar un backend basado en S3.

La configuración puede tener una forma similar a:


terraform {

  backend "s3" {

    bucket = "terraform-state"

    key    = "production/terraform.tfstate"

    region = "us-east-1"

  }

}


La configuración exacta depende de la versión de Terraform y del backend que utilicemos, pero la idea fundamental es: El State deja de estar únicamente en la máquina de un desarrollador y pasa a estar almacenado en un lugar compartido.


¿Y si dos personas ejecutan Terraform al mismo tiempo?

Este problema es todavía más interesante.

Imaginemos:


Emanuel

   │

   ├── terraform apply

   │

   ▼

   State


Juan

   │

   ├── terraform apply

   │

   ▼

   State


Si ambos intentan modificar la infraestructura simultáneamente, podemos terminar con operaciones que interfieren entre sí.


Por eso aparece otro concepto importante: State locking

Algunos backends permiten bloquear el State mientras una operación está en curso.

Conceptualmente:


Emanuel

   │

   └── apply

         │

         ▼

       LOCK 🔒

         │

         ▼

       State

         

Juan

   │

   └── apply

         │

         ▼

    espera/rechazo


De esta manera evitamos que dos operaciones modifiquen simultáneamente el mismo State.


¿Podemos guardar terraform.tfstate en Git?


En general, no es una buena práctica para proyectos reales.

Hay varias razones.

La primera es que el State puede contener información sensible.


Por ejemplo:

  • identificadores;
  • atributos de infraestructura;
  • información de configuración;
  • y, dependiendo del provider y de los recursos, valores que no queremos distribuir libremente.


Además, Git no es un backend diseñado para coordinar el acceso concurrente al State.

Por eso normalmente tenemos:


Código Terraform

      │

      ▼

     Git


pero:


Terraform State

      │

      ▼

Backend remoto


Son responsabilidades diferentes.

Después de conocer el State, podemos entender mejor qué ocurre cuando ejecutamos:

terraform plan


No es simplemente: "Leé los `.tf` y ejecutá los comandos."

Terraform tiene que construir un plan considerando:


                         Configuración

                                  │

                                 ▼

                           Terraform

                                 │

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

        ▼                                              ▼

      State                                  infraestructura

        │                                                │

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

                                 ▼

                               Plan

                                 │

                                ▼

                     cambios necesarios


Por ejemplo:

+ crear

~ modificar

- destruir


Y recién después:

terraform apply


se aplican esos cambios.


Podemos resumir todo esto con una frase: La configuración dice lo que queremos, la infraestructura dice lo que existe y el State permite a Terraform relacionar ambas cosas.

Por eso el State es tan importante.

No es un simple archivo temporal.

Es una parte fundamental del funcionamiento de Terraform.

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?