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

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.

domingo, 23 de noviembre de 2025

Cómo resuelve Scala los parámetros implícitos

En Scala, los parámetros implícitos permiten que una función reciba argumentos sin que el programador los pase explícitamente.

El compilador se encarga de buscar un valor adecuado en el ámbito para completar la llamada.

Veamos un ejemplo simple:


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

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


implicit val saludoPorDefecto: String = "Hola"


saludar("Emanuel") // Usa el valor implícito definido arriba


Cuando Scala ve que falta un argumento para un parámetro implicit, sigue este proceso:

  1. Busca en el ámbito local: valores o funciones marcados como implicit del tipo requerido.
  2. Si no encuentra ninguno, busca en el objeto compañero (companion object) del tipo esperado.
  3. Si encuentra más de uno y no puede decidir cuál usar, el compilador lanza un error por ambigüedad.
  4. Si no encuentra ninguno, lanza un error por parámetro implícito no encontrado.

Veamos un ejemplo de ambigüedad:


implicit val saludo1: String = "Hola"

implicit val saludo2: String = "Buenas"


saludar("Emanuel") // Error: ambiguous implicits


El compilador no puede elegir entre `saludo1` y `saludo2`.


Scala resuelve los parámetros implícitos buscando un valor del tipo adecuado en:

El ámbito local,

Los imports activos,

Y los companion objects relacionados.


Es un mecanismo potente que permite propagar contextos automáticamente (por ejemplo, ExecutionContext, Ordering, Numeric, etc.), reduciendo la necesidad de pasar dependencias manualmente.

viernes, 21 de noviembre de 2025

Parámetros implícitos en Scala ¿y en otros languages?



Scala ofrece una poderosa característica llamada parámetros implícitos (implicit parameters), que permite pasar argumentos a funciones sin tener que especificarlos explícitamente cada vez. Esta capacidad se utiliza mucho para inyección de dependencias, contextos compartidos o type classes.


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

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


implicit val saludoPorDefecto: String = "Hola"


saludar("Emanuel") // imprime "Hola, Emanuel!"


Aquí, el parámetro saludo se pasa de manera implícita gracias a la definición previa de un valor implicit.

Si se define otro valor implícito en el mismo alcance, ese será el utilizado, lo que permite una gran flexibilidad contextual.

Aunque el concepto de implícito es característico de Scala, existen ideas similares:

Haskell usa type classes, que se resuelven de forma implícita por el compilador.

Por ejemplo, la clase `Eq` o `Show` se comporta como una inyección automática de comportamientos según el tipo.


show 42 -- el compilador infiere automáticamente la instancia de Show Int


Rust usa traits y type inference, que cumplen un rol similar. Las implementaciones de traits se aplican automáticamente sin especificarlas cada vez.


println!("{}", 42); // Usa automáticamente el trait Display para i32


C# no tiene parámetros implícitos como tal, pero existen aproximaciones:

  • Inyección de dependencias en frameworks como ASP.NET.
  • Attributes y default parameters.
  • Desde C# 12, Primary constructors y default interface methods permiten inyectar comportamientos contextuales, aunque no son implícitos en tiempo de compilación.


Python tampoco tiene parámetros implícitos, pero se puede emular con:

  • Decoradores.
  • Context managers (with).
  • Argumentos por defecto o variables globales.


Los parámetros implícitos de Scala logran un equilibrio interesante entre claridad y potencia, especialmente en contextos funcionales.

Su uso debe ser cuidadoso, ya que abusar de ellos puede hacer que el flujo de datos sea menos evidente.

Sin embargo, cuando se aplican bien, son una herramienta que simplifica enormemente el código y reduce la verbosidad.

martes, 11 de noviembre de 2025

Type constraints en Elm


Elm es un lenguaje de tipado estático e inferencia fuerte: el compilador deduce los tipos automáticamente, pero también te permite declarar funciones genéricas que funcionan con más de un tipo.

Por ejemplo:


identity : a -> a

identity x = x


Esta función acepta cualquier tipo a.

Sin embargo, a veces queremos restringir qué tipos son válidos.

Ahí entran en juego los type constraints (restricciones de tipo).

En Elm, los type constraints permiten decir:

> “Este tipo genérico debe cumplir con ciertas propiedades (por ejemplo, ser comparable o numérico)”.


A diferencia de Haskell o Scala, Elm no tiene type classes, pero ofrece un pequeño conjunto de restricciones integradas que cubren los casos más comunes.

Elm define cuatro categorías de tipos con restricciones que podés usar en tus firmas de tipo:

  • number: Tipos que soportan operaciones aritméticas como Int, Float
  • comparable: Tipos que pueden ordenarse o compararse  como Int, Float, Char, String, tuples de comparables
  • appendable: Tipos que pueden concatenarse como String, List a
  • compappend:| Tipos que son a la vez comparable y appendable 


Podés restringir una función a operar solo sobre números:


sumar : number -> number -> number

sumar x y =

    x + y


Esto funciona con Int o Float, pero no con String.


Si necesitás ordenar o comparar valores:


menor : comparable -> comparable -> comparable

menor a b =

    if a < b then

        a

    else

        b


O incluso:


ordenar : List comparable -> List comparable

ordenar lista =

    List.sort lista


Cuando querés concatenar elementos:


concatenar : appendable -> appendable -> appendable

concatenar a b =

    a ++ b


Funciona con:


concatenar "Hola, " "mundo!"        -- "Hola, mundo!"

concatenar [1,2] [3,4]              -- [1,2,3,4]


En Elm no se pueden definir tipos personalizados que sean comparable o appendable.

Por ejemplo, este tipo:


type alias Persona =

    { nombre : String, edad : Int }


No puede usarse en una función List.sort directamente.

Pero podés ordenarlo con una clave usando List.sortBy:


ordenarPorEdad : List Persona -> List Persona

ordenarPorEdad personas =

    List.sortBy .edad personas


O definir un criterio personalizado:


ordenarPorNombreDesc : List Persona -> List Persona

ordenarPorNombreDesc personas =

    List.sortWith (\a b -> compare b.nombre a.nombre) personas


Elm mantiene su sistema de tipos simple pero poderoso: no hay typeclasses ni herencia, pero sí restricciones útiles y seguras para los casos más comunes.


domingo, 18 de mayo de 2025

Tipos Abstractos y Polimorfismo en Programación Funcional


Cuando pensamos en abstracción y polimorfismo, solemos imaginar clases, interfaces y herencia. Pero ¿sabías que la programación funcional también tiene sus propios superpoderes para modelar el comportamiento genérico y abstraer detalles? 

En POO, usamos clases abstractas o interfaces para definir estructuras que deben ser implementadas. En programación funcional, el enfoque es distinto, pero el objetivo es similar: ocultar detalles de implementación y exponer un comportamiento general.

Un ADT define un tipo por sus operaciones, no por cómo están implementadas. Por ejemplo:


data Pila a = Vacía | Empujar a (Pila a)


Este tipo Pila podría representar una pila genérica, y podríamos tener funciones que operen sobre ella sin importar cómo esté construida internamente.

En la programación funcional se identifican varios tipos de polimorfismo:

Polimorfismo Paramétrico: Permite escribir funciones genéricas sobre cualquier tipo. Es como los genéricos de Java, pero más poderoso:


identidad :: a -> a

identidad x = x


La función identidad funciona para cualquier tipo a.


Polimorfismo ad-hoc (Typeclasses / Traits / Protocolos) : En Haskell, Rust, Scala o Elixir podemos definir interfaces de comportamiento según el tipo. Esto recuerda al "método virtual" de POO.


class Metrico a where

    distancia :: a -> a -> Double


instance Metrico (Double, Double) where

    distancia (x1, y1) (x2, y2) =

        sqrt ((x2 - x1)^2 + (y2 - y1)^2)


Veamos un ejemplo en Scala:


trait Metrico[T] {

  def distancia(a: T, b: T): Double

}


implicit object Punto2D extends Metrico[(Double, Double)] {

  def distancia(a: (Double, Double), b: (Double, Double)) =

    math.sqrt(math.pow(a._1 - b._1, 2) + math.pow(a._2 - b._2, 2))

}


def calcularDistancia[T](a: T, b: T)(implicit m: Metrico[T]) =

  m.distancia(a, b)


Pattern Matching como Polimorfismo Estructural: Otro recurso poderoso es el pattern matching, que permite seleccionar comportamiento según la "forma" del dato.


sealed trait Forma

case class Circulo(r: Double) extends Forma

case class Rectangulo(ancho: Double, alto: Double) extends Forma


def area(f: Forma): Double = f match {

  case Circulo(r)       => math.Pi * r * r

  case Rectangulo(a, h) => a * h

}


¿Y qué ganamos con esto?

  • Abstracción sin herencia: no hay jerarquías rígidas.
  • Mayor seguridad de tipos: muchos errores se detectan en tiempo de compilación.
  • Separación de datos y comportamiento: las funciones no "viven" dentro de las estructuras de datos, lo cual facilita la composición y el testing.


La programación funcional ofrece mecanismos muy sólidos y expresivos para manejar abstracción y polimorfismo. Aunque no se usa herencia, se logra el mismo efecto (o incluso uno más flexible) usando funciones genéricas, pattern matching y typeclasses.


sábado, 10 de mayo de 2025

Clases padres, clases hijas… ¿y las madres qué?


La programación orientada a objetos (POO) nos trajo muchas cosas lindas: encapsulación, herencia, polimorfismo, y sobre todo, la posibilidad de inventarnos familias disfuncionales de clases sin necesidad de pasar por terapia.

Pero hay algo que siempre nos hizo ruido:

  • ¿Por qué hablamos de clases padre y clases hijas? 
  • ¿Dónde quedaron las madres, los tíos, las primas, o la abuela que todo lo sabe?

Lo más raro es que una clase padre puede ser hija de otra clase. Rarisisimo...

Todo empieza con el inglés. En POO se habla de parent class para referirse a la clase de la cual heredan otras. Y parent, significa “padre o madre”.

Pero claro, en español, por alguna razón misteriosa que seguro involucra a la Real Academia, siempre traducimos parent class como “clase padre”, y no “clase madre” o “clase progenitor/a” (aunque esa última suena a trámites en ANSES).

Entonces… ¿por qué decimos “clase hija”? Acá la gramática mete la cuchara. Como la palabra “clase” es femenina, cuando hablamos de su descendencia lógica usamos “hija” para que concuerde: La clase padre tiene muchas clases hijas.

¿Y si dijéramos “clase madre”?

¡Podemos! No hay ninguna ley que lo impida. De hecho, si queremos romper esquemas y escribir:


class Mamífero // clase madre

class Perro extends Mamífero // clase hija


...nadie de Scala te va a venir a buscar. Al contrario, tal vez sumes puntos con tus profes de literatura.

Eso sí, el término "clase madre" no es tan común, así que si lo usás, preparate para explicar o educar. (O poner una nota al pie tipo “uso madre porque soy inclusivo/a y rebelde”).

La POO no distingue género, pero el lenguaje humano sí. Y en nuestra necesidad de ponerle nombre a todo, terminamos replicando convenciones culturales sin cuestionarlas.

¿Querés decir clase madre? ¡Decilo!

¿Preferís clase base? ¡También está bien!

Lo importante es que tus clases compilen… y que no traumen a sus hijas.



lunes, 28 de abril de 2025

El Poder del underscore (_) en Scala


Scala es un lenguaje conciso y expresivo, y una de sus herramientas más versátiles es el underscore (_). Aunque parece un simple guion bajo, su significado depende del contexto, y puede representar una variable anónima, un tipo genérico, una función parcial, o incluso un importador wildcard. Vamos a repasar sus usos principales con ejemplos simples.

Cuando usás una función que espera un argumento, podés usar _ para decir “acá va ese argumento”.

val nums = List(1, 2, 3)

val dobles = nums.map(_ * 2)  // equivale a nums.map(x => x * 2)

Esto es súper útil para evitar código repetitivo.

Se puede utilizar para ignorar parámetros no usados

Cuando definís una función pero no te interesa usar todos los parámetros:


val funcion = (_: Int) => 42  // ignora el valor que recibe y siempre retorna 42


También se usa para desestructuración parcial:

val (a, _) = (1, 2)  // Ignora el segundo valor


Importaciones tipo wildcard. Igual que en Java con *, pero en Scala se usa _:


import scala.collection.mutable._  // importa todas las clases de mutable


Referencia a métodos como funciones. Cuando pasás un método como función, usas _ para convertirlo a función de orden superior:


def cuadrado(x: Int): Int = x * x

val lista = List(1, 2, 3).map(cuadrado)     // OK

val lista2 = List(1, 2, 3).map(cuadrado _)  // También válido, por conversión explícita


Inicialización por defecto en clases o valores:

var x: String = _  // valor por defecto: null

var y: Int = _     // valor por defecto: 0


Esto es más común en Java-style code o interoperabilidad con frameworks como Spark o Akka.


Tipos genéricos anónimos, como vimos antes:

val lista: List[_] = List("a", 1, true)  // lista de algún tipo desconocido


También podés usar _ <: Animal o _ >: Perro para acotar subtipos o supertypos.

Podés dejar argumentos sin aplicar en una llamada y usar _ para marcar que falta ese valor:


def multiplicar(a: Int, b: Int): Int = a * b

val porDos = multiplicar(2, _: Int)  // función que multiplica por 2

println(porDos(5))  // 10


Dominar el uso del _ te permite escribir código Scala más idiomático y elegante.




martes, 22 de abril de 2025

Tipos Genéricos Anónimos en Scala: Wildcards y Subtipado


Antes de empezar este post tal vez sería bueno que leas este post antes :D

Scala permite definir colecciones y estructuras de datos genéricas, pero a veces no necesitamos saber con precisión qué tipo contienen, o simplemente queremos permitir varios tipos relacionados. Para esos casos existen los tipos genéricos anónimos, representados por el underscore (_).

Un tipo genérico anónimo en Scala se escribe con un guion bajo (_) en lugar de especificar un tipo concreto. Es útil cuando:

  • No necesitás conocer el tipo exacto.
  • Queremos aceptar varios subtipos o supertypos.

Veamos un ejemplo:

Lista de cualquier tipo (wildcard total)


val lista: List[_] = List(1, "hola", true)


Esto indica que lista es de algún tipo List[T], pero no importa cuál es T.


Subtipado: _ <: Tipo

Permite aceptar cualquier subtipo del tipo dado (covarianza).


class Animal

class Perro extends Animal

class Gato extends Animal


val animales: List[_ <: Animal] = List(new Perro, new Gato)


Esto significa: una lista de algo que es subtipo de Animal.


Supertyping: _ >: Tipo

Permite aceptar cualquier supertipo del tipo dado (contravarianza).


val cosas: List[_ >: Perro] = List(new Animal, new Perro)


Esto significa: una lista de algo que es supertipo de Perro.


Y ¿Por qué usar genéricos anónimos?

  • Cuando escribís funciones genéricas que pueden aceptar muchos tipos.
  • Para asegurar compatibilidad con estructuras covariantes/contravariantes.
  • Para restringir o abrir el tipo de manera controlada.

Los tipos anónimos no te permiten hacer mucho con los elementos (no podés acceder a sus métodos específicos), porque Scala no sabe exactamente qué tipo hay.


val lista: List[_] = List("hola", "chau")

// lista(0).toUpperCase()  // ERROR: no se puede garantizar el tipo


En Java existe el signo de pregunta para esto (?):


List<?> lista;

List<? extends Animal> subtipos;

List<? super Perro> supertypos;


En Scala es más limpio y expresivo:


List[_]

List[_ <: Animal]

List[_ >: Perro]


Los tipos genéricos anónimos en Scala te permiten trabajar con estructuras de datos más genéricas y flexibles, especialmente en APIs o librerías donde no se necesita o no se conoce el tipo exacto. Son ideales para mantener la seguridad de tipos sin perder generalidad.


domingo, 20 de abril de 2025

Genéricos en Scala: Covarianza y Contravarianza


Scala es un lenguaje poderoso que combina programación funcional y orientada a objetos. Uno de sus conceptos más sofisticados es el sistema de tipos paramétricos, o genéricos, que permiten escribir código flexible y seguro. 

Los genéricos permiten parametrizar clases, traits y métodos con tipos. Por ejemplo:


class Caja[T](valor: T) {

  def get: T = valor

}


Esta clase puede contener un Int, un String, o cualquier otro tipo.

Imaginemos que tenemos las siguientes clases:


class Animal

class Perro extends Animal


Ahora, supongamos que existe una clase genérica Caja[T]. ¿Debería Caja[Perro] ser un subtipo de Caja[Animal]?


En Scala, la relación de subtipos entre tipos genéricos no se asume automáticamente. Vos decidís explícitamente cómo se comporta con respecto a la varianza.

Scala permite controlar la varianza de un tipo genérico con anotaciones en la declaración del tipo.


Invarianza (sin anotación):

class Caja[T](valor: T)

No hay relación de subtipos entre Caja[Perro] y Caja[Animal].


Covarianza (+T)

class Caja[+T](valor: T)

Caja[Perro] es subtipo de Caja[Animal] si Perro es subtipo de Animal.

Usá covarianza cuando vas a leer datos del tipo genérico, pero no escribir.


Ejemplo:

class ListaCovariante[+A](val cabeza: A)


No podés definir un método como:

def setCabeza(a: A): Unit // ERROR con +A


Porque podría romper la seguridad de tipos.


Contravarianza (-T)


class Procesador[-T] {

  def procesar(t: T): Unit = println("Procesando")

}


Procesador[Animal] es subtipo de Procesador[Perro].

Esto es útil cuando recibís datos del tipo genérico (por ejemplo, funciones o procesadores).

Veamos un ejemplo:


class Animal

class Perro extends Animal

class Gato extends Animal


// Covariante: productor

class CajaProductora[+A](val valor: A)


// Contravariante: consumidor

class Procesador[-A] {

  def procesar(a: A): Unit = println(s"Procesando: $a")

}


Uso:


val perro = new Perro

val gato = new Gato


val caja: CajaProductora[Animal] = new CajaProductora[Perro](perro)

val procesador: Procesador[Perro] = new Procesador[Animal]()


procesador.procesar(perro)


En Scala, los tipos de las funciones son contravariantes en los parámetros y covariantes en el resultado.


val f: Perro => Animal = (a: Animal) => a // válido


¿Por qué? Porque si necesitás una función que acepte Perro, es seguro usar una función que acepte Animal, ya que Animal puede incluir a Perro.


La varianza en Scala te permite expresar con precisión las relaciones de subtipos entre clases genéricas:

  • +T (covariante): útil para leer
  • -T (contravariante): útil para escribir
  • T (invariante): comportamiento por defecto


Comprender este sistema es clave para diseñar APIs robustas, especialmente en programación funcional y colecciones.


sábado, 18 de enero de 2025

El Operador |> de Elixir y sus equivalentes en otros lenguajes


En Elixir, el operador |> pasa el resultado de una expresión como el primer argumento de la siguiente función. Ya lo explicamos en el post anterior. 


" hello "

|> String.trim()

|> String.upcase()

Resultado: "HELLO"


Este diseño promueve una lectura fluida del código, eliminando la necesidad de paréntesis anidados.


F#, un lenguaje funcional inspirado en ML, también tiene un operador pipe |> con un propósito similar al de Elixir.


" hello "

|> String.trim

|> String.uppercase


El operador en F# permite que el flujo de datos sea explícito, facilitando la composición de funciones.


Python no tiene un operador pipe nativo, pero existen bibliotecas que lo emulan, como `pipe` o `toolz`. Sin embargo, sin bibliotecas adicionales, puedes lograr algo similar con reduce:


from functools import reduce


data = " hello "

result = reduce(lambda acc, fn: fn(acc), [str.strip, str.upper], data)

print(result)  # HELLO


Con una biblioteca como pipe:


from pipe import Pipe


result = " hello " | Pipe(str.strip) | Pipe(str.upper)

print(result)  # HELLO


JavaScript aún no tiene un operador pipe oficial, pero hay una propuesta en desarrollo en el comité TC39 (etapa 2 al momento de escribir). Con esta propuesta, el pipe se usa de la siguiente manera:


" hello "

  |> (x => x.trim())

  |> (x => x.toUpperCase());


Por ahora, puedes emularlo con funciones:


const pipeline = (...fns) => x => fns.reduce((v, f) => f(v), x);


const result = pipeline(

  x => x.trim(),

  x => x.toUpperCase()

)(" hello ");

console.log(result); // HELLO


Scala no tiene un operador pipe nativo, pero es posible definir uno:


implicit class PipeOps[T](val value: T) extends AnyVal {

  def |>[R](f: T => R): R = f(value)

}


val result = " hello "

  |> (_.trim)

  |> (_.toUpperCase)

println(result) // HELLO


En C#, aunque no existe un operador pipe, los métodos de extensión de LINQ se comportan de manera similar:


string result = " hello "

    .Trim()

    .ToUpper();

Console.WriteLine(result); // HELLO


El concepto detrás del operador pipe (`|>`) es universal: facilita la composición de funciones y mejora la legibilidad. Aunque su implementación varía entre lenguajes, su propósito sigue siendo el mismo: transformar datos paso a paso de manera clara y concisa.