Translate

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.


jueves, 10 de septiembre de 2026

¿Se puede hacer un servidor HTTP con Elm?


Cuando pensamos en Elm, normalmente pensamos en frontend.

Elm fue diseñado para construir interfaces web y tiene algunas características que lo hacen particularmente interesante para eso: tipado estático, programación funcional, ausencia de excepciones en tiempo de ejecución y un modelo de arquitectura bastante simple.


Pero... ¿podemos usar Elm para escribir un servidor?

La respuesta es: sí!


El proyecto Elm Simple Server muestra justamente cómo hacerlo.

El truco está en utilizar Platform.worker.

Un worker permite ejecutar un programa Elm sin interfaz gráfica, procesando mensajes y ejecutando comandos.


La arquitectura es aproximadamente:


HTTP Request

     │

     ▼

   Node.js

     │

     │ port

     ▼

    Elm

     │

     │ port

     ▼

   Node.js

     │

     ▼

HTTP Response


Es decir, Node.js se encarga del servidor HTTP y Elm de la lógica de la aplicación.

Esto es bastante parecido a utilizar Elm como un motor de procesamiento detrás de Node.

El proyecto propone una API muy sencilla:


main : Server

main =

    Server.serve <| \req ->

        case req.method of

            "GET" ->

                case req.url of

                    "/" ->

                        ok req Assets.home_elm


                    "/profile" ->

                        ok req Assets.profile_elm


                    "/signup" ->

                        ok req Assets.signUp_elm


                    "/login" ->

                        ok req Assets.login_elm


                    "/logout" ->

                        redirect [ clearCookie ] "/"


                    _ ->

                        notFound


            "POST" ->

                Resolver.succeed (Server.proxy "localhost" 9000)


            _ ->

                notFound_


La idea resulta bastante natural en Elm: un servidor es una función que transforma un Request en una respuesta.


De hecho, el proyecto define:


type Server

serve : (Request -> Resolver Response) -> Server


Y un request es simplemente información:


type alias Request =

    { method : String

    , url : String

    , headers : Dict String String

    , body : Maybe Body

    }


No tenemos objetos, excepciones ni un framework gigantesco.

Tenemos datos + funciones + tipos.


Las respuestas también están modeladas como tipos:


empty : Int -> List Header -> Response

string : Int -> List Header -> String -> Response

file : Int -> List Header -> File -> Response

proxy : String -> Int -> Response


Por ejemplo, conceptualmente podríamos construir una respuesta HTTP de esta manera:


string 200

    [ header "Content-Type" "text/plain" ]

    "Hola desde Elm"


La infraestructura HTTP sigue estando fuera de Elm, pero la lógica que decide qué responder está escrita en Elm.

¿Y las operaciones asíncronas?

Acá aparece Resolver.


El proyecto define:


type Resolver a

succeed : a -> Resolver a

fail : Resolver a -> Resolver a

andThen : (a -> Resolver b)

       -> Resolver a

       -> Resolver b


Además permite realizar requests HTTP y consultas a Acadia.

La idea es mantener los efectos controlados y bastante alineados con el modelo de Elm.


Por ejemplo:

query : Transaction a -> Resolver (Maybe a)


Esto permite que un servidor Elm pueda consultar datos sin convertir Elm en un lenguaje lleno de primitivas de I/O.


Para mí, lo más interesante del proyecto no es tener "un servidor escrito en Elm".

Es la pregunta que hay detrás: ¿Cuánto de un backend realmente necesita acceso directo a la infraestructura?


Si podemos modelar gran parte de nuestra aplicación como funciones puras sobre datos fuertemente tipados, podemos dejar que otra tecnología se encargue de las partes más "sucias":

Ese es precisamente el enfoque que explora el proyecto junto con Acadia: intentar mantener la mayor cantidad posible de código dentro de un "camino rápido" con tipos y garantías fuertes, y utilizar otros lenguajes cuando realmente haga falta.


¿Es un servidor Elm de producción?

No. El propio repositorio lo presenta como un prototipo y una exploración de diseño. Actualmente la implementación utiliza Elm 0.19.2, Node.js y Acadia 0.3.1.

Pero como experimento es interesante porque demuestra algo que muchas veces olvidamos:

Elm no necesita ser exclusivamente UI.


Platform.worker existe desde hace años y permite ejecutar programas Elm sin DOM. El proyecto simplemente lleva esa capacidad un paso más allá y la utiliza para implementar la lógica de un servidor.

Y ahí aparece una idea bastante linda:


Frontend Elm

     │

     ▼

Backend Elm

     │

     ▼

Database


Con el mismo lenguaje y, sobre todo, con la misma filosofía de tipos, datos inmutables y efectos controlados.

No necesariamente es la arquitectura que usaría para cualquier aplicación.

Pero como experimento de hasta dónde podemos llevar Elm, es realmente interesante.


Dejo link: 

https://github.com/acadia-engineering/elm-simple-server

miércoles, 9 de septiembre de 2026

Si una IA pudiera programar completamente sola, ¿qué lenguaje elegiría?


Durante décadas discutimos cuál es el mejor lenguaje de programación.

Java o C#.

Python o JavaScript.

Rust o C++.


Programación funcional u orientada a objetos.

Pero quizás estamos haciendo la pregunta equivocada.

Porque todos esos lenguajes tienen algo en común: fueron diseñados para nosotros.

Para humanos.


Un lenguaje de programación no solo existe para darle instrucciones a una computadora.

También existe para compensar nuestras limitaciones.

Necesitamos nombres descriptivos porque no podemos recordar todo.

Necesitamos archivos y carpetas porque tenemos que organizar mentalmente sistemas enormes.

Necesitamos sintaxis legible porque otro humano tendrá que entender nuestro código.

Queremos escribir menos porque nos cansamos, cometemos errores y tenemos un tiempo limitado.

Por eso discutimos sobre cosas como:


for (User user : users) {

    if (user.isActive()) {

        result.add(user);

    }

}


vs.


users.stream()

     .filter(User::isActive)

     .toList();


Pero una IA no tiene necesariamente esas mismas limitaciones.

Entonces aparece una pregunta interesante: Si una IA pudiera construir software completamente sola, ¿seguiría eligiendo Java, Python, Rust o cualquier otro lenguaje diseñado para humanos?

No estoy tan seguro.


Quizás elegiría algo más declarativo

Los lenguajes imperativos suelen obligarnos a describir cómo queremos hacer algo.

  • recorrer esta colección
  • por cada elemento
  • verificar esta condición
  • agregarlo al resultado


Pero quizás una IA preferiría expresar simplemente:

usuarios activos


O, de manera más formal:


result = { u ∈ users | u.active }



Y dejar que otro sistema decida:

  • qué algoritmo utilizar;
  • cómo paralelizarlo;
  • dónde ejecutarlo;
  • qué estructura de datos conviene;
  • si usar memoria, disco o una base de datos;
  • si compilarlo para CPU, GPU o WebAssembly.


Esta idea ya existe, en diferentes formas, en lenguajes como SQL, Prolog o Datalog.

  • Nosotros declaramos qué queremos.
  • El sistema decide cómo obtenerlo.
  • Para una IA, esa separación podría ser todavía más natural.


Una IA probablemente no tendría miedo a los tipos

Hay tecnologías que muchas veces evitamos porque son demasiado complejas para usar todos los días.

  • Tipos dependientes.
  • Pruebas formales.
  • Teoría de tipos.
  • Lenguajes como Idris, Agda o Lean.


Para un humano, escribir algo como esto puede parecer excesivo:


transfer :

    Account

    -> Account

    -> PositiveAmount

    -> ValidTransaction


Pero para una IA, ¿por qué sería un problema?

Quizás, al contrario, sería una ventaja.

Hoy solemos escribir software así:

código+ validaciones+ tests+ tests de integración+ documentación+ monitoreo

+ "esperemos que nadie rompa esto"


Una IA podría preferir expresar propiedades directamente:

El saldo nunca puede ser negativo.

Un usuario nunca puede acceder a un documento de otra organización.

Esta operación siempre libera el recurso.

Esta lista siempre está ordenada.


Y después construir una implementación que pueda demostrar que esas propiedades se cumplen.


No solo: "Todos los tests pasan".


Sino: "Esta propiedad no puede violarse dentro de este modelo".


Quizás elegiría algo entre Haskell, Idris, Prolog y Datalog

Si obligáramos a una IA a elegir entre los lenguajes que existen hoy, mi apuesta estaría lejos de los lenguajes más populares.


Probablemente miraría hacia ideas presentes en:

  • Haskell, por su composición y su modelo funcional.
  • Idris, por sus tipos dependientes.
  • Agda y Lean, por la verificación formal.
  • Prolog y Datalog, por la programación basada en reglas y relaciones.
  • Erlang, por su modelo de concurrencia y distribución.
  • Rust, cuando el rendimiento y el control fueran importantes.


Pero quizás tampoco elegiría uno.

Podría utilizar todos.

El lenguaje no tendría que ser también el lenguaje de ejecución

Hoy solemos elegir un lenguaje y aceptar sus consecuencias.


Elegiste Java: Entonces tu programa se ejecutará sobre la JVM.

Elegiste Rust: Compilarás código nativo.

Elegiste JavaScript: Terminarás en un navegador o en Node.


Pero una IA podría pensar de otra manera.

Primero define el problema: usuarios activos ordenados por fecha


Después genera la mejor implementación para cada contexto:

Backend crítico      → Rust

Servicio distribuido → Erlang

Consulta de datos    → SQL

Frontend             → WebAssembly

Procesamiento masivo → GPU



En ese escenario, Java, Rust o SQL ya no serían necesariamente el lenguaje en el que se piensa el software.

Serían simplemente targets.

El resultado final de una representación más abstracta.


Pero hay una posibilidad todavía más radical. Quizás una IA ni siquiera elegiría un lenguaje textual.


Nosotros pensamos en:

class User {

    String name;

}


Porque programamos escribiendo texto.

Pero una IA podría trabajar directamente sobre una representación interna.

  • Un grafo.
  • Un AST.
  • Un sistema de tipos.
  • Un conjunto de restricciones.


Algo conceptualmente parecido a esto:


Entity

 └── User

      ├── name : String

      └── organization : Organization



Rule

 └── User can access Document

      when same organization

      and permission = READ



El código fuente podría convertirse en algo secundario.

Una interfaz.

Una traducción.

Una forma de que los humanos podamos mirar lo que está pasando.


Quizás el problema nunca fue encontrar el mejor lenguaje

Quizás todos los lenguajes que usamos hoy son soluciones a un problema específico: ¿Cómo hacemos para que los humanos puedan construir software cada vez más complejo?


Pero si quitamos al humano del proceso de implementación, la pregunta cambia.

Ya no necesitamos optimizar para:

  • facilidad de escritura;
  • facilidad de aprendizaje;
  • cantidad de caracteres;
  • convenciones;
  • legibilidad;
  • onboarding;
  • documentación para otros desarrolladores.


Podemos optimizar para otras cosas:

  • correctitud;
  • verificabilidad;
  • rendimiento;
  • capacidad de transformación;
  • optimización automática;
  • adaptación a diferentes plataformas.


Y entonces quizás el lenguaje ideal para una IA sería algo parecido a:


Especificación + Tipos + Restricciones + Propiedades + Pruebas formales

      ↓

Modelo semántico

      ↓

Optimización automática

      ↓

Java · Rust · SQL · WASM · GPU · Assembly


Y quizás, en ese futuro, seguiremos escribiendo Java, Python o Rust.

Pero no necesariamente porque sean los mejores lenguajes para construir software.


Tal vez los sigamos usando porque son los mejores lenguajes para que nosotros podamos entender qué está construyendo la IA.


Durante décadas intentamos hacer que los lenguajes de programación fueran cada vez más cercanos al pensamiento humano.

Quizás el próximo paso sea el contrario.


Dejar que la IA programe en el lenguaje que le resulte natural… y construir una traducción para nosotros.


Y tal vez por eso lenguajes que hoy parecen demasiado académicos, extraños o poco prácticos —como Idris, Agda, Prolog o APL— tengan ideas que se parezcan mucho más al futuro de la programación de lo que imaginamos.


lunes, 7 de septiembre de 2026

¿Qué significa que una operación sea lazy en C++?



Cuando trabajamos con Ranges y Views en C++, muchas operaciones son lazy.

Pero, ¿qué significa exactamente?

Significa que una operación no se ejecuta inmediatamente.


Veamos un ejemplo:

#include <ranges>

#include <vector>

#include <iostream>


int main() {

    std::vector<int> numbers = {1, 2, 3, 4, 5};


    auto result = numbers

        | std::views::filter([](int x) {

            std::cout << "Filtering: " << x << '\n';

            return x % 2 == 0;

        });


    std::cout << "Pipeline created\n";

}


Podríamos esperar que al crear result se ejecutara el filter.

Pero no.


La salida será: Pipeline created

El filtro todavía no se ejecutó.


¿Por qué?

Porque esto:

auto result = numbers

    | std::views::filter(...);


no procesa los elementos.


Simplemente crea una View que describe cómo obtenerlos.

La operación se ejecuta cuando necesitamos los valores.


Por ejemplo:

for (auto value : result) {

    std::cout << value << '\n';

}


Ahora sí veremos:

Filtering: 1

Filtering: 2

2

Filtering: 3

Filtering: 4

4

Filtering: 5


El filter se ejecuta a medida que recorremos los elementos.

Una operación eager procesa los datos inmediatamente.


Por ejemplo:

std::vector<int> result;


for (int value : numbers) {

    if (value % 2 == 0) {

        result.push_back(value);

    }

}


Cuando termina este código, result ya contiene:

[2, 4]


Los elementos fueron procesados inmediatamente.

Con una View:

auto result =

    numbers

    | std::views::filter([](int x) {

        return x % 2 == 0;

    });


todavía no tenemos una nueva colección.


Tenemos una forma de decir: Cuando alguien recorra estos datos, devolvé solamente los números pares.


¿Por qué es útil? 

Supongamos este pipeline:

auto result = numbers

    | std::views::filter(isEven)

    | std::views::transform(multiplyByTen)

    | std::views::take(2);


Podemos leerlo así:

numbers

   |

filter pares

   |

multiplicar por 10

   |

tomar 2


Como las operaciones son lazy, C++ no necesita necesariamente procesar toda la colección.


Si solamente necesitamos:

2 elementos


el pipeline puede detenerse cuando los encuentra.


Por ejemplo:


[1, 2, 3, 4, 5, 6, 7, 8]


        ↓


filter pares


[2, 4, 6, 8]


        ↓


transform x * 10


[20, 40, 60, 80]


        ↓


take(2)


[20, 40]


La diferencia es que una evaluación eager podría procesar todos los elementos antes de aplicar take.

Con una View, las operaciones pueden realizarse solamente cuando son necesarias.


Las operaciones trabajan elemento por elemento. Esta es una de las cosas más interesantes de los pipelines lazy.


Veamos:


auto result = numbers

    | std::views::filter([](int x) {

        std::cout << "filter: " << x << '\n';

        return x % 2 == 0;

    })

    | std::views::transform([](int x) {

        std::cout << "transform: " << x << '\n';

        return x * 10;

    })

    | std::views::take(2);


Cuando recorremos el resultado:

for (auto value : result) {

    std::cout << "result: " << value << '\n';

}


El procesamiento ocurre más parecido a esto:

filter: 1

filter: 2

transform: 2

result: 20

filter: 3

filter: 4

transform: 4

result: 40


No necesariamente ocurre así:


  filtrar todos

        ↓

transformar todos

        ↓

tomar dos


Sino más parecido a:


elemento

   ↓

filter

   ↓

transform

   ↓

resultado


Y después:


siguiente elemento

   ↓

filter

   ↓

transform

   ↓

resultado


Lazy no significa que nunca se ejecuta, una View no evita el procesamiento.

Simplemente lo retrasa hasta que alguien necesita los datos.


Por ejemplo:


auto result = numbers

    | std::views::filter(isEven);


Hasta acá:

No procesamos los elementos


Pero cuando hacemos:


for (auto value : result) {

    // ...

}



ahí comienza la evaluación.


Podemos resumirlo así:


Crear View

    ↓

No procesar todavía

    ↓

Recorrer los datos

    ↓

Procesar elementos cuando son necesarios


Podemos pensar una View como una receta.


Esto:

auto result =

    numbers

    | std::views::filter(isEven)

    | std::views::transform(multiplyByTen);


no es el resultado final.


Es más parecido a decir: Tengo estos datos. Cuando alguien los necesite, primero filtrá los pares y después multiplicalos por diez.

La ejecución real ocurre cuando alguien consume los elementos.


Cuando una operación es lazy significa que:

No procesa los elementos inmediatamente.

Describe cómo obtener o transformar los datos.

Se ejecuta cuando alguien necesita los resultados.

Puede evitar trabajo innecesario.

Permite construir pipelines eficientes.


En C++ moderno, las Views nos permiten escribir:


numbers

    | filter

    | transform

    | take;


sin crear necesariamente una colección intermedia en cada paso.

Y quizás esa sea la idea más importante: Una View no es el resultado. Es una forma de llegar al resultado.


domingo, 6 de septiembre de 2026

Ranges y Views: programación funcional en C++



Cuando pensamos en C++, normalmente pensamos en for, iteradores y algoritmos como std::sort o std::find.

Pero C++ moderno incorporó una forma diferente de trabajar con colecciones: Ranges y Views.


Veamos un ejemplo:

#include <ranges>

#include <vector>

#include <iostream>


int main() {

    std::vector<int> numbers = {1, 2, 3, 4, 5};


    auto result = numbers

        | std::views::filter([](int x) {

            return x % 2 == 0;

        })

        | std::views::transform([](int x) {

            return x * 10;

        });


    for (auto value : result) {

        std::cout << value << " ";

    }

}



Resultado: 20 40


El código se puede leer como un pipeline:

[1, 2, 3, 4, 5]

        ↓

filter(x es par)

        ↓

[2, 4]

        ↓

transform(x * 10)

        ↓

[20, 40]


¿Qué es un Range?

Un Range es una abstracción que representa una secuencia de elementos.


Por ejemplo:

std::vector<int>


puede ser un Range.


También:

std::list<int>

std::array<int, 10>


La idea de Ranges es permitir trabajar con diferentes estructuras utilizando una interfaz común.

En lugar de pensar vector o list podemos pensar en secuencia de elementos


¿Qué es una View?

Una View es una forma de ver o transformar un Range sin necesariamente crear una nueva colección.


Por ejemplo:

auto evenNumbers =

    numbers

    | std::views::filter([](int x) {

        return x % 2 == 0;

    });


Podemos pensar que evenNumbers representa: [2, 4]

Pero no necesariamente se creó un nuevo std::vector.

En cambio, tenemos una vista sobre los datos originales.

Esto nos lleva a una característica muy importante: Lazy evaluation


Las Views normalmente trabajan de forma lazy.

Esto significa que las transformaciones no se ejecutan necesariamente en el momento en que creamos el pipeline.


Por ejemplo:

auto result = numbers

    | std::views::filter(isEven)

    | std::views::transform(multiplyByTen);


En este punto estamos definiendo cómo se van a transformar los datos.

La evaluación ocurre cuando recorremos el resultado:


for (auto value : result) {

    std::cout << value << " ";

}


Esto puede evitar crear colecciones intermedias.

Sin Views podríamos escribir:


std::vector<int> evenNumbers;


for (auto value : numbers) {

    if (value % 2 == 0) {

        evenNumbers.push_back(value);

    }

}


std::vector<int> result;


for (auto value : evenNumbers) {

    result.push_back(value * 10);

}



Con Ranges:


auto result = numbers

    | std::views::filter([](int x) {

        return x % 2 == 0;

    })

    | std::views::transform([](int x) {

        return x * 10;

    });


El código expresa directamente la intención: filtrar → transformar


¿Es programación funcional?

C++ sigue siendo C++.


Pero Ranges y Views incorporan ideas que resultarán familiares para quienes trabajan con programación funcional:

filter

map

pipelines

lazy evaluation


Por ejemplo: std::views::filter(...)

es similar a: filter


Y: std::views::transform(...)

es similar a: map


También podemos combinar operaciones:

numbers

    | std::views::filter(...)

    | std::views::transform(...)

    | std::views::take(10);


El resultado es un pipeline de transformaciones.


Pero con C++20 y C++23, conceptos como pipelines, transformaciones y lazy evaluation forman parte del C++ moderno.


sábado, 5 de septiembre de 2026

¿Qué tan lejos está la OOP tradicional del pensamiento de Alan Kay?


Kay no dijo que “C++ no sea OOP” en una definición formal; de hecho, en 1997 dijo que cuando acuñó object-oriented no tenía C++ en mente. Y en 2003 definió su propia concepción de OOP poniendo en el centro messaging, encapsulamiento del estado/proceso y extreme late-binding.

Cuando pensamos en Programación Orientada a Objetos, probablemente pensamos en algo así:


Clase

  ↓

Objeto

  ↓

Encapsulamiento

  ↓

Herencia

  ↓

Polimorfismo


Pensamos en class, extends, interfaces, métodos virtuales, getters, setters, jerarquías de tipos...

Y probablemente pensemos en lenguajes como C++, Java o C#.


Pero hay algo curioso: Esta no era exactamente la idea de Orientación a Objetos que tenía Alan Kay.


Y esto es especialmente interesante porque Alan Kay fue quien acuñó el término “object-oriented programming” mientras trabajaba en las ideas que terminarían dando lugar a Smalltalk. 

Entonces vale la pena hacer una pregunta incómoda: ¿Qué tan lejos está la OOP que aprendimos de la OOP que Kay tenía en mente?


La enseñanza tradicional suele comenzar con:

Encapsulamiento

Abstracción

Herencia

Polimorfismo


Y después aparecen las clases:


class Dog extends Animal {

    @Override

    void speak() {

        System.out.println("Woof");

    }

}


Esto funciona perfectamente como modelo de programación.

Pero observemos algo:ninguna de estas ideas aparece como el centro de la definición que Kay dio de OOP.


En 2003, cuando le preguntaron qué significaba para él object-oriented programming, respondió:

 “OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.”


Para Kay, el núcleo estaba en: messaging; mantener localmente el estado y proceso; proteger y ocultar ese estado; extreme late-binding. 


No aparece: herencia; clases; jerarquías; interfaces; getters y setters.

Y esto no es una casualidad.


El objeto de Kay no es simplemente una estructura con métodos

Esta diferencia es fundamental.

Es común imaginar objetos como: datos + funciones asociadas a esos datos.

Kay tenía otra metáfora.


Pensaba los objetos como células biológicas o como computadoras individuales conectadas a una red: entidades independientes que poseen su propio estado y se comunican mediante mensajes.


Lo importante no es solamente qué objetos existen.

Lo importante es: cómo se comunican.


De hecho, Kay llegó a decir que lamentaba haber puesto tanto énfasis en la palabra objects, porque podía hacer que la gente se concentrara en la parte menos importante. Para él, “the big idea is messaging”. 


Esta diferencia puede parecer sutil, pero es enorme.

En la forma habitual de pensar:


dog.speak();


tendemos a pensar: “Estoy invocando el método speak() de Dog.”


En el modelo de mensajes:

dog ← speak


la idea es más parecida a: “Le envío a este objeto el mensaje speak.”


No necesito conocer cómo responde.

Puede:

  • ejecutar código local;
  • cambiar su estado;
  • delegar;
  • enviar otro mensaje;
  • crear otro objeto;
  • incluso responder de una manera que yo no conocía cuando escribí el código.


El mensaje establece una frontera de comunicación.

Y ahí aparece el late binding.

En la OOP tradicional podemos tener:


Animal* animal = new Dog();

animal->speak();


y gracias a virtual, el método concreto se resuelve dinámicamente.

Eso ya es late binding.

Pero para Kay la idea debía llevarse mucho más lejos.


No se trata simplemente de decidir dinámicamente qué implementación de un método ejecutar.

Se trata de construir sistemas donde las partes estén desacopladas mediante mensajes y donde podamos cambiar las implementaciones sin que los demás componentes necesiten conocer esos detalles.


El mensaje define el contrato de comunicación.


La implementación queda detrás de esa frontera.

Acá aparece uno de los puntos más interesantes de toda esta discusión.


La OOP tradicional suele presentar:


        Animal

       /      \

     Dog      Cat


como algo central.


Pero la herencia no es necesaria en la definición de Kay.

Incluso en la historia temprana de estas ideas, la herencia no era necesariamente el punto de partida. El propio Kay explicó que hubo distintas líneas derivadas de Simula: una relacionada con el camino biológico/de redes que él siguió y otra relacionada con los abstract data types, que tuvo mucha más influencia posteriormente. 

Es decir: La OOP que terminó popularizándose estuvo fuertemente influenciada por el modelo de clases y tipos, pero ese no era necesariamente el centro de la visión original de Kay.


Entonces apareció C++. Y acá podemos entender por qué C++ es tan importante en esta historia.

C++ hizo que la orientación a objetos se volviera masiva.

Pero la OOP de C++ está fuertemente expresada mediante:


classes

inheritance

virtual functions

types

access control


Es un modelo muy poderoso.

Pero no es exactamente la metáfora de:


computadoras independientes

        ↕

     mensajes

        ↕

computadoras independientes


que inspiraba a Kay.


Por eso la pregunta no debería ser: ¿C++ está mal?

No.


La pregunta es: ¿C++ popularizó una interpretación particular de la orientación a objetos que terminó siendo confundida con la orientación a objetos en general?

Y probablemente esta sea una de las razones por las que hoy discutimos tanto si Rust, Go, JavaScript o incluso C son “orientados a objetos”.

Estamos utilizando una definición que mezcla diferentes tradiciones.


¿Y Smalltalk? Acá está la parte paradójica.

El lenguaje que nació de las ideas de Kay fue Smalltalk.

Y, sin embargo, incluso Smalltalk terminó desarrollando un fuerte énfasis en clases.


Kay llegó a aclarar que Smalltalk no debía entenderse simplemente como su sintaxis, su class library, ni siquiera como clases. Para él, el núcleo era el messaging.


Esto nos deja una idea bastante provocadora: Incluso el lenguaje que creó para explorar la orientación a objetos terminó siendo interpretado desde una perspectiva más centrada en clases de la que Kay consideraba fundamental.


Entonces, ¿qué es realmente OOP? Quizás después de todo este recorrido deberíamos volver a la pregunta original.


¿Es esto?

class

inheritance

encapsulation

polymorphism


¿O es esto?

objects

     ↕

 messages

     ↕

objects


Kay parece inclinarse claramente por la segunda visión.

Su definición de OOP es deliberadamente pequeña: messaging, encapsulamiento del estado/proceso y late binding extremo. 


Y esto cambia nuestra manera de mirar los lenguajes.

Un lenguaje puede tener clases y herencia y, aun así, utilizar una visión de objetos bastante diferente.


Y otro lenguaje puede no tener herencia y, sin embargo, compartir algunas ideas importantes con la orientación a objetos.


Por eso quizás la pregunta: “¿Rust es orientado a objetos?”


es menos interesante que: “¿Qué modelo de objetos utiliza Rust?”


Y lo mismo podemos preguntar de Go, C++, Java, C#, JavaScript o Smalltalk.


Tal vez la mayor diferencia entre la OOP tradicional y la visión de Kay sea esta:

La OOP tradicional nos enseñó a pensar en objetos como entidades que tienen datos y métodos.

Kay quería que pensáramos en objetos como pequeñas entidades autónomas que se comunican.


Una pone el foco en: ¿qué es este objeto?

La otra en ¿cómo se comunica este objeto con el resto del sistema?

Y esa diferencia no es menor.

Porque quizás la esencia de la orientación a objetos nunca estuvo en construir buenas jerarquías de clases.

Quizás estuvo, desde el principio, en diseñar buenas redes de comunicación entre entidades independientes.

Y si esto es así, entonces la pregunta que queda abierta es todavía más interesante:


¿Cuánto de la OOP que enseñamos hoy es realmente orientación a objetos y cuánto es simplemente programación basada en clases?


jueves, 3 de septiembre de 2026

El operador | en C++23: construyendo pipelines



En C++, el operador | siempre existió.

Tradicionalmente lo conocemos como el operador OR a nivel de bits:

int result = 5 | 3;


Pero con Ranges, el operador | también permite construir pipelines de transformaciones.


Por ejemplo:

#include <ranges>

#include <vector>

#include <iostream>


int main() {

    std::vector<int> numbers = {1, 2, 3, 4, 5};


    auto result = numbers

        | std::views::filter([](int x) {

            return x % 2 == 0;

        })

        | std::views::transform([](int x) {

            return x * 10;

        });


    for (auto value : result) {

        std::cout << value << " ";

    }

}


Resultado:

20 40


Podemos leer este código de izquierda a derecha:

numbers

   |

filter

   |

transform


Primero:

[1, 2, 3, 4, 5]


Aplicamos:

filter(x -> x es par)


Resultado:

[2, 4]


Luego:

transform(x -> x * 10)


Resultado:

[20, 40]


El código:

numbers

    | std::views::filter(...)

    | std::views::transform(...);


representa un pipeline.


Cada operación recibe el resultado de la operación anterior.

El operador | sigue siendo el mismo operador definido por C++.

Lo que cambia es que la biblioteca de Ranges utiliza operator overloading para darle un nuevo comportamiento cuando trabajamos con vistas.


Conceptualmente:

numbers | operation


es similar a escribir:

operation(numbers)


Por ejemplo:

numbers

    | std::views::filter(isEven)


podemos imaginarlo como:

std::views::filter(numbers, isEven);


La ventaja del operador | es la legibilidad.


Comparemos:

auto result =

    std::views::transform(

        std::views::filter(numbers, isEven),

        multiplyByTen

    );


Con:

auto result =

    numbers

    | std::views::filter(isEven)

    | std::views::transform(multiplyByTen);


Ambas expresan la misma idea, pero el pipeline permite leer el código en el orden en que ocurren las transformaciones.


Este estilo puede resultar familiar para quienes trabajan con otros lenguajes.


Por ejemplo, en F#:

data

|> filter

|> transform


O en Elixir:

data

|> filter()

|> transform()


En C++:


data

    | std::views::filter(...)

    | std::views::transform(...);


La idea es similar:

dato → operación → operación → resultado


En un post anterior vimos que C++23 no tiene una operación llamada flatMap.

Pero podemos construirla con un pipeline:

auto result = numbers

    | std::views::transform([](int x) {

        return std::vector{x, x * 10};

    })

    | std::views::join;


Podemos leerlo como:


numbers

   |

transform

   |

join


Es decir: flatMap = transform + join

C++23 no se convirtió en un lenguaje funcional.


Pero con Ranges y Views, C++ permite escribir código mucho más declarativo y expresar transformaciones sobre datos como pipelines.


Y quizás esa sea una de las características más interesantes del C++ moderno.


martes, 1 de septiembre de 2026

FlatMap en C++23


flatMap es una operación muy común en programación funcional.

La idea es simple: aplicamos una función a cada elemento de una colección, pero esa función devuelve otra colección.


Por ejemplo:

[1, 2, 3]


Aplicamos:

x -> [x, x * 10]


Con un map obtendríamos:

[[1, 10], [2, 20], [3, 30]]


Con flatMap obtenemos:

[1, 10, 2, 20, 3, 30]


En otras palabras: flatMap = map + flatten


¿Cómo hacemos flatMap en C++23?

C++ no tiene una función llamada flatMap, pero podemos conseguir el mismo comportamiento utilizando std::views::transform y std::views::join.


#include <iostream>

#include <ranges>

#include <vector>


int main() {

    std::vector<int> numbers = {1, 2, 3};


    auto result = numbers

        | std::views::transform([](int x) {

            return std::vector{x, x * 10};

        })

        | std::views::join;


    for (auto value : result) {

        std::cout << value << " ";

    }

}


Resultado: 1 10 2 20 3 30


Lo que ocurre es:

transform


produce:

[[1, 10], [2, 20], [3, 30]]


Y luego: join


aplana el resultado:

[1, 10, 2, 20, 3, 30]


Por lo tanto, en C++23 podemos pensar que:


transform(...) | join


es el equivalente a:

flatMap(...)


En Scala se puede hacer : numbers.flatMap(x => List(x, x * 10))

En Java:

numbers.stream()

       .flatMap(x -> Stream.of(x, x * 10))

       .toList();


Y en C++23:

numbers

    | std::views::transform(...)

    | std::views::join;


Distinta sintaxis, misma idea: flatMap = transformar + aplanar

Y aunque C++ no tenga un flatMap directamente en su biblioteca estándar, con Ranges podemos expresar el mismo concepto de una forma bastante declarativa.


domingo, 30 de agosto de 2026

¿La herencia es realmente un pilar de la Orientación a Objetos?


En el post anterior cuestionábamos una idea bastante instalada: que un lenguaje necesita clases, herencia, encapsulamiento y polimorfismo para ser considerado orientado a objetos.


Si entendemos la orientación a objetos como una forma de modelar sistemas mediante objetos que encapsulan comportamiento y colaboran entre sí, aparece una pregunta inevitable: ¿La herencia es realmente necesaria?

La respuesta corta es: no.

Y dos lenguajes modernos lo muestran muy bien: Go y Rust.

Si tenemos:

Dog

Cat

Horse


podemos identificar características comunes y construir una abstracción:


            Animal

       /          \           \

     Dog      Cat    Horse

              

La herencia de clases es una de las formas tradicionales de representar esa relación:


Animal

  ↑

Dog


Pero la relación conceptual: “Dog puede comportarse como Animal”, no implica necesariamente que tengamos que utilizar herencia.

Podemos expresar la misma idea de otras maneras.

Y ahí aparecen los traits de Rust y las interfaces de Go.


Go no tiene clases ni herencia de clases.

Sin embargo, podemos definir una abstracción:


type Animal interface {

    Speak()

}


Y después diferentes tipos pueden satisfacer esa interfaz:


type Dog struct{}


func (Dog) Speak() {

    fmt.Println("Woof")

}


type Cat struct{}


func (Cat) Speak() {

    fmt.Println("Meow")

}


Podemos trabajar con la abstracción:


func makeSpeak(animal Animal) {

    animal.Speak()

}


Y utilizar:

makeSpeak(Dog{})

makeSpeak(Cat{})


No existe: Dog extends Animal


Pero sí existe algo conceptualmente importante:

Dog ───────► Animal

Cat ───────► Animal


Los tipos concretos satisfacen un contrato común.

Y esto permite polimorfismo.

La diferencia fundamental es que Go utiliza composición e interfaces, en lugar de una jerarquía de clases.


Rust sigue una idea similar.

Podemos definir:


trait Animal {

    fn speak(&self);

}


y después implementar ese comportamiento:


struct Dog;


impl Animal for Dog {

    fn speak(&self) {

        println!("Woof");

    }

}


Otro tipo puede hacer lo mismo:


struct Cat;


impl Animal for Cat {

    fn speak(&self) {

        println!("Meow");

    }

}


Podemos utilizar el trait como abstracción:


fn make_speak(animal: &dyn Animal) {

    animal.speak();

}


Y nuevamente tenemos polimorfismo:

        Animal

        /    \

      Dog    Cat


pero sin una jerarquía de herencia.

El mecanismo es diferente.

Entonces, ¿qué reemplaza a la herencia? En realidad, la pregunta está ligeramente mal formulada.

No necesitamos reemplazar la herencia.

Necesitamos resolver los problemas que tradicionalmente intentábamos resolver mediante herencia.


Por ejemplo:

Abstracción


Animal

 └── speak()


Go lo expresa mediante interfaces.

Rust lo expresa mediante traits.


Polimorfismo


Animal

  ↑

Dog

Cat


Ambos lenguajes permiten que diferentes tipos proporcionen el mismo comportamiento.


Reutilización

En lugar de crear jerarquías:


Animal

 ├── Dog

 │    └── BigDog

 │         └── SuperBigDog


podemos utilizar composición.


En Rust:

struct Engine;


struct Car {

    engine: Engine,

}


El Car no necesita ser un Engine. Simplemente tiene un Engine.

Esta diferencia parece pequeña, pero conceptualmente es enorme:

Herencia expresa “es un”. Composición expresa “tiene un”.


Y muchos diseños orientados a objetos pueden construirse perfectamente utilizando composición.

¿Entonces la herencia no sirve?

Por supuesto que sí.


La herencia puede ser muy útil cuando realmente existe una relación de especialización que queremos representar. El problema aparece cuando empezamos a utilizarla simplemente para:

  • reutilizar código;
  • compartir estado;
  • evitar duplicación;
  • construir jerarquías artificiales.


Por eso una buena regla de diseño suele ser: Prefer composition over inheritance.

No porque la herencia sea mala, sino porque reutilizar implementación y modelar una relación de subtipo son problemas diferentes.


Acá aparece una diferencia conceptual que muchas veces queda escondida.

Podemos tener:


Generalización

       │

       ▼

   Abstracción


y podemos representar esa generalización mediante: Herencia

pero también mediante:

  • Interfaces
  • Traits
  • Composición
  • Protocolos
  • Duck typing


Por lo tanto: La generalización es un concepto del modelo; la herencia es una de las herramientas que un lenguaje puede proporcionar para expresarla.

La abstracción no pertenece exclusivamente a las clases.


Entonces, ¿Go y Rust son orientados a objetos?

Acá la respuesta vuelve a depender de nuestra definición.


Si definimos OOP como: “programar utilizando clases y herencia”

entonces Go y Rust no son lenguajes orientados a objetos.


Pero esa definición es demasiado restrictiva.

Si consideramos fundamentales conceptos como:

  • encapsulamiento;
  • objetos con estado y comportamiento;
  • abstracción;
  • polimorfismo;
  • composición;
  • colaboración entre entidades;


entonces Go y Rust pueden expresar muchos diseños que reconocemos como orientados a objetos, aunque utilicen mecanismos diferentes.


Y esto nos lleva nuevamente a una conclusión importante: No deberíamos confundir orientación a objetos con el modelo de clases y herencia que popularizaron C++ y Java.


La herencia de clases es una implementación posible de determinadas ideas de OOP.

No necesariamente es la esencia de OOP.


¿Y Smalltalk?

Esto nos devuelve al origen.

En Smalltalk, la idea central de la orientación a objetos no era: clases + herencia + polimorfismo


sino algo mucho más cercano a: objetos + mensajes + comportamiento


Desde esa perspectiva, resulta menos sorprendente que lenguajes modernos puedan adoptar algunas ideas de OOP sin adoptar necesariamente las clases y la herencia tradicional.


Quizás entonces deberíamos cambiar la pregunta.

En lugar de: ¿Este lenguaje tiene herencia? preguntar: ¿Qué modelo utiliza el lenguaje para representar objetos, comportamiento, abstracción y colaboración?


Porque la ausencia de herencia no implica necesariamente la ausencia de orientación a objetos.

Y quizás esa sea la conclusión más importante:

La herencia es una herramienta para implementar determinadas relaciones entre objetos. La orientación a objetos es un modelo mucho más amplio que la herencia.


En definitiva, no necesitamos herencia para pensar en objetos.


Y tampoco necesitamos clases.

Pero entonces aparece una pregunta todavía más interesante: ¿Necesitamos siquiera clases para hacer Orientación a Objetos?