Translate

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

domingo, 30 de agosto de 2026

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


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


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

La respuesta corta es: no.

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

Si tenemos:

Dog

Cat

Horse


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


            Animal

       /          \           \

     Dog      Cat    Horse

              

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


Animal

  ↑

Dog


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

Podemos expresar la misma idea de otras maneras.

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


Go no tiene clases ni herencia de clases.

Sin embargo, podemos definir una abstracción:


type Animal interface {

    Speak()

}


Y después diferentes tipos pueden satisfacer esa interfaz:


type Dog struct{}


func (Dog) Speak() {

    fmt.Println("Woof")

}


type Cat struct{}


func (Cat) Speak() {

    fmt.Println("Meow")

}


Podemos trabajar con la abstracción:


func makeSpeak(animal Animal) {

    animal.Speak()

}


Y utilizar:

makeSpeak(Dog{})

makeSpeak(Cat{})


No existe: Dog extends Animal


Pero sí existe algo conceptualmente importante:

Dog ───────► Animal

Cat ───────► Animal


Los tipos concretos satisfacen un contrato común.

Y esto permite polimorfismo.

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


Rust sigue una idea similar.

Podemos definir:


trait Animal {

    fn speak(&self);

}


y después implementar ese comportamiento:


struct Dog;


impl Animal for Dog {

    fn speak(&self) {

        println!("Woof");

    }

}


Otro tipo puede hacer lo mismo:


struct Cat;


impl Animal for Cat {

    fn speak(&self) {

        println!("Meow");

    }

}


Podemos utilizar el trait como abstracción:


fn make_speak(animal: &dyn Animal) {

    animal.speak();

}


Y nuevamente tenemos polimorfismo:

        Animal

        /    \

      Dog    Cat


pero sin una jerarquía de herencia.

El mecanismo es diferente.

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

No necesitamos reemplazar la herencia.

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


Por ejemplo:

Abstracción


Animal

 └── speak()


Go lo expresa mediante interfaces.

Rust lo expresa mediante traits.


Polimorfismo


Animal

  ↑

Dog

Cat


Ambos lenguajes permiten que diferentes tipos proporcionen el mismo comportamiento.


Reutilización

En lugar de crear jerarquías:


Animal

 ├── Dog

 │    └── BigDog

 │         └── SuperBigDog


podemos utilizar composición.


En Rust:

struct Engine;


struct Car {

    engine: Engine,

}


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

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

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


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

¿Entonces la herencia no sirve?

Por supuesto que sí.


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

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


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

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


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

Podemos tener:


Generalización

       │

       ▼

   Abstracción


y podemos representar esa generalización mediante: Herencia

pero también mediante:

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


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

La abstracción no pertenece exclusivamente a las clases.


Entonces, ¿Go y Rust son orientados a objetos?

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


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

entonces Go y Rust no son lenguajes orientados a objetos.


Pero esa definición es demasiado restrictiva.

Si consideramos fundamentales conceptos como:

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


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


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


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

No necesariamente es la esencia de OOP.


¿Y Smalltalk?

Esto nos devuelve al origen.

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


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


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


Quizás entonces deberíamos cambiar la pregunta.

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


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

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

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


En definitiva, no necesitamos herencia para pensar en objetos.


Y tampoco necesitamos clases.

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


domingo, 16 de agosto de 2026

¿Y si la abstracción fuera el verdadero hilo conductor de la Orientación a Objetos?


Cuando aprendemos Orientación a Objetos, normalmente aparecen los famosos cuatro pilares:

  • Encapsulamiento
  • Abstracción
  • Herencia
  • Polimorfismo

Pero hay una pregunta interesante: ¿Qué entendemos realmente por abstracción?

Si entendemos la abstracción como un proceso de generalización, aparece una relación mucho más profunda entre varios de estos conceptos.


Imaginemos un conjunto de objetos:


🐕 Dog

🐕 Dog

🐈 Cat

🐈 Cat

🐎 Horse


Observamos características y comportamientos comunes y realizamos una generalización:

Dog ─┐

Dog ─┤

Cat ─┼──► Animal

Cat ─┤

Horse┘


Estamos clasificando y abstrayendo.

Identificamos qué tienen en común distintos objetos y construimos una representación general.

En un modelo basado en clases, esa abstracción puede representarse mediante una clase:


abstract class Animal {

    abstract void makeSound();

}


Esto puede verse como un proceso de clasificación:

> objetos concretos → categoría común → clase.


Pero podemos volver a generalizar.

Tenemos:

Dog

Cat

Horse

   ↓

Animal


y podemos observar que Animal comparte propiedades con otras entidades:


Animal

Plant

   ↓

LivingBeing


Otra vez estamos abstrayendo.

Por lo tanto, la abstracción no necesariamente ocurre una sola vez. Podemos construir niveles sucesivos de generalización:


objetos

   ↓

clases

   ↓

superclases

   ↓

abstracciones más generales


La herencia puede verse como el mecanismo que representa una relación de generalización/especialización:


          Animal

         /      \

       Dog      Cat


Animal representa una abstracción más general.

Dog y Cat son especializaciones de esa abstracción.


Por eso podemos pensar: La abstracción permite identificar lo general; la herencia permite expresar estructuralmente esa generalización en el modelo.


No son lo mismo, pero están profundamente relacionadas.

El polimorfismo aparece cuando podemos utilizar esas especializaciones a través de la abstracción común:


List<Animal> animals = ...;


for (Animal animal : animals) {

    animal.makeSound();

}


No necesitamos saber si estamos tratando con un Dog, un Cat o un Horse.

Trabajamos contra la abstracción Animal.

Entonces podemos visualizarlo así:


                                 ABSTRACCIÓN

                                             │

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

              │                                                            │

        CLASIFICACIÓN                      GENERALIZACIÓN

              │                                                          │

        objetos → clases                        clase → superclase

                                              │

                                       HERENCIA

                                              │

                                  POLIMORFISMO


Entonces, ¿son realmente cuatro pilares independientes?

Quizás no.


Si entendemos abstracción como generalización, entonces la abstracción participa en dos procesos fundamentales del modelado orientado a objetos:

Clasificación: objetos → clase


y generalización


clase → superclase


La herencia es una forma de representar esta última relación, mientras que el polimorfismo permite utilizar diferentes especializaciones mediante una abstracción común.


Esto también nos obliga a revisar una definición muy común: “Abstracción es ocultar los detalles de implementación.”


Eso describe una consecuencia o una técnica de abstracción, pero quizás no su esencia.


Una definición más profunda podría ser: Abstraer es identificar aquello que permanece común entre diferentes casos y construir una representación general que nos permita razonar sobre ellos.

Y desde esa perspectiva, la abstracción no es simplemente uno de los cuatro pilares de OOP: es uno de los procesos fundamentales mediante los cuales construimos el modelo orientado a objetos.

Es decir, la abstracción se utiliza para generalizar un conjunto de objetos en una clase (tomando el comportamiento y atributos comunes) y utilizamos la misma técnica para generalizar un conjunto de clases en clases más generales que nos permitan reutilizar código.  

Y si tomamos esta filosofía, lenguajes como Go o Rust son orientados a objetos, por más que no tengan herencia de forma común (como Java o C#) tienen un mecanismo de abstracción con interfaces. 

Qué opinan?



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.


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 

miércoles, 10 de junio de 2026

Native AOT vs GraalVM Native Image


¿Quién compila mejor a código nativo: .NET o Java?


Durante años, tanto Java como .NET estuvieron dominados por el mismo modelo:

  • bytecode/intermediate language
  • máquinas virtuales
  • JIT compilation
  • optimizaciones runtime


Ese enfoque funcionó extremadamente bien durante décadas.

Pero el mundo cambió.

Hoy vivimos en un ecosistema dominado por:

  • Kubernetes
  • microservicios
  • containers efímeros
  • serverless
  • autoscaling
  • cold starts


Y en ese contexto, el tiempo de arranque y el consumo de memoria comenzaron a importar muchísimo más.


Por eso aparecieron dos tecnologías muy similares:

GraalVM

Native AOT de .NET


Ambas prometen algo revolucionario: Compilar aplicaciones administradas a binarios nativos standalone


Pero aunque el objetivo es parecido, las filosofías y enfoques son bastante diferentes.

En java tradicionalmente:

Java -> Bytecode -> JVM -> JIT -> Código Máquina


En .NET:

C# -> IL -> CLR -> JIT -> Código Máquina


Ambos mundos dependen fuertemente de:

  • compilación dinámica
  • reflection
  • metadata
  • optimizaciones runtime


En java con GraalVM Native Image:

Java -> Bytecode -> análisis estático -> binario nativo


Native AOT:

C# -> IL -> Native AOT toolchain -> binario nativo


En ambos casos:

  • desaparece el JIT
  • desaparece gran parte del runtime
  • mejora muchísimo el startup


El gran objetivo: Cloud Native

Ambas tecnologías existen por el mismo motivo: reducir startup time y consumo de memoria. Y los dos lo hacen muy bien. 


En el tamaño del binario hay diferencias interesantes.

En GraalVM, los binarios suelen ser bastante grandes. Especialmente con frameworks pesados.

Native AOT, también genera binarios relativamente grandes. Aunque frecuentemente más pequeños en aplicaciones simples.


Y los dos sufren por no poder usar reflection.

AOT y GraalVM necesita saber todo el código que será utilizado.

Eso se llama: Closed World Assumption


El compilador asume: “Sé exactamente qué código será ejecutado.”

Pero reflection rompe esa idea.


Frameworks: acá aparece una diferencia importante

Java históricamente fue MUY dinámico


Frameworks como:

  • Spring
  • Hibernate
  • Jakarta EE


usan reflection masivamente.

Eso vuelve más complejo el AOT.


.NET históricamente fue menos dinámico

ASP.NET Core moderno:

  • usa menos magia runtime
  • menos reflection extrema
  • más generación compile-time


Eso hace que Native AOT encaje bastante naturalmente.


Por eso aparecieron frameworks especiales en Java

Para adaptarse a GraalVM nacieron:

  • Quarkus
  • Micronaut
  • Helidon


Todos intentan:

  • minimizar reflection
  • mover trabajo al compile-time
  • ser AOT friendly


Mientras tanto en .NET. ASP.NET Core Minimal APIs ya estaba bastante alineado con esa filosofía.

Por eso Native AOT se siente más “natural” dentro del ecosistema.


Con respecto a Build Time, GraalVM suele sufrir más. La compilación puede tardar muchísimo. Especialmente en aplicaciones grandes.


Porque realiza:

  • análisis estático agresivo
  • optimizaciones complejas
  • tree shaking profundo


Native AOT, también aumenta el tiempo de build.

Pero generalmente menos extremo.

Ambos son excelentes para containers.


Permiten:

  • imágenes más pequeñas
  • menor consumo
  • escalado más rápido
  • menor costo cloud


GraalVM parece:  “Java intentando escapar de la JVM clásica”


Porque el ecosistema Java tradicional:

  • depende muchísimo del runtime
  • reflection
  • proxies
  • bytecode generation


Native AOT parece: “La evolución natural de .NET moderno”


ASP.NET Core ya venía moviéndose hacia:

  • menos magia
  • más compile-time
  • menos configuración runtime
  • minimalismo


¿Entonces quién gana?

Depende completamente del escenario.


GraalVM brilla especialmente si:

  • ya vivís en el ecosistema Java
  • necesitás Spring/Quarkus/Micronaut
  • querés mejorar startup brutalmente


Native AOT brilla especialmente si:

  • trabajás con ASP.NET Core
  • hacés microservicios
  • usás Minimal APIs
  • querés DX muy integrada


Lo más interesante: el regreso del compile-time

Durante años, Java y .NET apostaron fuertemente al runtime dinámico.


Ahora ambos ecosistemas están regresando a:

  • compile-time analysis
  • generación estática
  • tree shaking
  • optimización anticipada


Curiosamente, acercándose cada vez más a ideas tradicionales de:

  • C++
  • Rust
  • Go


 ¿Se está muriendo el JIT?

No.


El JIT sigue siendo extremadamente poderoso.


Especialmente para:

  • aplicaciones grandes
  • workloads largos
  • optimizaciones runtime avanzadas
  • desktop
  • sistemas complejos


Pero el cloud moderno cambió las prioridades.


Hoy muchas veces importa más:

  • arrancar rápido
  • consumir menos memoria
  • escalar rápido
  • reducir costos cloud


Y ahí AOT tiene muchísimo sentido.


Tanto GraalVM como Native AOT representan uno de los cambios más importantes en la evolución moderna de las plataformas administradas.


Ambos intentan resolver el mismo problema: Cómo llevar runtimes modernos al mundo cloud-native

Y aunque utilizan estrategias similares, sus ecosistemas muestran diferencias profundas:

  • Java todavía lucha contra décadas de dinamismo runtime
  • .NET parece haber llegado más preparado al mundo AOT


Sin embargo, ambos demuestran algo muy claro:

El futuro del desarrollo moderno probablemente combine JIT y AOT dependiendo del tipo de aplicación y del contexto donde será ejecutada.


miércoles, 3 de junio de 2026

¿Qué es Native AOT en .NET?


Durante muchos años, las aplicaciones .NET funcionaron utilizando un modelo basado en JIT (Just-In-Time Compilation).

Esto significa que el código C# no se ejecuta directamente como código máquina, sino que primero se compila a un lenguaje intermedio llamado IL (Intermediate Language), y luego el runtime de .NET lo convierte a código nativo en tiempo de ejecución.

Este modelo funciona muy bien… pero tiene costos:

  • startup más lento
  • mayor consumo de memoria
  • necesidad de instalar el runtime
  • contenedores más pesados


Con la llegada del cloud, Kubernetes y serverless, estos problemas comenzaron a ser cada vez más importantes.

Para resolverlos, Microsoft introdujo Native AOT.

Native AOT (Ahead-Of-Time Compilation) es una tecnología de .NET que permite compilar una aplicación directamente a código máquina nativo.


En lugar de:

C# -> IL -> JIT -> Código Máquina


tenemos:

C# -> IL -> Código Máquina Nativo


El resultado es un ejecutable standalone que no necesita el runtime de .NET instalado en la máquina destino.


Conceptualmente, es muy similar a:

  • GraalVM
  • Go
  • Rust
  • Zig


Crearemos una aplicación Native AOT

Paso 1: Crear el proyecto

dotnet new console -n HolaNativeAot

cd HolaNativeAot


Paso 2: Modificar el .csproj

Agregamos:

<PropertyGroup>

    <PublishAot>true</PublishAot>

</PropertyGroup>


El archivo queda algo así:

<Project Sdk="Microsoft.NET.Sdk">


  <PropertyGroup>

    <OutputType>Exe</OutputType>

    <TargetFramework>net9.0</TargetFramework>

    <PublishAot>true</PublishAot>

  </PropertyGroup>


</Project>


Paso 3: Compilar


en Windows

dotnet publish -c Release -r win-x64


en Linux

dotnet publish -c Release -r linux-x64


Se genera algo similar a:

bin/Release/net9.0/linux-x64/publish/


Y dentro encontraremos un ejecutable nativo:

HolaNativeAot


o en Windows:

HolaNativeAot.exe


¿Qué ventajas tiene?

Al no existir JIT:

  • a aplicación arranca casi instantáneamente
  • ideal para serverless
  • ideal para microservicios


Menor consumo de memoria, porque:

  • no hay compilación runtime
  • menos metadata
  • runtime reducido


No hace falta instalar .NET.

El ejecutable ya contiene todo lo necesario.

Native AOT funciona muy bien junto con Docker.

Podemos generar imágenes muy livianas y rápidas de iniciar.


No todo es perfecto.

Native AOT necesita saber en tiempo de compilación qué código será utilizado.

Pero reflection rompe esa idea.


Ejemplo:

var type = Type.GetType("MiClase");

Activator.CreateInstance(type);


El compilador no puede garantizar que ese tipo será usado.

Entonces puede eliminarlo durante el trimming.

Native AOT utiliza una técnica llamada: IL Trimming

El compilador elimina código que considera innecesario.

Muy parecido al concepto de: Closed World Assumption utilizado por GraalVM.


Las librerías más dinámicas suelen tener problemas.


Por ejemplo:

  • reflection pesada
  • generación dinámica de código
  • plugins
  • proxies dinámicos
  • serializers mágicos


Native AOT funciona especialmente bien con:

  • Minimal APIs
  • gRPC
  • Workers
  • herramientas CLI
  • microservicios simples


ASP.NET Core tiene soporte parcial para Native AOT.


Se complica si usamos :

  • MVC tradicional
  • Razor Pages dinámicas
  • plugins
  • reflection intensiva


Existe también: ReadyToRun (R2R)


Se activa con:

<PublishReadyToRun>true</PublishReadyToRun>


No elimina completamente el JIT, pero mejora considerablemente el startup.

Sería una especie de “semi-AOT”.


Native AOT aparece en un contexto muy concreto:

  • Kubernetes
  • containers efímeros
  • serverless
  • autoscaling
  • cold starts


En estos escenarios:

  • arrancar rápido importa
  • consumir menos RAM importa
  • tener imágenes pequeñas importa


El modelo JIT sigue siendo excelente para:

  • aplicaciones grandes
  • desktop
  • sistemas dinámicos
  • frameworks complejos
  • escenarios con mucha reflection


Native AOT está pensado principalmente para:

  • cloud-native
  • microservicios
  • serverless
  • APIs livianas
  • herramientas CLI


Native AOT representa uno de los cambios más importantes en la evolución moderna de .NET.


donde el resultado final es:

  • un único binario
  • startup instantáneo
  • menor consumo de memoria
  • deploy extremadamente simple


Sin embargo, también obliga a abandonar parte de la flexibilidad dinámica que históricamente caracterizó a plataformas como .NET y Java.


Y justamente ahí es donde comienza uno de los debates más interesantes del desarrollo moderno: ¿El futuro es AOT o JIT?




sábado, 2 de mayo de 2026

HTTP 418: I'm a teapot


Si alguna vez viste el código HTTP 418, probablemente pensaste: “¿Esto es real o me están trolleando?”

Spoiler: ambas cosas.


¿Qué significa el error 418?

El código HTTP 418 – "I'm a teapot" es un código de error del cliente (4xx) que, literalmente, significa:

 “Soy una tetera y no puedo hacer café” 


La idea es simple:

El cliente hace una request (ej: “haceme café” ☕)

El servidor responde:

  “No puedo, soy una tetera”


Este código nace en 1998 en el RFC 2324, llamado:

Hyper Text Coffee Pot Control Protocol (HTCPCP)


Sí, un protocolo para controlar cafeteras por internet… y sí, fue publicado el 1 de abril (April Fools). 


La especificación decía cosas como:

  • Método HTTP: BREW
  • Headers para azúcar o leche
  • Y este glorioso error 418

 ¿Es un chiste o es real?

Las dos:

  • Es parte “oficial” del ecosistema HTTP (aunque reservado/no usado formalmente) 
  • No tiene uso serio en producción
  • Pero muchos frameworks lo incluyen igual


Ejemplos reales:

  • Java / Spring: podés devolver 418
  • .NET: `StatusCodes.Status418ImATeapot`
  • Node / Go / Python también lo soportan 


¿Se usa en la vida real?

No debería… pero igual aparece:

  • Easter eggs en APIs
  • Testing de errores
  • Bloqueo de bots
  • Mensajes humorísticos para developers


Incluso hay devs que lo usan para decir: “Tu request es absurda”


Dejo link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/418

lunes, 2 de marzo de 2026

Canales tipados en Go: comunicación segura entre goroutines


Una de las ideas más elegantes de Golang es su modelo de concurrencia por comunicación.

En lugar de compartir memoria y protegerla con locks, Go propone lo contrario: no comuniques compartiendo memoria; compartí memoria comunicándote.

La pieza central de este modelo son los canales — estructuras tipadas que permiten pasar valores entre goroutines (hilos livianos de ejecución) de manera segura y sincronizada.

Un canal (chan) es una estructura que conecta dos goroutines, permitiendo enviar y recibir valores.

Podés imaginarlo como un tubo: lo que se envía en un extremo se recibe en el otro.


ch := make(chan int) // canal que transporta enteros


go func() {

    ch <- 42 // enviar

}()


valor := <-ch // recibir

fmt.Println(valor) // imprime 42


Los canales son tipados: chan int solo puede transportar int, chan string solo string, y así sucesivamente.

Esto significa que el compilador verifica que el tipo del valor enviado o recibido sea correcto.

Una característica clave es que el envío y la recepción están sincronizados:

  • Si una goroutine intenta enviar en un canal sin que otra esté lista para recibir, queda bloqueada.
  • Lo mismo ocurre a la inversa: recibir de un canal vacío bloquea la goroutine hasta que alguien envíe.

Esto elimina la necesidad de usar mutexes o locks para coordinar acceso compartido: los canales actúan como puntos de sincronización naturales.


func worker(done chan bool) {

    fmt.Println("Trabajo iniciado...")

    time.Sleep(time.Second)

    fmt.Println("Trabajo finalizado.")

    done <- true

}


func main() {

    done := make(chan bool)

    go worker(done)

    <-done // espera hasta que worker termine

}


Por defecto, los canales son sin buffer (bloqueantes).

Pero podés crear canales con buffer especificando su capacidad:


ch := make(chan string, 2)

ch <- "hola"

ch <- "mundo"

// No se bloquea hasta llenar el buffer


fmt.Println(<-ch)

fmt.Println(<-ch)


En este caso, el envío solo bloquea si el buffer está lleno, y la recepción solo bloquea si está vacío.

Esto permite desacoplar parcialmente la velocidad de producción y consumo, como en una cola de mensajes ligera.


Otra propiedad importante (y poco conocida por los principiantes) es que los canales pueden ser direccionales.

Podés declarar que una función solo envía o solo recibe valores, reforzando la seguridad de tipos.


func productor(ch chan<- int) {

    for i := 1; i <= 3; i++ {

        ch <- i

    }

    close(ch)

}


func consumidor(ch <-chan int) {

    for n := range ch {

        fmt.Println("Recibido:", n)

    }

}


func main() {

    ch := make(chan int)

    go productor(ch)

    consumidor(ch)

}


Esto evita errores donde una goroutine accidentalmente intente usar un canal en la dirección incorrecta.

En otras palabras, los canales no solo tipan qué pasa, sino también cómo pasa.


Cuando ya no vas a enviar más datos, podés cerrar el canal con close(ch).

El receptor puede detectar el cierre de dos formas:

  1. Usando for range, que itera hasta que el canal se cierra.
  2. Usando la forma de recepción doble:



valor, ok := <-ch

if !ok {

    fmt.Println("Canal cerrado")

}


Cerrar un canal indica “no habrá más valores”.

Intentar enviar a un canal cerrado provoca panic, lo que fuerza a un manejo explícito del ciclo de vida del canal.


El select de Go permite esperar simultáneamente por varios canales, y ejecutar la rama que esté lista primero.


ch1 := make(chan string)

ch2 := make(chan string)


go func() {

    time.Sleep(time.Second)

    ch1 <- "uno"

}()

go func() {

    time.Sleep(2 * time.Second)

    ch2 <- "dos"

}()


for i := 0; i < 2; i++ {

    select {

    case msg1 := <-ch1:

        fmt.Println("Recibido de ch1:", msg1)

    case msg2 := <-ch2:

        fmt.Println("Recibido de ch2:", msg2)

    }

}


select convierte la concurrencia en Go en algo composable: múltiples fuentes de eventos, sin bloqueos ni polling activo.


Patrones comunes con canales:

  1. Fan-out / Fan-in: múltiples productores envían al mismo canal (fan-in) o un productor reparte el trabajo entre varios canales (fan-out).
  2. Pipeline: salida de una goroutine es entrada de otra, formando cadenas de procesamiento asíncronas.
  3. Worker pools: un conjunto de goroutines consumen tareas de un canal común y publican resultados en otro.


Estos patrones se logran con pocas líneas y sin necesidad de bibliotecas adicionales.


Mientras que en lenguajes como Java o C# el modelo concurrente se basa en bloqueos y memoria compartida, Go opta por un modelo más cercano al de CSP (Communicating Sequential Processes):

cada proceso (goroutine) se comunica solo mediante mensajes.

En Go, el canal tipado reemplaza estructuras de sincronización manual como colas bloqueantes o mutexes.

En Erlang, los procesos se comunican con mensajes asíncronos no tipados.

Go adopta una versión más estricta y estáticamente verificada: los canales tienen tipo, dirección y límites de buffer conocidos.


Esto hace que la concurrencia sea más predecible y verificable en tiempo de compilación, reduciendo el riesgo de errores de sincronización y data races.


martes, 20 de enero de 2026

Concurrencia en Crystal: Fibers y Channels al Estilo Go


El lenguaje Crystal combina la sintaxis elegante de Ruby con un modelo de concurrencia ligero y eficiente, inspirado en Go.

En lugar de hilos del sistema operativo, Crystal usa fibers, que permiten ejecutar múltiples tareas de forma concurrente dentro del mismo proceso.

Una fiber es una unidad ligera de ejecución gestionada por el runtime de Crystal (no por el sistema operativo).

Varias fibers pueden ejecutarse “en paralelo” sobre un solo hilo de sistema, haciendo que la concurrencia sea cooperativa y eficiente.


Se crean usando la palabra clave spawn:


spawn do

  puts "Hola desde una fiber!"

end


puts "Hola desde el hilo principal!"

sleep 0.1


Salida posible:

Hola desde el hilo principal!

Hola desde una fiber!


El sleep al final evita que el programa termine antes de que la fiber se ejecute (las fibers corren de forma asíncrona).

Las fibers se comunican a través de channels, una abstracción segura para enviar y recibir mensajes sin necesidad de locks.


channel = Channel(String).new

spawn do

  channel.send("Mensaje desde otra fiber")

end


puts channel.receive


Salida:

Mensaje desde otra fiber


Los Channel son tipados (Channel(Int32), Channel(String), etc.), y pueden usarse para coordinar tareas concurrentes.


Veamos un ejemplo más realista con varias fibers:


channel = Channel(Int32).new


# Productor

spawn do

  5.times do |i|

    puts "Produciendo #{i}"

    channel.send(i)

    sleep 0.2

  end

  channel.close

end


# Consumidor

spawn do

  for value in channel

    puts "Consumiendo #{value}"

  end

end


sleep 2


Salida:

Produciendo 0

Consumiendo 0

Produciendo 1

Consumiendo 1

Produciendo 2

Consumiendo 2

Produciendo 3

Consumiendo 3

Produciendo 4

Consumiendo 4


Crystal no crea múltiples hilos del sistema por cada fiber.

Las fibers son gestionadas por el scheduler del runtime.

El modelo es asíncrono cooperativo: las fibers ceden el control cuando hacen operaciones de I/O o esperan datos.

Este enfoque reduce el costo de cambio de contexto y permite miles de fibers concurrentes sin overhead.

Crystal adopta el modelo CSP (Communicating Sequential Processes) de Go, pero mantiene la simplicidad y legibilidad de Ruby.

Es importante notar que Crystal 1.x usa un solo hilo del sistema (no hay paralelismo real entre núcleos).

Sin embargo, el equipo de Crystal está trabajando en soporte multithreaded para futuras versiones.


Esto significa que las fibers son ideales para:

  • I/O concurrente (HTTP, base de datos, archivos).
  • Operaciones asíncronas livianas.
  • Pero no para tareas intensivas en CPU.


La concurrencia en Crystal es una de sus características más elegantes:

  • Usa fibers para tareas concurrentes sin complicaciones.
  • Permite comunicación segura con channels.
  • Ofrece un modelo simple, escalable y eficiente.


Si disfrutás del enfoque de Go o Elixir, pero querés la sintaxis de Ruby y velocidad de C, Crystal es una alternativa brillante para explorar.

sábado, 17 de enero de 2026

Crystal: El Lenguaje de Programación que Combina la Elegancia de Ruby con la Velocidad de C


En el mundo de la programación, los lenguajes suelen ubicarse entre dos extremos: los rápidos y eficientes, como C o Rust, y los expresivos y productivos, como Ruby o Python.

Crystal intenta unir lo mejor de ambos mundos: la velocidad de C con la sintaxis elegante de Ruby.

Crystal es un lenguaje de programación compilado, tipado estáticamente, y con una sintaxis muy parecida a Ruby.

Está diseñado para ofrecer una experiencia de desarrollo rápida y agradable, sin sacrificar el rendimiento.


Algunos de sus pilares son:

  • Sintaxis legible y concisa.
  • Compilación nativa a binarios.
  • Tipado estático con inferencia de tipos.
  • Recolección de basura (GC).
  • Soporte para concurrencia mediante fibers y channels, inspirados en Go.


Un programa clásico en Crystal se ve así:



def greet(name : String)

  puts "Hola, #{name}!"

end


greet("Emanuel")


A simple vista, parece Ruby. Pero a diferencia de Ruby, Crystal compila a código máquina:


crystal build hello.cr

./hello

# => Hola, Emanuel!


Crystal detecta automáticamente los tipos sin necesidad de declararlos explícitamente, usa inferencia de tipos:


name = "Crystal"

version = 1.12

puts "#{name} #{version}"


El compilador infiere que name es String y version es Float64, verificando los tipos en tiempo de compilación.

Esto evita muchos errores sin perder flexibilidad.


Crystal implementa un modelo de concurrencia basado en fibers (hilos ligeros) y channels, similar a Go:


channel = Channel(Int32).new


spawn do

  3.times do |i|

    channel.send(i)

  end

end


3.times do

  puts "Recibido: #{channel.receive}"

end


Cada spawn ejecuta una tarea concurrente dentro del mismo proceso, permitiendo aplicaciones altamente escalables sin la complejidad de los hilos tradicionales.


Crystal incluye muchas herramientas integradas:

  • crystal build → compila el código a un ejecutable.
  • crystal run → ejecuta directamente un programa.
  • crystal spec → framework de pruebas (similar a RSpec).
  • shards → gestor de dependencias oficial.


Ejemplo de uso con Shards:


shards init

shards install


Crystal se utiliza en:

  • Desarrollo de APIs REST (con frameworks como Kemal o Lucky).
  • CLI tools y aplicaciones de sistema.
  • Programas que requieren rendimiento sin sacrificar legibilidad.


Ejemplo con Kemal (un microframework web):


require "kemal"


get "/" do

  "Hola desde Crystal!"

end


Kemal.run


Crystal es un lenguaje ideal si buscás:

  • La belleza sintáctica de Ruby.
  • El rendimiento de C.
  • Un sistema de tipos seguro pero sin verbosidad.
  • Concurrencia sencilla y eficiente.


Aunque su ecosistema es más pequeño que el de Go o Rust, Crystal está ganando tracción entre quienes valoran productividad y rendimiento equilibrados.



viernes, 2 de enero de 2026

Métodos Monádicos en Go — Parte 2


En el post anterior definimos Result[T], un tipo genérico para representar operaciones que pueden fallar, junto con las funciones Map, FlatMap y OrElse.

Ahora vamos a llevar ese patrón al mundo concurrente, donde los errores y la sincronización suelen volverse un caos si no se estructuran bien.

Go facilita lanzar tareas concurrentes con goroutines, pero mezclar concurrencia y manejo de errores puede ser tedioso:


go func() {

    v, err := doSomething()

    if err != nil {

        // manejar error...

    }

    res, err := doSomethingElse(v)

    if err != nil {

        // otro error...

    }

    // ...

}()


Cada paso necesita su if err != nil, y los canales deben sincronizar resultados y errores manualmente.

Podemos crear una versión concurrente del patrón Result, donde cada operación devuelve un canal que emite un Result[T].


func Async[T any](f func() Result[T]) <-chan Result[T] {

    ch := make(chan Result[T], 1)

    go func() {

        defer close(ch)

        ch <- f()

    }()

    return ch

}


Creamos versiones asíncronas de Map y FlatMap:


func MapAsync[A, B any](in <-chan Result[A], f func(A) B) <-chan Result[B] {

    ch := make(chan Result[B], 1)

    go func() {

        defer close(ch)

        r := <-in

        ch <- Map(r, f)

    }()

    return ch

}


func FlatMapAsync[A, B any](in <-chan Result[A], f func(A) <-chan Result[B]) <-chan Result[B] {

    ch := make(chan Result[B], 1)

    go func() {

        defer close(ch)

        r := <-in

        if r.Err != nil {

            ch <- Err[B](r.Err)

            return

        }

        out := f(r.Value)

        ch <- <-out

    }()

    return ch

}


Supongamos que tenemos operaciones concurrentes que pueden fallar:


func FetchData() Result[int] {

    time.Sleep(time.Millisecond * 100)

    return Ok(10)

}


func Compute(x int) Result[int] {

    return Ok(x * 2)

}


func SaveResult(x int) Result[string] {

    if x > 15 {

        return Ok(fmt.Sprintf("Saved %d", x))

    }

    return Err[string](fmt.Errorf("too small"))

}


Podemos encadenarlas de forma limpia:


result := FlatMapAsync(

    MapAsync(Async(FetchData), Compute),

    func(x int) <-chan Result[string] { return Async(func() Result[string] { return SaveResult(x) }) },

)


fmt.Println(OrElse(<-result, "failed")) // "Saved 20"

Las ventajas son :

  • Las tareas se ejecutan en goroutines separadas.
  • Si ocurre un error en cualquier paso, se propaga automáticamente.
  • No hay if err != nil, ni sincronización manual.


Podés crear fácilmente un combinador All para ejecutar varias tareas concurrentes que devuelvan Result:


func All[T any](tasks ...func() Result[T]) Result[[]T] {

    ch := make(chan Result[T], len(tasks))

    for _, f := range tasks {

        go func(fn func() Result[T]) { ch <- fn() }(f)

    }


    var results []T

    for i := 0; i < len(tasks); i++ {

        r := <-ch

        if r.Err != nil {

            return Err[[]T](r.Err)

        }

        results = append(results, r.Value)

    }

    return Ok(results)

}


Si alguna tarea falla, el error se devuelve inmediatamente.

Si todas tienen éxito, se devuelven los resultados combinados.


El enfoque monádico aplicado a Go te permite combinar funciones puras y goroutines sin perder control del flujo ni del manejo de errores.

Aporta claridad a sistemas concurrentes y simplifica el código de coordinación.

Métodos Monádicos en Go


Go no usa palabras como mónada, pero su estilo basado en funciones y retorno explícito de errores se adapta perfectamente a la idea de encadenar operaciones que pueden fallar, sin perder claridad.

Con los genéricos (desde Go 1.18), podemos escribir funciones reutilizables que se comportan como map, flatMap, orElse de otros lenguajes.


El estilo Go para manejar errores:


value, err := DoSomething()

if err != nil {

    return 0, err

}

result, err := DoSomethingElse(value)

if err != nil {

    return 0, err

}

return result, nil


Funciona bien, pero escala mal cuando hay muchos pasos encadenados.

Podemos mejorarlo aplicando ideas monádicas.

Vamos a representar una operación que puede tener éxito o error:


type Result[T any] struct {

    Value T

    Err   error

}


Y creamos constructores simples:


func Ok[T any](v T) Result[T]   { return Result[T]{Value: v} }

func Err[T any](e error) Result[T] { return Result[T]{Err: e} }


Aplica una función al valor, si no hay error.


func Map[A, B any](r Result[A], f func(A) B) Result[B] {

    if r.Err != nil {

        return Err[B](r.Err)

    }

    return Ok(f(r.Value))

}


Aplica una función que también devuelve un Result, y lo aplana.


func FlatMap[A, B any](r Result[A], f func(A) Result[B]) Result[B] {

    if r.Err != nil {

        return Err[B](r.Err)

    }

    return f(r.Value)

}


OrElse: Devuelve un valor alternativo si hubo error.


func OrElse[T any](r Result[T], fallback T) T {

    if r.Err != nil {

        return fallback

    }

    return r.Value

}


Y como lo usamos? 


func ParseInt(s string) Result[int] {

    n, err := strconv.Atoi(s)

    if err != nil {

        return Err[int](err)

    }

    return Ok(n)

}


func DivideByTwo(n int) Result[int] {

    if n%2 != 0 {

        return Err[int](fmt.Errorf("odd number"))

    }

    return Ok(n / 2)

}


func main() {

    result := FlatMap(ParseInt("42"),

        func(n int) Result[int] {

            return Map(DivideByTwo(n), func(x int) int { return x * 3 })

        })


    fmt.Println(OrElse(result, 0)) // 63

}

¿Cuáles son las ventajas?

  • Si ocurre un error en cualquier paso, se propaga automáticamente.
  • Sin if err != nil en cada línea.
  • Código más funcional y declarativo.


Aunque Go no tenga sintaxis monádica ni azúcar funcional, su modelo basado en valores de retorno y funciones puras encaja perfectamente con la idea.

Con un par de funciones genéricas, podés escribir código más declarativo y mantenible.


viernes, 9 de mayo de 2025

Trabajando con partes de colecciones sin copiar: slices, spans y más


Cuando trabajamos con colecciones o buffers de datos, es muy común necesitar operar solo sobre una parte de ellos: una porción de un array, un segmento de texto, o un rango de bytes. La solución más directa suele ser copiar esa parte a una nueva estructura… pero eso introduce sobrecarga innecesaria de memoria y CPU. Si queremos trabajar con partes de una colección (por ejemplo, un array, un string o un buffer de bytes) sin crear copias, es decir, tener una vista o referencia a un fragmento de la colección original.  

Es decir, queremos qu esta solución sea:

  • Ligera en memoria (sin asignaciones adicionales)
  • Segura (sin acceder fuera de los límites)
  • Eficiente (idealmente sin costo en tiempo de ejecución)

Muchos lenguajes modernos han ido incorporando construcciones para resolver este problema. Veamos cómo lo hacen C#, Go y Rust.

A partir de C# 7.2, se introdujo Span<T>, una estructura de tipo ref struct que representa una ventana sobre memoria contigua. 


int[] datos = { 10, 20, 30, 40 };

Span<int> segmento = datos.AsSpan(1, 2); // contiene {20, 30}


Podés usar Span<T> para:

  • Evitar copiar arrays o strings.
  • Trabajar con memoria en el stack (stackalloc).
  • Reutilizar buffers en pipelines o parsers.
  • Procesar archivos grandes en trozos.


Span<byte> buffer = stackalloc byte[1024]; // sin heap


Limitaciones:

  • No puede usarse como campo de clases (sólo en structs).
  • No se puede usar con async/await ni capturar en lambdas.
  • Solo dentro del alcance del stack (por diseño).


Go tiene slices desde siempre: son una capa por encima de los arrays. Un slice guarda un puntero al array subyacente, longitud y capacidad.


arr := [5]int{1, 2, 3, 4, 5}

s := arr[1:4] // contiene {2, 3, 4}


Ventajas:

  • Livianos y eficientes.
  • Se puede modificar el contenido (afecta al array original).
  • Permiten crecer mediante append si hay capacidad.


s[0] = 99 // también cambia arr[1]


La semántica de slicing en Go es natural y permite componer operaciones sin asignar memoria.


Rust maneja este problema con referencias segmentadas: &[T] para vistas inmutables y &mut [T] para mutables.


let arr = [1, 2, 3, 4];

let slice = &arr[1..3]; // &[2, 3]


Características:

  • Completamente seguras en tiempo de compilación.
  • El borrow checker impide aliasing mutable.
  • Altamente eficientes, sin sobrecarga.
  • Son la base de muchas APIs estándar.


fn print_slice(s: &[i32]) {

    for val in s {

        println!("{}", val);

    }

}


¿Y cuál conviene?

  • Si estás en un lenguaje GC-friendly como C#, Span<T> te da poder sin pagar costo de GC.
  • Si buscás simplicidad y velocidad de desarrollo, Go es imbatible con su slicing natural.
  • Si necesitás seguridad al máximo y performance nativa, Rust con slices es lo más robusto.

Es decir, depende del lenguaje que estes usando ... 

Y Otros lenguajes tambien tenemos cosas parecidas: 

  • C++20: std::span<T> cumple un rol casi idéntico a Span<T> de C#, y también es zero-copy.
  • Python: memoryview permite trabajar con buffers sin copiar, aunque menos seguro.
  • Java: No tiene slices como tal, pero ByteBuffer puede simularlos.
  • Nim, Zig, D: Todos ofrecen slices como vistas eficientes sobre datos.


En la práctica, estas estructuras son fundamentales para escribir código eficiente, especialmente en procesamiento de datos, parsers, sistemas embebidos o cualquier aplicación donde el rendimiento importa.  


martes, 22 de octubre de 2024

Borgo, lenguaje de programación que compila a Go.


Borgo es un lenguaje de programación relativamente nuevo que está diseñado para ser versátil y eficiente en varios entornos de desarrollo. Aunque aún está en sus primeras etapas, promete ofrecer un equilibrio entre alto rendimiento y facilidad de uso, haciendo énfasis en la simplicidad y la expresividad del código.

  1. Simplicidad y Legibilidad: Uno de los pilares de Borgo es la simplicidad en su sintaxis. Se enfoca en evitar la verbosidad innecesaria, permitiendo que los desarrolladores escriban código más limpio y comprensible.
  2. Eficiencia y Rendimiento: Borgo está optimizado para ejecutar código de manera eficiente, lo que lo hace ideal para aplicaciones de alto rendimiento. Su compilador se enfoca en generar binarios pequeños y rápidos.
  3. Soporte para Programación Funcional y Orientada a Objetos: Al igual que lenguajes como Scala y Kotlin, Borgo combina paradigmas funcionales y orientados a objetos, lo que permite a los desarrolladores elegir el enfoque más adecuado para su proyecto.
  4. Concurrencia Nativa: Al ser un lenguaje moderno, Borgo incluye soporte para concurrencia y paralelismo de manera nativa, facilitando la creación de aplicaciones altamente escalables sin tener que recurrir a bibliotecas externas.
  5. Ecosistema Modular: Borgo apuesta por un ecosistema modular, donde los desarrolladores pueden añadir funcionalidad mediante paquetes externos, similar a lo que ofrecen lenguajes como Python con `pip` o Node.js con `npm`.

Relindas las características pero veamos un poco de código: 


use fmt

enum NetworkState<T> {

    Loading,

    Failed(int),

    Success(T),

}


struct Response {

    title: string,

    duration: int,

}


fn main() {

    let res = Response {

        title: "Hello world",

        duration: 0,

    }


    let state = NetworkState.Success(res)


    let msg = match state {

        NetworkState.Loading => "still loading",

        NetworkState.Failed(code) => fmt.Sprintf("Got error code: %d", code),

        NetworkState.Success(res) => res.title,

    }


    fmt.Println(msg)

}


Como ven, es como que go y rust hubieran tenido un hijo... 

Borgo es un lenguaje emergente con mucho potencial, diseñado para combinar simplicidad con eficiencia. Aunque aún no es ampliamente adoptado, sus características prometen hacer que valga la pena seguirlo de cerca, especialmente para aquellos desarrolladores que buscan una alternativa moderna a lenguajes tradicionales. 

Dejo link: https://borgo-lang.github.io/

jueves, 11 de julio de 2024

Como podemos manejar las referencias nulas?


El error más frecuente en Java es NullPointerException y me imagino que en otros lenguajes alguno similar...  Para abordar esto, se han introducido estructuras y operadores que ayudan a manejar la ausencia de valores de manera más segura y explícita. 

Por ejemplo en Java se introdujo la clase `Optional` en la versión 8 para manejar valores potencialmente nulos de una manera más segura. `Optional` es un contenedor que puede o no contener un valor no nulo.

import java.util.Optional;


public class OptionalExample {

    public static void main(String[] args) {

        Optional<String> optional = Optional.of("Hello, World!");

        

        // Verificar si hay un valor presente

        if (optional.isPresent()) {

            System.out.println(optional.get());

        }

        

        // Uso del método ifPresent

        optional.ifPresent(System.out::println);

        

        // Proveer un valor predeterminado

        String value = optional.orElse("Default Value");

        System.out.println(value);

        

        // Proveer un valor predeterminado usando un Supplier

        value = optional.orElseGet(() -> "Default Value from Supplier");

        System.out.println(value);

    }

}


Scala utiliza la clase `Option` para representar un valor opcional. `Option` tiene dos subclases: `Some` y `None`, lo que proporciona una forma elegante y funcional de manejar valores que pueden estar ausentes. Esta idea es similar a la monada `Maybe` en Haskell.


object OptionExample extends App {

  val someValue: Option[String] = Some("Hello, World!")

  val noneValue: Option[String] = None


  // Uso de getOrElse

  println(someValue.getOrElse("Default Value"))

  println(noneValue.getOrElse("Default Value"))


  // Uso del patrón de coincidencia (Pattern Matching)

  someValue match {

    case Some(value) => println(value)

    case None => println("No value")

  }


  noneValue match {

    case Some(value) => println(value)

    case None => println("No value")

  }

}


Scala "copio" esta forma de Haskell. Haskell utiliza el tipo de datos `Maybe` para manejar valores opcionales `Maybe` puede ser `Just` un valor o `Nothing`.


main :: IO ()

main = do

    let someValue = Just "Hello, World!"

    let noneValue = Nothing


    -- Uso de fromMaybe

    putStrLn (fromMaybe "Default Value" someValue)

    putStrLn (fromMaybe "Default Value" noneValue)


    -- Uso del patrón de coincidencia (Pattern Matching)

    case someValue of

        Just value -> putStrLn value

        Nothing -> putStrLn "No value"


    case noneValue of

        Just value -> putStrLn value

        Nothing -> putStrLn "No value"


Kotlin es similar a Scala en muchos aspectos pero no en este. Kotlin introduce el operador `?` para facilitar la gestión de valores nulos. Este operador se utiliza para declarar tipos de datos que pueden ser nulos y para realizar operaciones seguras contra nulos.


fun main() {

    var nullableString: String? = "Hello, World!"


    // Uso del operador ?. para llamadas seguras

    println(nullableString?.length)


    // Uso del operador ?: para proporcionar un valor predeterminado

    val length = nullableString?.length ?: 0

    println(length)


    nullableString = null


    // Uso de let para ejecutar código solo si el valor no es nulo

    nullableString?.let {

        println(it)

    }

}


C# ha incluido varias características para manejar valores nulos, como el operador `?`, que facilita el manejo seguro de tipos que pueden ser nulos.


using System;


class Program

{

    static void Main()

    {

        string? nullableString = "Hello, World!";

        

        // Uso del operador ?. para llamadas seguras

        Console.WriteLine(nullableString?.Length);


        // Uso del operador ?? para proporcionar un valor predeterminado

        int length = nullableString?.Length ?? 0;

        Console.WriteLine(length);


        nullableString = null;


        // Uso de pattern matching para verificar nulos

        if (nullableString is string nonNullString)

        {

            Console.WriteLine(nonNullString);

        }

    }

}


Rust maneja la ausencia de valores y los errores de una manera robusta utilizando los tipos `Option` y `Result`. `Option` puede ser `Some` o `None`, mientras que `Result` puede ser `Ok` o `Err`.


fn main() {

    let some_value: Option<String> = Some("Hello, World!".to_string());

    let none_value: Option<String> = None;


    // Uso de unwrap_or

    println!("{}", some_value.unwrap_or("Default Value".to_string()));

    println!("{}", none_value.unwrap_or("Default Value".to_string()));


    // Uso del patrón de coincidencia (Pattern Matching)

    match some_value {

        Some(value) => println!("{}", value),

        None => println!("No value"),

    }


    match none_value {

        Some(value) => println!("{}", value),

        None => println!("No value"),

    }

}


Go no tiene un tipo de datos específico para manejar valores opcionales, pero utiliza la convención de retornar múltiples valores, incluyendo un valor y un `error`. Que la verdad no me gusta, te pasas preguntando todo el tiempo si hay error o si los valores son nulos. 


package main


import (

    "errors"

    "fmt"

)


func getValue() (string, error) {

    return "Hello, World!", nil

}


func getNullableValue() (string, error) {

    return "", errors.New("no value")

}


func main() {

    value, err := getValue()

    if err != nil {

        fmt.Println("Error:", err)

    } else {

        fmt.Println("Value:", value)

    }


    nullableValue, err := getNullableValue()

    if err != nil {

        fmt.Println("Error:", err)

    } else {

        fmt.Println("Value:", nullableValue)

    }

}


Python utiliza la palabra clave `None` para representar la ausencia de valor. Aunque no tiene una estructura específica como `Optional`, los desarrolladores pueden utilizar condicionales y manejo de excepciones.


def get_value():

    return "Hello, World!"


def get_nullable_value():

    return None


value = get_value()

nullable_value = get_nullable_value()


if value is not None:

    print(value)

else:

    print("Default Value")


if nullable_value is not None:

    print(nullable_value)

else:

    print("Default Value")


Ruby utiliza `nil` para representar la ausencia de valor. Al igual que en Python, no tiene una estructura específica para valores opcionales, pero proporciona métodos para manejar `nil`.


value = "Hello, World!"

nullable_value = nil


# Uso del operador ||

puts value || "Default Value"

puts nullable_value || "Default Value"


# Uso de condicionales

puts value.nil? ? "Default Value" : value

puts nullable_value.nil? ? "Default Value" : nullable_value


C++ utiliza punteros inteligentes (`smart pointers`) para gestionar la memoria y prevenir errores relacionados con punteros nulos. Los punteros inteligentes, como `std::unique_ptr` y `std::shared_ptr`, se encargan de la gestión automática de la memoria.


#include <iostream>

#include <memory>


int main() {

    std::unique_ptr<int> uniquePtr(new int(42));

    if (uniquePtr) {

        std::cout << *uniquePtr << std::endl;

    }


    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);

    if (sharedPtr) {

        std::cout << *sharedPtr << std::endl;

    }


    // Uso de weak_ptr para evitar ciclos de referencia

    std::weak_ptr<int> weakPtr = sharedPtr;

    if (auto lockedPtr = weakPtr.lock()) {

        std::cout << *lockedPtr << std::endl;

    }


    return 0;

}


TypeScript, un superconjunto de JavaScript, permite tipos opcionales y tiene un soporte robusto para manejar valores `null` y `undefined`.


let nullableString: string | null = "Hello, World!";


// Uso del operador ? para llamadas seguras

console.log(nullableString?.length ?? 0);


// Uso de if para asegurar valores no nulos

if (nullableString !== null) {

    console.log(nullableString);

}


TypeScript utiliza tipos opcionales para manejar valores que pueden ser `null` o `undefined`, proporcionando un enfoque seguro para evitar errores comunes relacionados con valores nulos. El operador `?.` permite realizar llamadas seguras, y el operador `??` proporciona valores predeterminados en caso de valores `null` o `undefined`.

En fin, aunque la gestión de valores nulos varía entre lenguajes, la idea subyacente es la misma: proporcionar mecanismos más seguros y expresivos para manejar la ausencia de valores. Ya sea mediante clases contenedoras como `Optional` en Java y `Option` en Scala, tipos de datos como `Maybe` en Haskell, operadores específicos como `?` en Kotlin y C#, punteros inteligentes en C++, o enfoques específicos en Rust, Go, Python y Ruby, estos enfoques ayudan a reducir los errores y a escribir un código más robusto y mantenible.


lunes, 20 de mayo de 2024

Lenguajes utilizados en los proyectos apache

Tal vez hace mucho que Apache publico este gráfico pero yo recién lo veo : 



Como se puede ver Java es el lenguaje más utilizado por los proyectos de apache, seguido de python, c++, c, javascript, scala, C#, go, perl, etc ... 




jueves, 16 de mayo de 2024

Hacer un servicio con gRPC y go-zero


Hagamos un hola mundo con go-zero utilizando gRPC. Para empezar vamos a hacer el archivo .proto : 

syntax = "proto3";


package hello;


option go_package = "./hello";


message Request {

}


message Response {

  string msg = 1;

}


service Hello {

  rpc Ping(Request) returns(Response);

}


$ goctl rpc protoc hello.proto --go_out=server --go-grpc_out=server --zrpc_out=server

Done.

luego hacemos : 

cd server

go mod tidy 


Completamos el archivo server/internal/logic/pinglogic.go


func (l *PingLogic) Ping(in *hello.Request) (*hello.Response, error) {

return &hello.Response{ Msg: "pong" }, nil

}


y luego en el archivo server/etc/hello.yaml agregamos que estamos trabajando en modo dev: 


Name: hello.rpc
ListenOn: 0.0.0.0:8080
Mode: dev

y por ultimo corremos el proyecto: 

go run hello.go



martes, 30 de abril de 2024

Queres probar código y no queres instalar nada? paiza.io


Queres probar código y no queres instalar nada? 

Bueno podes utilizar paiza. Que es una pagina donde podemos correr código en diferentes lenguajes: 

Bash, C, C#, C++, Clojure, Cobol, CoffeeScript, D, Elixir, Erlang, F#, Go, Haskell, Java, JavaScript, Kotlin, MySQL, Nadesiko, Objective-C, Perl, PHP, Python2, Python3, R, Ruby, Rust, Scala, Scheme, Swift, TypeScript, VB


Dejo link: https://paiza.io/

miércoles, 6 de marzo de 2024

Primera API con Go-zero


Después de completar la instalación de goctl, podemos crear un servicio HTTP mínimo para obtener una descripción general del servicio API go-zero de goctl.


# Create workspaces and enter the directory

$ mkdir -p ~/workspace/api && cd ~/workspace/api

# Execute instructions generated demo service

$ goctl api new demo

Done.


Después de ejecutar la instrucción, se generará un directorio de demostración en el directorio actual que contiene un servicio HTTP minimizado, verificaremos la estructura del directorio del servicio.


$ cd ~/workspace/api/demo

$ ls

demo.api demo.go  etc      go.mod   internal

$ tree

.

├── demo.api

├── demo.go

├── etc

│   └── demo-api.yaml

├── go.mod

└── internal

    ├── config

    │   └── config.go

    ├── handler

    │   ├── demohandler.go

    │   └── routes.go

    ├── logic

    │   └── demologic.go

    ├── svc

    │   └── servicecontext.go

    └── types

        └── types.go


Después de completar la generación del código anterior, podemos encontrar los archivos ~/workspace/api/demo/internal/logic/demologic.go, y podemos completar el códigos entre las líneas 27 y 28:

resp = new(types.Response)

resp.Message = req.Name


Después de escribir el código anterior, podemos iniciar el servicio con las siguientes instrucciones:


# Enter service directory

$ cd ~/workspace/api/demo

# to organize dependencies

$ go mod tidy

# Run go program

$ go run demo.go


Cuando vea el siguiente resultado "Starting server at 0.0.0.0.0:888.." indica que el servicio se ha iniciado correctamente, entonces visitamos el servicio HTTP.


$ curl --request GET 'http://127.0.0.0.1:8888/from/me'


Cuando vea el resultado en la terminal {"message":"me"} en nombre de su servicio se inició con éxito.


Y listo!!

Dejo link: https://go-zero.dev/en/docs/tasks/cli/api-demo