Translate

Mostrando las entradas para la consulta scala ordenadas por fecha. Ordenar por relevancia Mostrar todas las entradas
Mostrando las entradas para la consulta scala ordenadas por fecha. Ordenar por relevancia Mostrar todas las entradas

lunes, 10 de agosto de 2026

La programación concurrente es un paradigma o no?


Es muy común encontrar libros, cursos e incluso materias universitarias que presentan la programación concurrente como un paradigma, junto a la programación imperativa, orientada a objetos o funcional.

En mi opinión, esa clasificación es incorrecta.

Un paradigma de programación describe cómo se modela y expresa un programa.

Un paradigma de programación es una filosofía o un estilo para construir software. Define las reglas, el modo de pensar y la estructura para organizar y ejecutar las instrucciones de un programa. 

Por ejemplo:

  • Imperativo: el programa es una secuencia de instrucciones que modifican el estado.
  • Orientado a objetos: el programa se organiza mediante objetos que colaboran entre sí.
  • Funcional: el programa se construye a partir de funciones y composición.
  • Lógico: el programa se expresa mediante hechos y reglas.

La concurrencia responde a otra pregunta completamente distinta: ¿Cómo se ejecutan varias tareas al mismo tiempo y cómo se coordinan entre ellas?

Es decir, la concurrencia es un modelo de programación (o incluso un modelo de ejecución), no un paradigma en sí mismo.

La prueba es muy sencilla: la concurrencia puede resolverse desde prácticamente cualquier paradigma.


  • Java y C# utilizan threads, virtual threads, async/await y actores desde un enfoque imperativo/OO.
  • Scala combina programación funcional con Futures, Actors, STM o Cats Effect.
  • Go implementa CSP sobre un lenguaje imperativo.
  • Erlang implementa actores sobre un lenguaje funcional.
  • Existen variantes concurrentes de Prolog basadas en programación lógica.


Si la concurrencia fuera realmente un paradigma, no podría aparecer de forma natural en paradigmas tan diferentes.


Lo que realmente cambia es el modelo de concurrencia elegido:

  • Threads
  • Actores
  • CSP
  • STM (Software Transactional Memory)
  • Async/Await
  • Dataflow


Estos modelos pueden implementarse sobre distintos paradigmas.


Por eso resulta más útil separar tres conceptos:

  • Paradigma: Imperativo, OO, Funcional, Lógico 
  • Modelo de concurrencia: Threads, Actores, CSP, STM, Async/Await 
  • Modelo de ejecución: Secuencial, Concurrente, Paralelo, Distribuido 


Esta clasificación evita muchas confusiones y explica por qué un mismo lenguaje puede ser funcional, usar actores y ejecutarse en forma distribuida.

La concurrencia no reemplaza a un paradigma. Lo complementa.


viernes, 7 de agosto de 2026

¿Qué significa List[?]? Wildcard element type en Scala 3


Si vienes de Java, probablemente List<?> te resulte familiar. En Scala 3 existe exactamente la misma idea, pero con una sintaxis más simple:

List[?]


El ? representa un tipo desconocido.

No significa "cualquier objeto" (Any), sino "existe algún tipo, pero no sabemos cuál".


Por ejemplo:

val xs: List[?] = List(1, 2, 3)

val ys: List[?] = List("Scala", "Java")


Ambas asignaciones son válidas porque el tipo concreto de los elementos queda oculto.


¿Qué puedo hacer con un List[?]?


Como el compilador desconoce el tipo de los elementos, solo permite las operaciones que son seguras para cualquier tipo.

val xs: List[?] = List(1, 2, 3)


xs.size      // ✅

xs.isEmpty   // ✅

xs.head      // Devuelve Any


Pero no es posible asumir que los elementos son de un tipo específico.

val n: Int = xs.head // ❌ No compila


¿Es lo mismo que List[Any]?

No.


List[Any] es una lista cuyos elementos son de tipo Any. En cambio, List[?] es una lista cuyo tipo de elemento es desconocido.

La diferencia es importante. Una List[String] puede verse como List[?] porque simplemente ocultamos el tipo. No ocurre lo mismo con todos los genéricos respecto de Any, especialmente si son invariantes.


¿Por qué existe?


El wildcard permite escribir APIs cuando el tipo concreto no importa.

Si un método solo necesita recorrer una colección, conocer su tamaño o pasarla a otro método, expresar List[?] comunica mucho mejor la intención que usar Any o un parámetro de tipo innecesario.


En definitiva, ? no significa "cualquier tipo". Significa algo mucho más preciso: "Existe un tipo, pero no necesito saber cuál."


Es una pequeña diferencia sintáctica, pero una gran diferencia semántica.


lunes, 6 de julio de 2026

Diseñando MateScript: Diseñando Object, Null y Optional


Cuando pensamos en un lenguaje de programación, solemos imaginar su sintaxis, su compilador o su sistema de tipos.

Sin embargo, existe otro componente igual de importante: la biblioteca estándar.


De poco sirve tener una sintaxis elegante si los tipos fundamentales del lenguaje no forman un modelo coherente.

En este artículo comenzaremos a diseñar tres de los tipos más importantes de MateScript:

  • Object
  • Null
  • Optional


Los tres estarán profundamente relacionados.

En el artículo anterior llegamos a una conclusión interesante.


Queríamos que el desarrollador pudiera escribir:

let name: String?;


Pero no queríamos que el operador ? fuera una característica mágica del compilador.

Por eso decidimos definirlo como un alias de tipos.

T?

es equivalente a:

Optional<T>


Y Optional<T> se implementa utilizando un Union Type.


class Optional<T> {

    value: T | Null;

}


Ahora la pregunta cambia.


¿Cómo deben comportarse Optional y Null?


Como MateScript se ejecutará sobre la JVM, tiene sentido adoptar un modelo de objetos similar al de Java.


Todos los objetos heredarán de una clase base.


class Object {


    equals(other: Object): Boolean


    hashCode(): Int


    toString(): String


}


Esto significa que cualquier objeto del lenguaje compartirá el mismo comportamiento básico.


Por ejemplo:

person.toString()

list.hashCode()

optional.equals(other)


No existen excepciones.

Todo es un objeto.


MateScript intenta reducir la cantidad de código repetitivo.

Por eso no será necesario escribir modificadores como:


public


private


protected


La regla será mucho más simple.

Los atributos serán privados por defecto.

Los métodos serán públicos por defecto.


Por ejemplo:


class Person {


    name: String

    age: Int


    greet() {

        print("Hello!")

    }

}


Es una sintaxis muy cercana a TypeScript, pero manteniendo un modelo de objetos inspirado en Java.


En muchos lenguajes, null es un valor especial incorporado por el compilador.

En MateScript queremos seguir un enfoque diferente.


Null será un objeto singleton.


object Null {


    toString(): String


    equals(other: Object): Boolean


    hashCode(): Int


}


Existirá una única instancia.

Como cualquier otro objeto del lenguaje.

No será un valor mágico.

No requerirá reglas especiales.


Cuando empezamos a diseñar Optional, apareció una idea interesante.

Supongamos el siguiente código.


let name: String? = ...


let upper =

    name.map(x => x.toUpperCase());


¿Qué ocurre si name no contiene ningún valor?

Una posibilidad sería lanzar una excepción.

Otra sería devolver un nuevo Optional.

Pero existe una alternativa mucho más elegante.

Que el propio objeto Null implemente el mismo comportamiento.


Por ejemplo:

Null.map(...)


simplemente devolvería:

Null


Lo mismo ocurre con:

Null.flatMap(...)

Null.filter(...)


Todas estas operaciones simplemente devolverían nuevamente Null.


Esto permite que el código continúe fluyendo de forma natural sin necesidad de realizar comprobaciones constantes.


¿Entonces map pertenece a Object?

Podría parecer una buena idea.

Después de todo, todos los objetos podrían responder al método map().

Sin embargo, rápidamente aparecen situaciones extrañas.

5.map(...)

person.map(...)

car.map(...)


¿Qué significa transformar un número mediante map()?

¿O una persona?


La realidad es que map() no representa una operación común a todos los objetos.

Representa una operación sobre estructuras que contienen un valor.


En lugar de agregar map() a Object, resulta más razonable definir una interfaz específica.

Conceptualmente podría verse así:


interface Functor<T> {


    map<R>(

        mapper: (T) -> R

    ): Functor<R>;


}


Esta idea proviene de la programación funcional y aparece en lenguajes como Haskell, Scala y muchos otros.


No es necesario conocer teoría de categorías para utilizarla.

La idea es muy sencilla.


Un Functor es cualquier estructura capaz de transformar el valor que contiene sin perder su estructura.


Ahora Optional puede implementar esa interfaz.


class Optional<T>

    implements Functor<T> {


    value: T | Null;


}


Cuando existe un valor, map() aplica la función recibida.

Cuando el valor es Null, simplemente devuelve Null.

Desde el punto de vista del desarrollador, el código siempre se escribe de la misma manera.


name

    .map(...)

    .map(...)

    .map(...)


Sin preguntar constantemente si existe un valor.


Lo interesante es que nunca planeé incorporar conceptos de programación funcional.

Simplemente estaba buscando una forma elegante de implementar Optional.

Sin embargo, al avanzar en el diseño comenzaron a aparecer naturalmente ideas como:

  • objetos singleton;
  • Union Types;
  • Functors.


Y eso es una de las cosas más fascinantes de diseñar un lenguaje.

Las buenas abstracciones suelen aparecer como consecuencia de resolver problemas reales, no como un objetivo en sí mismo.


En este punto, MateScript comienza a tener una identidad propia.

  • Todos los objetos heredan de Object.
  • Null deja de ser un valor mágico para convertirse en un objeto singleton.
  • Optional<T> representa explícitamente la posibilidad de ausencia mediante T | Null.
  • Las operaciones de transformación se modelan mediante interfaces específicas, manteniendo la clase Object pequeña y coherente.



domingo, 5 de julio de 2026

Diseñando MateScript: ¿Cómo representar la ausencia de un valor?


Uno de los aspectos más interesantes de diseñar un lenguaje de programación es descubrir que muchas de las características que damos por sentadas están profundamente relacionadas entre sí.

Un buen ejemplo es la representación de los valores nulos.

A simple vista parece una decisión menor. Sin embargo, termina afectando el sistema de tipos, el compilador, la biblioteca estándar e incluso el runtime del lenguaje.

En lugar de intentar inventar una solución completamente nueva, MateScript adopta una filosofía diferente: tomar buenas ideas de otros lenguajes y combinarlas de una forma consistente.


En este artículo aparecen varias influencias claras:

  • El operador ? de Kotlin y C#.
  • Los Union Types de TypeScript.
  • Los objetos singleton de Scala.


Lo interesante es que ninguna de estas ideas fue incorporada de forma aislada. Todas terminan formando parte de un único diseño.

Desde el punto de vista del desarrollador, me gusta mucho la sintaxis introducida por Kotlin y posteriormente adoptada por C#.


let name: String?;


La intención es evidente.

La variable puede no contener un valor.

Sin embargo, no me gusta que el operador ? sea una característica mágica del compilador.


Siempre que sea posible prefiero que las características del lenguaje puedan explicarse utilizando otros conceptos ya existentes. 

En lugar de convertir ? en una regla especial del compilador, podemos definirlo como un alias de tipos.


Es decir:

T?

es simplemente equivalente a:

Optional<T>


Por ejemplo:

let name: String?;


sería exactamente lo mismo que escribir:

let name: Optional<String>;


Una vez realizado este reemplazo, el compilador deja de conocer la existencia del operador ?.

Para el resto del proceso de compilación solamente existe Optional<T>.


Ahora aparece una pregunta mucho más interesante. ¿Cómo implementamos Optional<T>?


Podríamos imaginar algo como esto:


class Optional<T> {

    private value: T;

}


Pero inmediatamente encontramos un problema. ¿Cómo representamos un value vacío?

En Java utilizaríamos null. Pero MateScript no tendrá un valor mágico llamado null.

Entonces necesitamos otra solución.


La idea tomada de Scala es tratar a Null como un objeto singleton del lenguaje.


Conceptualmente:

object Null


Esto significa que existe una única instancia de Null, exactamente igual que cualquier otro singleton.

No es una palabra reservada.

No es un valor especial incorporado por el compilador.

Es simplemente un objeto del lenguaje.


Hasta aquí parece que el problema está resuelto.

Pero todavía queda una pregunta.

Si value puede contener un objeto de tipo T o el objeto Null, entonces su tipo ya no puede ser simplemente T.

Necesitamos representar que un valor puede pertenecer a más de un tipo.


Es decir: T | Null


Y casi sin buscarlo llegamos a otro concepto conocido. Los Union Types.


La idea proviene de TypeScript.

Un Union Type representa un valor que puede pertenecer a uno de varios tipos.


Por ejemplo:

String | Null


indica que un valor puede ser:

un objeto String, o

el objeto Null.


Gracias a esto podemos redefinir Optional.

class Optional<T> {

    private value: T | Null;

}


Ahora sí el modelo es completamente consistente.


Un Optional<String> puede contener un String o el objeto Null.

No existe ningún valor mágico dentro del lenguaje.

Todo está expresado mediante el sistema de tipos.


Lo interesante es que los Union Types no aparecieron porque quisiera agregarlos al lenguaje.

Surgieron como una consecuencia natural del diseño de Optional.


Una vez que el compilador fue capaz de representar tipos como: T | Null apareció una pregunta inevitable.

¿Por qué limitar esa capacidad únicamente a Optional?


Si el compilador ya sabe trabajar con Union Types, también puede permitir que los desarrolladores los utilicen directamente.


Por ejemplo:

let value: String | Int;

let result: Success | Error;

let animal: Dog | Cat;


Lo que comenzó siendo una solución para representar la ausencia de un valor terminó convirtiéndose en una nueva característica del lenguaje.


Curiosamente, cada una de estas decisiones resuelve un problema distinto, pero juntas forman un diseño mucho más consistente.


El desarrollador escribe:

String?


El parser lo transforma en:

Optional<String>


Y Optional se implementa como:

Optional<T>

    └── value : T | Null


Todo utilizando conceptos generales del lenguaje.


No existen excepciones.

No existen reglas especiales.

No existen valores mágicos.


Diseñar un lenguaje muchas veces consiste en descubrir que una buena solución termina resolviendo varios problemas al mismo tiempo.


En MateScript decidimos combinar ideas provenientes de distintos lenguajes:

  • De Kotlin y C# tomamos la sintaxis ? para representar tipos opcionales.
  • De TypeScript adoptamos los Union Types para representar valores que pueden pertenecer a más de un tipo.
  • De Scala incorporamos la idea de los objetos singleton para representar Null como un objeto del lenguaje y no como un valor mágico del compilador.


Lo interesante es que ninguna de estas características fue agregada por separado.

Cada decisión fue llevando naturalmente a la siguiente, hasta formar un sistema de tipos más uniforme y coherente.



viernes, 3 de julio de 2026

Diseñando MateScript: un JavaScript de tipado estático para la JVM


Hace tiempo que me interesa el diseño de lenguajes de programación.

No solamente cómo construir compiladores, sino también cómo tomar decisiones sobre la sintaxis, el sistema de tipos y la experiencia de desarrollo.

Por eso decidí comenzar un proyecto experimental: MateScript.


¿Qué es MateScript?

MateScript es un lenguaje inspirado en JavaScript, pero con tipado estático y compilación a bytecode JVM.

JavaScript es uno de los lenguajes más populares del mundo, pero también arrastra varios problemas derivados de sus decisiones históricas.


Por ejemplo:

let age = 42;

age = "hola";


Esto es perfectamente válido.

MateScript buscará detectar estos problemas durante la compilación.


let age = 42;

age = "hola";


Resultado:

Type mismatch: expected Int found String


MateScript intentará mantener las características más atractivas de JavaScript:

  • Sintaxis sencilla
  • Funciones de primera clase
  • Lambdas
  • Inferencia de tipos
  • Programación funcional
  • Objetos


Pero agregando:

  • Tipado estático
  • Null Safety
  • Pattern Matching
  • Compilación a bytecode JVM
  • Interoperabilidad con Java


¿Por qué no usar TypeScript?

TypeScript es una excelente herramienta.

Sin embargo, sigue dependiendo del ecosistema JavaScript.


El objetivo de MateScript es diferente:

  • Compilar directamente a bytecode JVM
  • Ejecutarse sin Node.js
  • Integrarse con bibliotecas Java
  • Aprovechar el rendimiento y madurez de la plataforma JVM


Primeras decisiones de diseño

Variables inmutables por defecto

let name = "Emanuel";


Una variable declarada con let no podrá modificarse.


Para variables mutables utilizaremos:

var counter = 0;


Inferencia de tipos

No será necesario declarar tipos explícitamente.


let age = 42;


El compilador inferirá:

Int


Funciones

La sintaxis será muy similar a JavaScript.


function greet(name: String): String {

    return "Hola " + name;

}


Lambdas

let square = x => x * x;


Null Safety

Los tipos no aceptarán valores nulos por defecto.

let name: String = null;

Error de compilación.


Para permitir nulos:

let name: String?;


Y acá podemos hacer algo bastante elegante, podemos hacer que los tipos tengan alias por lo tanto : 

String? va a ser -> Optional<String> 


Ahora bien, como hago Optinal<T> , acá podemos utilizar el camino que usa Typescript union de tipos : T|Null y podemos hacer que Null sea un object como Scala. Y no vamos a tener nulos en nuestro lenguaje. 


¿Por qué la JVM?

La JVM es una de las plataformas más maduras de la industria.

  • Proporciona:
  • Garbage Collector
  • JIT Compiler
  • Ecosistema enorme
  • Bibliotecas maduras
  • Herramientas de monitoreo


En lugar de construir una máquina virtual propia, MateScript aprovechará toda esa infraestructura.


¿Qué sigue?

En los próximos artículos comenzaremos la implementación de MateScript.

El primer paso será construir una gramática utilizando ANTLR capaz de reconocer programas simples como:


let name = "Emanuel";

print(name);


A partir de allí iremos avanzando gradualmente hacia un compilador completo capaz de generar bytecode JVM.


La meta no es crear el próximo Kotlin. La meta es aprender cómo se construyen realmente los lenguajes de programación.


miércoles, 1 de julio de 2026

Programación funcional en PHP


La programación funcional no es exclusiva de lenguajes como Haskell, Elixir o Scala. PHP también permite adoptar muchos de sus principios para escribir código más limpio, expresivo y fácil de mantener.

Veamos algunas de las herramientas que ofrece PHP.


Funciones como ciudadanos de primera clase

Las funciones pueden almacenarse en variables, pasarse como argumentos y devolverse desde otras funciones.


<?php

$greet = fn(string $name) => "Hello $name!";

echo $greet("Emanuel");


Salida:

Hello Emanuel!


Arrow Functions

Introducidas en PHP 7.4, las Arrow Functions permiten escribir funciones pequeñas de manera mucho más concisa.


$numbers = [1, 2, 3, 4];


$squares = array_map(

    fn($n) => $n * $n,

    $numbers

);


print_r($squares);


Resultado:

Array

(

    [0] => 1

    [1] => 4

    [2] => 9

    [3] => 16

)


map()


Aunque PHP no posee un método map() sobre los arrays, dispone de array_map().


$names = ["john", "mary", "alice"];


$upper = array_map(

    "strtoupper",

    $names

);


print_r($upper);


Resultado:

JOHN

MARY

ALICE


filter()

Para filtrar colecciones se utiliza `array_filter()`.


$numbers = [1,2,3,4,5,6];


$even = array_filter(

    $numbers,

    fn($n) => $n % 2 === 0

);


print_r($even);


Resultado:

2

4

6


reduce()

array_reduce() permite reducir una colección a un único valor.


$numbers = [1,2,3,4,5];


$sum = array_reduce(

    $numbers,

    fn($acc, $n) => $acc + $n,

    0

);


echo $sum;


Resultado

15


Funciones puras

Una función pura siempre devuelve el mismo resultado para la misma entrada y no produce efectos secundarios.


function add(int $a, int $b): int

{

    return $a + $b;

}



En cambio, esta función no es pura:


$total = 0;


function addToTotal(int $n): void

{

    global $total;

    $total += $n;

}


Inmutabilidad

PHP no posee estructuras inmutables por defecto, pero es posible trabajar evitando modificar los datos originales.

$numbers = [1,2,3];


$newNumbers = [...$numbers, 4];


print_r($numbers);

print_r($newNumbers);


Resultado:

Original:

1 2 3

Nuevo:

1 2 3 4


Composición de funciones


Podemos construir funciones más complejas combinando funciones pequeñas.


$trim = fn($s) => trim($s);

$upper = fn($s) => strtoupper($s);


$normalize = fn($s) => $upper($trim($s));


echo $normalize("   hello   ");

Resultado

HELLO


Closures

Las closures permiten capturar variables del contexto.


function multiplier(int $factor)

{

    return fn($n) => $n * $factor;

}


$double = multiplier(2);

$triple = multiplier(3);


echo $double(10);

echo $triple(10);


Resultado

20

30


Encadenando operaciones


Una secuencia típica de programación funcional consiste en filtrar, transformar y reducir datos.


$numbers = [1,2,3,4,5,6];


$result = array_reduce(

    array_map(

        fn($n) => $n * $n,

        array_filter(

            $numbers,

            fn($n) => $n % 2 === 0

        )

    ),

    fn($acc, $n) => $acc + $n,

    0

);


echo $result;


Proceso:

  • Filtra los números pares.
  • Calcula el cuadrado de cada uno.
  • Suma todos los cuadrados.


Resultado:

56


Aunque PHP sigue siendo un lenguaje predominantemente orientado a objetos, incorpora suficientes herramientas para adoptar un estilo funcional cuando resulta conveniente. El uso de funciones puras, array_map(), array_filter(), array_reduce(), closures y Arrow Functions permite escribir código más declarativo, reutilizable y sencillo de razonar.

No reemplaza a lenguajes puramente funcionales como Haskell o Elixir, pero demuestra que la programación funcional es un paradigma que puede aplicarse con éxito en prácticamente cualquier lenguaje moderno.


lunes, 22 de junio de 2026

ANTLR: El primer paso para construir un lenguaje de programación


Cuando usamos Java, Kotlin o Scala, normalmente escribimos código sin pensar demasiado en lo que ocurre detrás de escena.


int result = 10 + 20;


Sin embargo, antes de que la JVM pueda ejecutar ese programa, alguien tuvo que diseñar un compilador capaz de entender ese texto y transformarlo en bytecode.

En esta serie vamos a construir nuestro propio lenguaje para la JVM. No pretendemos competir con Kotlin o Scala, pero sí recorrer muchos de los mismos conceptos que utilizan estos lenguajes internamente.


Y el primer paso es aprender una herramienta fundamental: ANTLR.


¿Qué es ANTLR?

ANTLR (ANother Tool for Language Recognition) es una herramienta que permite generar analizadores léxicos y sintácticos a partir de una gramática.

En lugar de escribir manualmente todo el código necesario para interpretar un lenguaje, simplemente describimos las reglas del mismo y ANTLR genera automáticamente gran parte de la infraestructura necesaria.


Por ejemplo, podemos definir una regla como:


expression

    : expression '+' expression

    | NUMBER

    ;


A partir de ella ANTLR podrá reconocer expresiones como:

1 + 2 + 3


Un compilador suele dividirse en varias etapas:


Código Fuente

      │

      ▼

 Lexer

      │

      ▼

 Tokens

      │

      ▼

 Parser

      │

      ▼

 AST

      │

      ▼

 Análisis Semántico

      │

      ▼

 Generación de Código


ANTLR participa principalmente en las dos primeras:

  • Lexer
  • Parser


¿Qué hace el Lexer?

El Lexer toma una secuencia de caracteres y la divide en tokens.


Por ejemplo:

let age = 42


se transforma en:

LET

IDENTIFIER(age)

ASSIGN

NUMBER(42)


El Lexer no entiende el significado del programa. Solo identifica patrones.


¿Qué hace el Parser?

El Parser toma los tokens generados por el Lexer y verifica que respeten las reglas definidas por la gramática.


Por ejemplo:

LET IDENTIFIER ASSIGN NUMBER


podría convertirse en:

Assignment

 ├── Variable(age)

 └── Number(42)


Esta estructura representa la forma del programa y servirá como base para las siguientes etapas.


¿Qué es una gramática?

Una gramática define qué construcciones son válidas dentro de un lenguaje.


Por ejemplo:

variableDeclaration

    : 'let' IDENTIFIER '=' expression

    ;


Esta regla indica que una declaración válida debe contener:

  • La palabra reservada let
  • Un identificador
  • El símbolo =
  • Una expresión


¿ANTLR genera compiladores?

No.


ANTLR resuelve principalmente:

  • Análisis léxico
  • Parsing


Todavía debemos implementar:

  • Árboles sintácticos abstractos (AST)
  • Tablas de símbolos
  • Sistema de tipos
  • Generación de bytecode JVM


Por eso ANTLR es una herramienta muy importante, pero representa solo una parte del compilador completo.


miércoles, 17 de junio de 2026

¿Por qué Clojure compila diferente? Ventajas y desventajas de ser un "Hosted Language" en la JVM


Cuando pensamos en lenguajes para la JVM, solemos meter en la misma bolsa a Java, Scala, Kotlin y Clojure. Después de todo, todos terminan ejecutándose sobre la misma máquina virtual.


Pero hay una diferencia fundamental en cómo están construidos.


Scala y Kotlin generan bytecode JVM de manera bastante directa. Clojure, en cambio, adopta una filosofía distinta: es un hosted language, es decir, un lenguaje "hospedado" sobre la plataforma Java.


¿Y qué significa eso? ¿Tiene ventajas? ¿Tiene costos?

Lenguajes como Java, Scala o Kotlin siguen aproximadamente este esquema:


Código fuente

      ↓

Compilador

      ↓

Bytecode JVM (.class)

      ↓

JVM


Muchas características del lenguaje se traducen directamente en clases, métodos y bytecode especializado.


Por ejemplo, una case class de Scala:

case class Person(name: String)

genera automáticamente métodos como:

  • equals()
  • hashCode()
  • copy()
  • toString()


Todo esto queda representado directamente en bytecode.


Clojure también produce bytecode JVM, pero gran parte del comportamiento del lenguaje vive en su runtime:


Código Clojure

      ↓

Compilador

      ↓

Bytecode + Runtime de Clojure

      ↓

JVM


Por eso Rich Hickey describe a Clojure como un hosted language.


La plataforma Java no es solamente un destino de compilación: es el ecosistema sobre el cual está construido el lenguaje.


Ventaja: Aprovecha toda la plataforma Java


Desde Clojure podemos usar directamente cualquier clase Java:


(import java.time.LocalDate)

(LocalDate/now)


No hay puentes especiales ni adaptadores.

Clojure reutiliza:

  • la JVM;
  • el recolector de basura;
  • las bibliotecas Java;
  • los hilos;
  • las excepciones;
  • las herramientas de profiling;
  • todo el ecosistema existente.


En vez de reinventar la rueda, se apoya en ella.


Ventaja: Un compilador relativamente pequeño


Muchas características importantes de Clojure viven en bibliotecas y no en el compilador:

  • secuencias perezosas;
  • colecciones persistentes;
  • STM;
  • transducers.


Esto hace que el compilador sea considerablemente más sencillo que los de Scala o Kotlin.


Ventaja: Desarrollo interactivo y REPL


Una de las mayores fortalezas de Clojure es su modelo de desarrollo interactivo.

Podemos definir una función:


(defn cuadrado [x]

  (* x x))


y cargarla inmediatamente en la REPL, sin recompilar todo el proyecto.


Este enfoque favorece:

  • feedback rápido;
  • hot reloading;
  • experimentación;
  • desarrollo incremental.


Ventaja: Evolución mediante librerías


Muchas características avanzadas de Clojure se agregaron sin modificar demasiado el lenguaje:

  • transducers;
  • core.async;
  • spec;
  • STM.


Al depender más del runtime y menos del compilador, la evolución del ecosistema suele ser más flexible.


Las desventajas

Menos optimizaciones

Scala y Kotlin generan bytecode más especializado.

En Clojure, muchas operaciones pasan por componentes del runtime como:

  • IFn;
  • PersistentVector;
  • PersistentMap;
  • RT.


Esto introduce niveles adicionales de indirección y, en ciertos casos, puede afectar el rendimiento.


Más reflexión

Si escribimos:

(defn largo [s]

  (.length s))


el compilador puede recurrir a reflexión.

Podemos ayudarlo con type hints:


(defn largo [^String s]

  (.length s))


pero esto requiere intervención del programador.


Menos comprobaciones en tiempo de compilación


Scala y Kotlin verifican muchas cosas antes de ejecutar:

  • tipos;
  • nulabilidad;
  • exhaustividad del pattern matching;
  • restricciones genéricas.


Clojure, al ser dinámico, detecta muchos errores recién en tiempo de ejecución.


Interoperabilidad asimétrica

Desde Clojure usar Java es muy fácil.

Pero desde Java consumir código Clojure no es tan natural:


IFn plus = Clojure.var("myns", "plus");


Mientras que una clase escrita en Scala o Kotlin suele verse como una clase Java convencional.


Dos filosofías distintas

No es que una aproximación sea mejor que la otra.

Scala y Kotlin intentan enriquecer la JVM mediante compiladores sofisticados y bytecode especializado.

Clojure adopta otra filosofía: construir un lenguaje pequeño y dinámico que reutilice al máximo la plataforma existente.

Al final, la diferencia más interesante no es a qué compilan, porque todos terminan ejecutándose en la JVM.

La verdadera diferencia es: ¿Cuánto del lenguaje vive en el compilador y cuánto vive en el runtime?

Y en esa decisión de diseño está gran parte de la personalidad de Clojure.


domingo, 14 de junio de 2026

Mundial de Lenguajes de Programación



Le pregunte a la IA que onda si hacemos partidos de diferentes tecnologías con diferentes lenguajes, y este fue el resultado : 



Partido 1: Aplicaciones Web

Java vs C#


🇯🇵 Java sale con Spring Boot.

🇺🇸 C# responde con ASP.NET Core.


Ambos tienen experiencia internacional, planteles maduros y una enorme cantidad de títulos.

Resultado: Empate 1-1.


> Si tenés que hacer una API corporativa, cualquiera de los dos te lleva a octavos.


 🏟️ Partido 2: Concurrencia

Elixir vs Go


🇧🇷 Elixir apuesta por la posesión y el juego asociado con actores y supervisores.

🇩🇪 Go juega más directo con goroutines y channels.


Elixir domina el mediocampo cuando aparecen millones de procesos concurrentes.

Resultado: Elixir 2 - Go 1.

La BEAM sigue siendo una de las ligas más difíciles de enfrentar.


 🏟️ Partido 3: Aplicaciones Standalone

C++ vs Rust


🇮🇹 C++ es el veterano con ocho mundiales encima.

🇳🇱 Rust llega con una generación joven y una defensa impecable.


Rust reduce errores, pero C++ todavía tiene una experiencia enorme.

Resultado: C++ 1 - Rust 1.

La leyenda sigue vigente, pero la promesa pide pista.


 🏟️ Partido 4: Cloud

Java vs Node.js


🇦🇷 Java viene con GraalVM y Spring Boot.

🇪🇸 Node.js juega rápido y con poco peso.


En microservicios y aplicaciones cloud modernas, ambos se sienten cómodos.

Resultado: Empate.

El VAR determina que la elección depende más del equipo que del lenguaje.


🏟️ Partido 5: Inteligencia Artificial

Python vs el resto del mundo


🇫🇷 Python entra a la cancha.

Resultado: Python 5 - 0.

Hubo partido durante cinco minutos.


 🏟️ Partido 6: Programación Funcional

Scala vs Haskell

🇺🇾 Scala combina técnica y pragmatismo.

🇭🇷 Haskell juega un fútbol hermoso que pocos entienden.


Resultado: Scala 2 - Haskell 1.

La posesión fue 90% para Haskell, pero Scala convirtió las que tuvo.


🏟️ Partido 7: Frontend

TypeScript vs JavaScript


🇵🇹 JavaScript es el capitán histórico.

🇵🇹 TypeScript es el mismo equipo, pero con preparador físico.

Resultado: TypeScript 3 - JavaScript 1.

 Los errores en tiempo de compilación fueron la figura del partido.


 🏟️ Partido 8: Bases de Datos

PostgreSQL vs MySQL


🇨🇴 PostgreSQL juega con mucha técnica.

🇲🇽 MySQL apuesta por la experiencia.


Resultado: PostgreSQL 2 - MySQL 1.

PostgreSQL ganó por posesión y calidad de juego.


Y listo, no hice mucho, ni dice mucho pero es divertido :P 

jueves, 28 de mayo de 2026

Implementando reduce y construyendo map en Scala


Una de las ideas más interesantes de la programación funcional es que muchas operaciones sobre colecciones pueden construirse a partir de una sola función general: fold (o un reduce más flexible).


Por ejemplo, este reduce recursivo:


def reduce[A, B](list: List[A], initial: B)(f: (B, A) => B): B =

  if (list.isEmpty) initial

  else reduce(list.tail, f(initial, list.head))(f)


Recorre la lista acumulando un resultado.

¿Qué hace exactamente?

La función recibe:

  • una lista
  • un valor inicial
  • una función acumuladora


La función f recibe:

(B, A) => B


Es decir:

  • el acumulador actual (B)
  • el elemento actual (A)
  • y devuelve un nuevo acumulador (B)


Ejemplo: sumar números

val numbers = List(1, 2, 3, 4)

val result =  reduce(numbers, 0)((acc, n) => acc + n)

println(result)


Salida: 10


El proceso sería algo así:

(((0 + 1) + 2) + 3) + 4


Ejemplo: concatenar strings


val words = List("Hola", "Scala", "!")

val result =  reduce(words, "")((acc, word) => acc + " " + word)

println(result)


Salida: Hola Scala !


Acá viene la parte interesante.

map transforma cada elemento de una lista:


List(1, 2, 3).map(_ * 2)


Resultado: List(2, 4, 6)


Pero podemos implementarlo usando únicamente nuestro reduce.


def map[A, B](list: List[A])(f: A => B): List[B] =

  reduce(list, List.empty[B]) { (acc, elem) =>

    acc :+ f(elem)

  }


val numbers = List(1, 2, 3)

val doubled =  map(numbers)(_ * 2)


println(doubled)


Salida: List(2, 4, 6)


En cada iteración:

acc :+ f(elem)


1. Se transforma el elemento con f

2. Se agrega al acumulador


Por ejemplo:

List()

List(2)

List(2, 4)

List(2, 4, 6)


Lo interesante es que:

  • sum
  • filter
  • map
  • count
  • flatMap

y muchas otras operaciones funcionales pueden construirse a partir de un único patrón: recorrer una estructura acumulando un resultado. Ese patrón es justamente fold.



miércoles, 20 de mayo de 2026

std::optional en C++


En muchos lenguajes modernos existe alguna forma de representar “un valor que puede no existir”:

  • Optional en Java
  • Option en Scala
  • Maybe en Haskell
  • Option en Rust (Some/None)


En C++, la respuesta oficial es std::optional, introducido en C++17 y mejorado muchísimo en C++23 con operaciones monádicas.

¿Qué problema resuelve?

Antes de optional, era común devolver:

  • nullptr
  • valores mágicos (-1)
  • flags adicionales
  • excepciones


Ejemplo clásico:


int findUserId(const std::string& name) {

    if(name == "emanuel")

        return 10;

    return -1;

}


Problema:

  • -1 no expresa claramente ausencia
  • alguien podría olvidarse de validarlo
  • el contrato del método no es explícito


Con std::optional:


#include <optional>


std::optional<int> findUserId(const std::string& name) {

    if(name == "emanuel")

        return 10;


    return std::nullopt;

}


Ahora el tipo expresa claramente: “puede devolver un entero… o no”.


Crear un optional

std::optional<int> number = 10;


Vacío:

std::optional<int> empty = std::nullopt;


Verificar si tiene valor

if(number.has_value()) {

    std::cout << "Tiene valor";

}


o más idiomático:

if(number) {

    std::cout << "Tiene valor";

}


Obtener el valor

std::cout << number.value();


Ojo! Si no tiene valor, lanza excepción.


Más seguro:

if(number) {

    std::cout << *number;

}


Valor por defecto

std::optional<int> x;

std::cout << x.value_or(0);


Resultado: 0


Muy parecido a:

  • orElse() en Java
  • getOrElse() en Scala


¿Por qué es mejor que punteros?

Muchas APIs antiguas usan punteros nullable:

User* findUser();


Problemas:

  • ownership ambiguo
  • riesgo de dangling pointers
  • semántica poco clara


Con optional:

std::optional<User> findUser();


El contrato queda explícito y seguro.

En programación funcional, una mónada permite:

  • encadenar operaciones
  • evitar checks manuales
  • propagar automáticamente ausencia/error


Antes de C++23 esto era incómodo.


Había que hacer:

if(result) {

    ...

}


todo el tiempo.


C++23 agregó operaciones monádicas oficiales.

transform

Equivalente a map.

Transforma el valor si existe.


std::optional<int> number = 10;


auto result =

    number.transform([](int x) {

        return x * 2;

    });


std::cout << *result;


Resultado: 20

Si el optional está vacío, no ejecuta nada.


and_then

Equivalente a flatMap.

Permite encadenar funciones que devuelven optional.


std::optional<int> parse(const std::string& s) {

    if(s == "42")

        return 42;


    return std::nullopt;

}


auto result =

    parse("42")

        .and_then([](int x) -> std::optional<int> {

            if(x > 0)

                return x * 2;


            return std::nullopt;

        });


Diferencia entre transform y and_then

transform convierte:

optional<T> -> optional<U>

cuando la función devuelve un valor normal, and_then convierte:

optional<T> -> optional<U>


pero la función YA devuelve optional.

Evita nested optionals: optional<optional<int>>


or_else

Permite ejecutar lógica si está vacío.


std::optional<int> value;


value.or_else([] {

    std::cout << "No había valor";

    return std::optional<int>{0};

});


Ahora podemos escribir código mucho más declarativo:


auto result =

    parse("42")

        .transform([](int x) {

            return x * 2;

        })

        .and_then([](int x) -> std::optional<int> {

            if(x < 100)

                return x;


            return std::nullopt;

        })

        .or_else([] {

            return std::optional<int>{0};

        });


Esto ya se parece muchísimo a:

  • Scala
  • Haskell
  • Rust
  • Kotlin

¿Cuándo usar optional?

Ideal para:

  • búsquedas
  • parseos
  • resultados opcionales
  • operaciones que pueden fallar naturalmente


¿Cuándo NO usarlo?

  • errores complejos
  • información detallada de fallos


En esos casos es mejor:

  • std::expected (C++23)
  • excepciones


std::optional empezó en C++17 como una forma segura de representar ausencia de valor.

Pero en C++23 evolucionó muchísimo:

  • transform
  • and_then
  • or_else


lo convierten en una herramienta claramente influenciada por programación funcional y mónadas como Maybe.

C++ sigue siendo multiparadigma, pero cada vez incorpora más ideas del mundo funcional… sin dejar de ser C++.

miércoles, 13 de mayo de 2026

Kotlin 2.4.0


La versión 2.4.0 de Kotlin sigue consolidando muchas características que venían evolucionando desde releases anteriores.

Más que agregar “una gran feature”, esta versión termina de estabilizar varias piezas importantes del ecosistema.

El nuevo compilador K2 dejó de sentirse “experimental” y pasó a ser la base real del futuro de Kotlin.


¿Qué aporta?

  • Compilaciones más rápidas
  • Mejor análisis de tipos
  • Mensajes de error más claros
  • Infraestructura más simple para futuras features


K2 no es solamente una optimización, es prácticamente una reescritura del compilador.


Los Context Parameters siguen evolucionando y acercan a Kotlin a ideas similares a:

  • implicits de Scala
  • type classes funcionales
  • dependency injection implícita


Ejemplo:


context(Logger)

fun processOrder() {

    log("Processing order")

}


Esto permite escribir APIs mucho más declarativas.


Kotlin sigue empujando fuerte el desarrollo multiplataforma; en 2.4.0 hay mejoras importantes en:

  • compilación incremental
  • interoperabilidad con iOS
  • performance de Kotlin/Native
  • sharing de código entre plataformas


El garbage collector y el manejo de memoria continúan mejorando para Kotlin/Native. 


Esto impacta directamente en:

  • apps iOS
  • aplicaciones embebidas
  • performance general


Históricamente Kotlin/Native era uno de los puntos más débiles del ecosistema. Las últimas versiones muestran una mejora enorme.


También hay mejoras en:

  • IntelliJ IDEA
  • Gradle
  • debugging
  • análisis estático
  • tiempos de indexing


Muchas veces estas mejoras no aparecen en los titulares, pero son las que realmente cambian la experiencia diaria.


Lo más interesante de Kotlin 2.4.0 quizás no sea una feature puntual.

Es que muchas ideas que antes parecían experimentales ahora empiezan a sentirse “normales”:

  • K2
  • Multiplatform
  • Native
  • Context Parameters


Kotlin está entrando en una etapa mucho más madura del lenguaje.


martes, 21 de abril de 2026

Records vs Clases vs Lombok vs Kotlin vs Scala


¿Cuál es la mejor forma de modelar datos? Desde los Struct de c++ nos venimos preguntando esto. Vamos a ver algunas opciones modernas que nos provee la plataforma java. 

Cuando trabajamos con objetos que representan datos (DTOs, Value Objects, etc.), distintos lenguajes ofrecen soluciones para evitar el boilerplate.


En este post comparamos:

  • Records en Java
  • Clases tradicionales
  • Lombok
  • Data classes en Kotlin
  • Case classes en Scala


1. Clase tradicional (Java)


public class Persona {

    private final String nombre;

    private final int edad;


    public Persona(String nombre, int edad) {

        this.nombre = nombre;

        this.edad = edad;

    }


    public String getNombre() { return nombre; }

    public int getEdad() { return edad; }


    @Override public boolean equals(Object o) { ... }

    @Override public int hashCode() { ... }

    @Override public String toString() { ... }

}

Ventajas

  • Total control
  • Compatible con todo (JPA, frameworks)


Desventajas

  • Mucho boilerplate
  • Propenso a errores


2. Records (Java)


public record Persona(String nombre, int edad) {}


Ventajas

  • Ultra conciso
  • Inmutabilidad garantizada
  • Sin dependencias


Desventajas

  • Menos flexible
  • No sirve bien con JPA
  • No herencia


3. Lombok (@Data)


import lombok.Data;


@Data

public class Persona {

    private String nombre;

    private int edad;

}


Ventajas

  • Reduce mucho código
  • Mutable o inmutable (configurable)


Desventajas

  • Dependencia externa
  • "Magia" en compilación (puede confundir)
  •  Problemas en tooling/debug


4. Data Classes (Kotlin)


data class Persona(val nombre: String, val edad: Int)


Ventajas

  • Muy conciso
  • Inmutable por defecto
  • copy() incluido
  • Destructuring


val (nombre, edad) = persona


Desventajas

  • Requiere usar Kotlin
  • Interoperabilidad Java no siempre perfecta


5. Case Classes (Scala)


case class Persona(nombre: String, edad: Int)


Ventajas

  • Inmutables
  • Pattern matching nativo
  • copy() automático
  • Muy expresivas


persona match {

  case Persona(nombre, edad) => println(nombre)

}


Desventajas

  • Curva de aprendizaje
  • Ecosistema más complejo


Y entonces? Y ninguno es super mejor, pero podemos tener estas reglas: 


Java Records

Ideal para:

  • DTOs simples
  • APIs REST
  • Código moderno sin dependencias


Son el "mínimo viable elegante" en Java.


Lombok

Ideal para:

  • Proyectos legacy
  • Equipos que ya lo usan


 Soluciona el problema… pero no es parte del lenguaje.


Kotlin

La mejor experiencia general para modelado de datos.

  • copy()
  • destructuring
  • null-safety


Es claramente superior en ergonomía.


Scala

El más poderoso conceptualmente.

  • Pattern matching real
  • Inmutabilidad fuerte
  • Integración con FP


Pero más complejo.


 Clases Java

 Siguen siendo necesarias cuando:

  • Usás JPA
  • Necesitás mutabilidad
  • Requerís control total


Si estás en Java moderno → Records

Si querés máxima productividad → Kotlin

Si buscás poder expresivo → Scala

Si estás en legacy → Lombok o clases



domingo, 19 de abril de 2026

¿Por qué Java usa Streams?


Cuando Java introdujo Streams en Java 8, no fue solo para “hacer código más lindo”, sino para cambiar la forma en que procesamos colecciones.


Pero otros lenguajes de la JVM ya resolvían esto de forma distinta.

Entonces, la pregunta es: ¿por qué Java eligió Streams y no otro modelo?


Antes de Java 8:

List<String> result = new ArrayList<>();

for (String s : lista) {

    if (s.length() > 3) {

        result.add(s.toUpperCase());

    }

}


Mucho boilerplate

  • No es declarativo
  • Difícil de paralelizar
  • Mezcla lógica con control de flujo


La solución: Streams en Java


Con Streams:


List<String> result = lista.stream()

    .filter(s -> s.length() > 3)

    .map(String::toUpperCase)

    .toList();


Claves del diseño:

  • Evaluación lazy
  • Pipeline de operaciones
  • Separación entre datos y procesamiento
  • Fácil paralelización (parallelStream())


Java construyó un modelo nuevo, no solo métodos en Collection ¿y por qué? Otros lenguajes los hacían bien. 

Scala

lista.filter(_.length > 3).map(_.toUpperCase)


Características

  • Colecciones inmutables por defecto
  • Operaciones directamente sobre la colección
  • Lazy solo si usás View o LazyList

Scala no necesita Streams porque su API de colecciones ya es funcional


Kotlin

lista.filter { it.length > 3 }

     .map { it.uppercase() }


Características:

  • API funcional sobre colecciones
  • Operaciones eager por defecto


Para lazy:

lista.asSequence()

    .filter { it.length > 3 }

    .map { it.uppercase() }


Diferencia clave

Kotlin separa:

  • List (eager)
  • Sequence (lazy)


Java unificó esto en Streams.


Groovy

lista.findAll { it.length() > 3 }

     .collect { it.toUpperCase() }


Características

  • Muy expresivo
  • Dinámico
  • API funcional desde hace años


Diferencia clave

Más simple, pero:

  • menos eficiente
  • no lazy por defecto
  • sin optimización tipo pipeline


Entonces ¿Por qué Java eligió Streams?


Java tenía restricciones fuertes:

1. Compatibilidad hacia atrás, no podía romper Collection

2. Necesidad de lazy evaluation para evitar:

  • listas intermedias
  • consumo extra de memoria


3. Paralelismo

Streams permiten: lista.parallelStream() sin cambiar el código lógico


4. Pipeline optimizable


La JVM puede optimizar: filter → map → reduce, como una sola operación


Java no eligió Streams por casualidad.

Fue una decisión para:

  • mantener compatibilidad
  • introducir programación funcional
  • mejorar performance
  • habilitar paralelismo


Mientras otros lenguajes:

  • ya eran funcionales (Scala)
  • o tomaron caminos más simples (Kotlin, Groovy)



martes, 14 de abril de 2026

Alias en imports en Java: lo que no existe (y cómo resolverlo)


Cuando venís de otros lenguajes, es común esperar algo como esto:

import com.ejemplo.A as A1; // ❌


Pero en Java esto simplemente no existe.


 ¿Qué son los alias en imports?


Un alias permite:

  • Importar dos clases con el mismo nombre
  • Y diferenciarlas con nombres alternativos


Ejemplo típico en otros lenguajes:


Kotlin

import com.ejemplo.a.Clase as ClaseA

import com.ejemplo.b.Clase as ClaseB


Scala

import com.ejemplo.a.{Clase => ClaseA}

import com.ejemplo.b.{Clase => ClaseB}


C#

using ClaseA = Ejemplo.A.Clase;

using ClaseB = Ejemplo.B.Clase;


¿Qué pasa en Java?


Si tenés dos clases con el mismo nombre:


import com.ejemplo.a.Clase;

import com.ejemplo.b.Clase; // ❌ conflicto


Java no sabe cuál usar.

La solución es tenés que usar el nombre completo en al menos uno:


import com.ejemplo.a.Clase;


public class Main {

    public static void main(String[] args) {

        Clase a = new Clase();

        com.ejemplo.b.Clase b = new com.ejemplo.b.Clase();

    }

}


¿Por qué Java no tiene alias?


El lenguaje prioriza:

  • Simplicidad
  • Legibilidad explícita
  • Evitar ambigüedades en compilación

No hay transformación de nombres en imports


Problemas reales que genera:

  • Código más verboso
  • Menor ergonomía
  • Conflictos frecuentes en proyectos grandes
  • Difícil integración entre librerías con naming similar

Java no soporta alias en imports y la solución es usar nombres completos. Pero otros lenguajes modernos sí lo resuelven mejor


sábado, 4 de abril de 2026

¿Cual es el estado de los lenguajes que corren sobre la plataforma Java?



La plataforma Java no es solo el lenguaje Java. Gracias a la JVM (Java Virtual Machine), es posible ejecutar múltiples lenguajes con distintos paradigmas y objetivos.

A continuación, un resumen breve de los más relevantes:


Java

Objetivo: Lenguaje generalista, orientado a objetos.

Uso típico: Backend, enterprise, Android (históricamente).

Estado: Activo y en constante evolución (LTS recientes, mejoras funcionales).


Kotlin

Objetivo: Alternativa moderna a Java, más concisa y segura.

Uso típico: Android, backend, multiplataforma.

Estado:  Muy activo, impulsado por JetBrains y adoptado oficialmente por Google.


Scala

Objetivo: Mezclar programación funcional y orientada a objetos.

Uso típico: Big Data, sistemas distribuidos.

Estado: Activo, pero con menor adopción reciente frente a Kotlin.


Groovy

Objetivo: Lenguaje dinámico para simplificar Java.

Uso típico: Scripts, testing, herramientas como Gradle.

Estado:  Estable, pero en segundo plano.


Clojure

Objetivo: Programación funcional pura (Lisp en la JVM).

Uso típico: Sistemas concurrentes, data processing.

Estado: Activo en nichos específicos.


Jython

Objetivo: Implementación de Python sobre la JVM.

Uso típico: Integración con ecosistema Java.

Estado: Limitado (sin soporte moderno de Python 3 completo).


JRuby

Objetivo: Ejecutar Ruby en la JVM.

Uso típico: Integración con sistemas Java.

Estado: Activo, pero nicho.


Frege

Objetivo: Lenguaje funcional inspirado en Haskell.

Uso típico: Académico / experimental.

Estado: Poco activo.


Eta

Objetivo: Llevar Haskell a la JVM.

Uso típico: Funcional puro sobre JVM.

Estado: Proyecto prácticamente detenido.


 JavaScript (GraalVM)

Objetivo: Ejecutar JavaScript en la JVM mediante GraalVM.

Uso típico: Polyglot, microservicios, scripting.

Estado:  Activo y en crecimiento.


Python (GraalVM)

Objetivo: Ejecutar Python sobre la JVM con GraalVM.

Uso típico: Integración polyglot.

Estado:  Experimental.


La JVM es en una plataforma polyglot, donde distintos lenguajes conviven según la necesidad.

miércoles, 18 de febrero de 2026

Controlando los Efectos Colaterales: Elm, Akka y Koka


Los efectos colaterales son inevitables en cualquier programa real. Tarde o temprano necesitamos leer datos, escribir en pantalla, hacer una petición HTTP o guardar algo en una base de datos.

El problema no está en tener efectos, sino en no saber dónde y cuándo ocurren.


Lenguajes y frameworks como Elm, Akka y Koka proponen tres enfoques distintos para enfrentar este desafío, pero todos comparten la misma filosofía:

Mantener la lógica pura y controlada, y ejecutar los efectos en un punto bien definido.


Elm es un lenguaje funcional puro que se ejecuta en el navegador. En Elm, ninguna función puede tener efectos secundarios directamente.

En su lugar, el programa describe qué debería pasar, y el runtime de Elm se encarga de hacerlo realidad.


Por ejemplo, el corazón de un programa Elm suele tener esta función:


update : Msg -> Model -> (Model, Cmd Msg)


Cuando el usuario genera un mensaje (Msg), la función update:

  • recibe el estado actual (Model),
  • devuelve un nuevo modelo,
  • y opcionalmente un comando (Cmd Msg), que describe un efecto.


El punto clave es que update no ejecuta el efecto; simplemente lo describe.

Es el runtime de Elm quien decide cuándo y cómo hacerlo.

Así, todo el código de tu aplicación es puro y determinista, y los efectos están completamente bajo control.


Un ejemplo simple:


update msg model =

    case msg of

        LoadData ->

            ( model, Http.get { url = "/data", expect = GotData } )


        GotData response ->

            ( { model | data = response }, Cmd.none )


Aquí, LoadData devuelve una descripción de un efecto HTTP, pero Elm no lo ejecuta directamente.

La ejecución real ocurre más tarde, en un punto centralizado del runtime.


Akka, en el mundo de Scala y Java, aplica un principio muy similar desde la programación concurrente.

En Akka, los actores son unidades independientes que:

  • procesan un mensaje a la vez,
  • mantienen su propio estado interno,
  • y pueden enviar mensajes a otros actores.


Cada actor es como una pequeña cápsula que contiene su estado y sus efectos.

Nada del mundo exterior puede acceder a su estado directamente; solo se comunica mediante mensajes.


Por ejemplo:


class Counter extends Actor {

  var count = 0


  def receive: Receive = {

    case "inc" =>

      count += 1

      sender() ! count

  }

}


Este actor mantiene su propio contador y solo responde a mensajes.

El efecto (enviar una respuesta, escribir logs, actualizar estado) está encapsulado dentro del actor.


El runtime de Akka —el ActorSystem— se encarga de distribuir los mensajes y ejecutar los efectos concurrentemente, garantizando aislamiento y seguridad.

Desde afuera, un actor parece una función pura: recibe un mensaje y devuelve una respuesta.

Pero los efectos reales ocurren dentro del sistema, no en tu código.


Koka lleva la idea aún más lejos.

En lugar de confiar en un runtime que ejecuta los efectos, Koka los modela en el sistema de tipos.

Cada función en Koka declara explícitamente qué efectos puede producir.

Por ejemplo:


fun add(x : int, y : int) : int {

  x + y

}


Esta función es completamente pura.

En cambio:


fun printAdd(x : int, y : int) : <io> int {

  println("Sumando...")

  x + y

}


Aquí, el tipo <io> indica que esta función puede realizar operaciones de entrada/salida.

Koka rastrea los efectos de manera estática: el compilador sabe exactamente qué partes del programa son puras y cuáles no.


Esto permite escribir programas donde los efectos son explícitos, controlados y predecibles.

No hace falta leer todo el código para saber si una función puede modificar el mundo: el tipo te lo dice.


Aunque Elm, Akka y Koka se desarrollan en contextos muy diferentes —frontend funcional, backend concurrente y teoría de tipos—, los tres comparten una visión profunda de la pureza y el control de efectos.

  • Elm describe los efectos y delega su ejecución al runtime.
  • Akka encapsula los efectos en actores que procesan mensajes de forma aislada.
  • Koka anota los efectos en los tipos, haciéndolos explícitos desde el nivel más básico del lenguaje.


En los tres casos, la meta es la misma: escribir código predecible, testeable y sin sorpresas.


viernes, 13 de febrero de 2026

Inferencia comportamental en Iris


A veces un lenguaje de programación rompe las reglas de lo establecido.

Eso es lo que ocurre con Iris, un lenguaje funcional moderno que se anima a repensar algo que la mayoría de los lenguajes considera “resuelto”: la inferencia de tipos.

Mientras lenguajes como Haskell, Scala o TypeScript utilizan sistemas derivados de Hindley–Milner —que deducen tipos a partir de la forma y las restricciones estáticas del código— Iris propone algo distinto: una inferencia comportamental, donde los tipos no se deducen solo por lo que una expresión es, sino por lo que hace.


Para entender a Iris, conviene empezar por Haskell.

En Haskell, el compilador busca deducir el tipo más general posible de una expresión.

Por ejemplo:

f x = x + 1


El compilador infiere que f :: Num a => a -> a.

Eso significa: f toma algo de tipo a, siempre que a sea una instancia de Num, y devuelve otro a.

La inferencia en Haskell es declarativa: parte de un conjunto de reglas sobre cómo los tipos se combinan.

El compilador no intenta entender qué hace f, sino cómo encaja dentro del sistema de tipos predefinido.


Iris toma otro camino.

Cuando uno escribe algo como:

fn addOne(x) = x + 1


el compilador no busca una clase de tipo Num ni una restricción declarativa.

En cambio, observa el comportamiento del código:

  • nota que x participa en una operación aritmética,
  • deduce que x debe pertenecer a un tipo que sepa sumarse,
  • y propaga esa característica como una capacidad requerida, no como una categoría rígida.


De esta forma, Iris no infiere tipos por forma, sino por rol.

El tipo de x no es una etiqueta estática, sino un conjunto de propiedades comportamentales deducidas del uso.


Este enfoque convierte a Iris en un lenguaje que parece dinámico, pero mantiene seguridad estática.

Por ejemplo, si una función usa un valor solo para imprimirlo, Iris no necesita saber que es un String; basta con que ese valor sepa representarse como texto.

Si más adelante ese mismo valor participa en una suma, el sistema de tipos lo amplía para incluir también la capacidad de actuar como número, sin perder la anterior.


Así, el tipo resultante se vuelve una composición de comportamientos inferidos.

En vez de limitar, los tipos se adaptan al uso que el programa les da.


En Haskell, el tipo Num a => a -> a expresa una restricción declarativa: a debe cumplir las leyes de Num.

En Iris, el tipo inferido sería más bien una descripción semántica: “esta función requiere algo que sepa sumarse.”


La diferencia puede parecer semántica, pero en realidad separa dos filosofías:

  • Haskell razona sobre qué debe cumplirse para que el programa sea válido.
  • Iris razona sobre qué comportamiento se manifiesta en el código.


Haskell clasifica; Iris describe.

El primero etiqueta los valores; el segundo observa lo que hacen.

La inferencia comportamental de Iris puede parecer un experimento académico, pero en realidad apunta a una tendencia profunda: sistemas de tipos que entienden la intención del código, no solo su estructura.

Durante décadas, el tipado estático fue visto como un conjunto de reglas sintácticas para detectar errores antes de ejecutar el programa.

Iris sugiere otra visión: los tipos como modelos del significado del programa.

En lugar de decir “esto es un número”, el compilador podría decir “esto se comporta como algo sumable, imprimible o clonable”.


Ese cambio abre un nuevo horizonte:

  • una inferencia más rica, que se adapta al contexto,
  • una frontera difusa entre lenguajes estáticos y dinámicos,
  • y optimizaciones semánticas, donde el compilador comprende la intención del código.


En última instancia, Iris nos recuerda que los tipos no tienen por qué ser jaulas.

Pueden ser descripciones de comportamiento, espejos del flujo semántico del programa.

Y si el futuro del tipado estático pasa por hacer que las máquinas comprendan el significado del código, quizás Iris esté señalando, silenciosamente, hacia ese horizonte.


Dejo link: https://github.com/connorjacobsen/iris


domingo, 25 de enero de 2026

Scala 3.8: Una evolución significativa del lenguaje


El 22 de enero de 2026 se anunció el lanzamiento de Scala 3.8, una versión que moderniza partes importantes del lenguaje y prepara el camino para Scala 3.9 LTS (soporte a largo plazo). Esta actualización trae cambios técnicos profundos, mejoras en la biblioteca estándar y estabilizaciones de características esperadas por la comunidad. 


Scala 3.8 requiere JDK 17 o posterior para compilar y ejecutar programas. Esto marca una ruptura con versiones anteriores que soportaban JDK 8, y responde a cambios en las futuras versiones de la JVM donde APIs internas como sun.misc.Unsafe ya no son accesibles por defecto. 


Hasta ahora la biblioteca estándar de Scala se compilaba con Scala 2.13 y se reutilizaba desde Scala 3 gracias a la compatibilidad binaria cuidada. En Scala 3.8 la biblioteca estándar ya se compila con Scala 3 nativamente.

Esto no solo moderniza su implementación, sino que sienta las bases para liberar la biblioteca estándar de las dependencias históricas de Scala 2 en versiones futuras. 


A partir de Scala 3.8 el REPL ya no se incluye directamente en la distribución principal del compilador. Ahora es un artefacto independiente que debe agregarse explícitamente si lo necesitás. 

Esto permite:

  • Reducir el tamaño base de Scala.
  • Integrar mejor el REPL en herramientas y entornos de desarrollo.
  • Mejorar la experiencia de uso con nuevas librerías como fansi y pprint, haciendo la salida más legible. 


Scala 3.8 estabiliza varias mejoras importantes al lenguaje que estaban en preview:

“Better Fors”:  La versión mejorada de las comprensiones for que se desugarizan de forma más eficiente y natural ya está activada por defecto. Esto hace al código más predecible y evita algunas operaciones innecesarias de map/flatMap. 


La nueva característica runtimeChecked (SIP-57) reemplaza el uso histórico de: @unchecked cuando queremos que ciertas verificaciones de patrón se hagan en tiempo de ejecución. Es más clara y usable en cadenas de operaciones que pueden lanzar excepciones por patrones no exhaustivos. 


Características experimentales:


Scala 3.8 también introduce varias mejoras en estado de preview, disponibles con el flag -preview:

  • Implicits con into: Permite permitir conversiones implícitas de forma más controlada usando la palabra clave into.
  • Igualdad estricta con patrones: Una novedad experimental que facilita el uso de pattern matching seguro bajo strictEquality.
  • Varargs flexibles: Soporte experimental para spread múltiple en argumentos, haciendo el uso de varargs más expresivo y menos limitado. 


Además hay cambios del compilador y la biblioteca estándar:

  • Recomendaciones en herramientas de construcción: SBT, Mill y Scala CLI necesitan versiones actualizadas para trabajar correctamente con Scala 3.8. 
  • IDEs como IntelliJ IDEA y Metals están adaptándose para dar soporte completo a las nuevas características y cambios en la librería. 


Scala 3.8 marca el final del ciclo de nuevas características antes de entrar en feature freeze para preparar la versión Scala 3.9 LTS, que se espera sea la próxima distribución con soporte a largo plazo. 

Esto significa que muchas de las funciones estabilizadas en 3.8 serán la base para la próxima versión LTS, sin cambios incompatibles mayores cuando se publique. 


Scala 3.8 representa un hito en la evolución del lenguaje:

  • Moderniza la biblioteca estándar.
  • Eleva el requisito de JDK 17+.
  • Mejora el desugaring y el runtimeChecked.
  • Separa el REPL como artefacto.
  • Estabiliza mejoras largamente esperadas.
  • Abre la puerta a nuevas características experimentales.


En resumen, es una versión que cambia la base técnica del lenguaje, prepara el terreno para el futuro y consolida Scala como una de las opciones más potentes en la JVM para programación moderna. 


Dejo link:  https://www.scala-lang.org/news/3.8/

domingo, 30 de noviembre de 2025

Parámetros implícitos en Scala 3: given y using


Scala 3 introdujo una nueva forma de manejar los parámetros implícitos, reemplazando las viejas palabras clave implicit val y implicit def por un sistema más legible: given y using.


def saludar(nombre: String)(using saludo: String): Unit =

  println(s"$saludo, $nombre!")


given String = "Hola"


saludar("Emanuel") // Usa el valor dado automáticamente


given: define un valor que puede usarse de forma implícita.

using: marca el parámetro que puede ser resuelto automáticamente.


Ya no hay necesidad de usar la palabra implicit, lo que hace el código más claro y menos propenso a ambigüedades.

Veamos un ejemplo con varios contextos:


given idioma: String = "español"

given tono: String = "amistoso"


def saludar(nombre: String)(using idioma: String, tono: String): Unit =

  println(s"Saludo en $idioma con tono $tono: ¡Hola, $nombre!")


saludar("Emanuel")


El compilador resuelve ambos using buscando given del tipo adecuado en el ámbito actual.


Scala 2: implicit → flexible, pero a veces confuso.

Scala 3: given/using → más explícito y seguro.


El concepto sigue siendo el mismo: inyectar contextos automáticamente, pero con una sintaxis que favorece la claridad y la mantenibilidad del código.