Translate

lunes, 7 de octubre de 2024

Evolución de Pattern Matching en C#


El Pattern Matching es una poderosa característica de C# que permite realizar operaciones basadas en coincidencias de patrones. A medida que han pasado las versiones, esta funcionalidad se ha enriquecido significativamente. A continuación, se explica cómo ha evolucionado:

Type Pattern (C# 7.0)

Permite verificar y hacer un cast de un tipo directamente.


if (obj is int i)

{

    Console.WriteLine(i); // Desde C# 7.0

}


Switch con Pattern Matching (C# 7.0)

El switch puede realizar comparaciones de tipos.


switch (obj)

{

    case int i:

        Console.WriteLine(i);  // Desde C# 7.0

        break;

}


Property Pattern (C# 8.0)

Verifica propiedades de un objeto en un switch.


var person = new Person("John", 30);

var result = person switch

{

    { Name: "John", Age: 30 } => "Matched!", // Desde C# 8.0

    _ => "No Match"

};


Positional Pattern (C# 8.0)

Permite trabajar con tipos que tienen métodos `Deconstruct`.


public record Point(int X, int Y);


var point = new Point(1, 2);

string description = point switch

{

    (1, 2) => "Point at (1,2)",  // Desde C# 8.0

    _ => "Other point"

};


Logical Patterns (and, or, not) (C# 9.0)

Se pueden combinar patrones usando operadores lógicos.


int x = 5;

string result = x switch

{

    > 0 and < 10 => "Between 1 and 9",   // Desde C# 9.0

    _ => "Other"

};


Relational Pattern (C# 9.0)

Se usa para comparar valores numéricos directamente en los patrones.


int x = 20;

string result = x switch

{

    > 10 => "Greater than 10",  // Desde C# 9.0

    _ => "10 or less"

};



List Patterns (C# 11.0)

Trabaja con colecciones de una manera más directa.


int[] numbers = { 1, 2, 3 };

var result = numbers switch

{

    [1, 2, ..] => "Starts with 1, 2",  // Desde C# 11.0

    _ => "Other"

};


 Slice Patterns (C# 11.0)

Captura sublistas utilizando `..`.


int[] numbers = { 1, 2, 3, 4 };

var result = numbers switch

{

    [_, _, .. int[] rest] => $"Rest: {string.Join(", ", rest)}",  // Desde C# 11.0

    _ => "No Match"

};


Y falto hablar de las Guards. Un guard es una condición adicional que puedes usar dentro de un patrón de coincidencia para hacer una verificación más específica. Se utiliza con la palabra clave when. Veamos un ejemplo:


int number = 5;


string result = number switch

{

    int n when n > 0 => "Positive number",

    int n when n < 0 => "Negative number",

    _ => "Zero"

};


Console.WriteLine(result);  // Output: Positive number


El Pattern Matching ha evolucionado desde su introducción en C# 7.0, permitiendo escribir código más expresivo, con patrones que facilitan el trabajo con tipos, propiedades, listas y operadores lógicos. Las versiones más recientes, como C# 11.0, añaden aún más flexibilidad, como los List y Slice Patterns. ¡Con cada nueva versión, el Pattern Matching sigue siendo una de las características más poderosas del lenguaje!

sábado, 5 de octubre de 2024

Let assert de Gleam


import gleam/io


pub fn main() {

  let a = unsafely_get_first_element([123])

  io.debug(a)


  let b = unsafely_get_first_element([])

  io.debug(b)

}


pub fn unsafely_get_first_element(items: List(a)) -> a {

  // This will panic if the list is empty.

  // A regular `let` would not permit this partial pattern

  let assert [first, ..] = items

  first

}

El resultado: 

123

Error: Pattern match failed, no pattern matched the value.


let assert es la última forma de bloquear intencionalmente su programa Gleam. Es similar a la palabra clave panic en el sentido de que bloquea cuando el programa ha llegado a un punto al que nunca debería llegar.

let assert es similar a let en el sentido de que es una forma de asignar valores a las variables, pero es diferente en el sentido de que el patrón puede ser parcial. El patrón no necesita coincidir con todos los valores posibles del tipo que se está asignando.

Al igual que panic, esta función debe usarse con moderación y probablemente no debe usarse en absoluto en las bibliotecas.

viernes, 4 de octubre de 2024

Pattern Matching en TypeScript


Pattern Matching es una característica poderosa que permite comparar una estructura de datos con un patrón y ejecutar el código dependiendo de cómo coincidan. Aunque TypeScript no tiene un soporte nativo de pattern matching al estilo de lenguajes como Scala o Haskell, pero se puede simular de manera efectiva utilizando algunas características como los tipos discriminados y el refinamiento de tipos para implementar pattern matching. Estos tipos combinan un campo común discriminante que puede diferenciar uniones de tipos de forma segura.

Veamos un ejemplo:


type Shape = 

  | { kind: 'circle', radius: number }

  | { kind: 'square', sideLength: number }

  | { kind: 'rectangle', width: number, height: number };


function area(shape: Shape): number {

  switch (shape.kind) {

    case 'circle':

      return Math.PI * shape.radius ** 2;

    case 'square':

      return shape.sideLength ** 2;

    case 'rectangle':

      return shape.width * shape.height;

  }

}


const myCircle: Shape = { kind: 'circle', radius: 5 };

console.log(area(myCircle)); // 78.53981633974483


Otra forma de hacer pattern matching es mediante guard clauses, que son condiciones específicas para cada caso. Aquí tienes un ejemplo:


function printNumber(x: number | string): string {

  if (typeof x === 'number') {

    return `Es un número: ${x}`;

  } else if (typeof x === 'string') {

    return `Es una cadena de texto: ${x}`;

  } else {

    return `Valor no soportado`;

  }

}


// Uso

console.log(printNumber(42));   // Es un número: 42

console.log(printNumber('42')); // Es una cadena de texto: 42


TypeScript también permite un estilo de pattern matching mediante desestructuración de objetos y arrays.


type Person = { name: string, age: number };

type Animal = { species: string };


function describe(input: Person | Animal): string {

  if ('name' in input) {

    return `Persona: ${input.name}, Edad: ${input.age}`;

  } else {

    return `Especie: ${input.species}`;

  }

}


// Uso

const person: Person = { name: 'John', age: 30 };

const animal: Animal = { species: 'Dog' };


console.log(describe(person)); // Persona: John, Edad: 30

console.log(describe(animal)); // Especie: Dog


El uso de `switch` puede complementarse con guardias para realizar un matching más fino de patrones, filtrando por condiciones adicionales.


function classifyNumber(x: number): string {

  switch (true) {

    case x < 0:

      return 'Número negativo';

    case x === 0:

      return 'Cero';

    case x > 0:

      return 'Número positivo';

    default:

      return 'Valor desconocido';

  }

}


console.log(classifyNumber(-5));  // Número negativo

console.log(classifyNumber(0));   // Cero

console.log(classifyNumber(10));  // Número positivo


Si bien TypeScript no tiene soporte nativo para el pattern matching al nivel de otros lenguajes funcionales, podemos simularlo utilizando sus características de refinamiento de tipos, tipos discriminados, guard clauses y desestructuración.

Con estos enfoques, puedes aplicar las ideas de pattern matching de forma clara y eficiente en TypeScript. Este tipo de técnica puede mejorar la legibilidad de tu código y hacerlo más fácil de mantener.


jueves, 3 de octubre de 2024

Panic en Gleam


import gleam/io


pub fn main() {

  print_score(10)

  print_score(100_000)

  print_score(-1)

}


pub fn print_score(score: Int) {

  case score {

    score if score > 1000 -> io.println("High score!")

    score if score > 0 -> io.println("Still working on it")

    _ -> panic as "Scores should never be negative!"

  }

}


La palabra clave panic es similar a la palabra clave todo, pero se utiliza para bloquear el programa cuando este ha llegado a un punto al que nunca debería llegar.

¡Esta palabra clave casi nunca debería utilizarse! Puede ser útil en prototipos y scripts iniciales, pero su uso en una biblioteca o aplicación de producción es una señal de que el diseño podría mejorarse. Con tipos bien diseñados, el sistema de tipos se puede utilizar normalmente para hacer que estos estados no válidos sean irrepresentables.

miércoles, 2 de octubre de 2024

Covarianza, Contravarianza e Invarianza en TypeScript


En el contexto de tipos genéricos, la varianza define cómo los subtipos y supertipos de un tipo afectan las relaciones entre otros tipos.

Existen tres tipos principales de varianza:

Covarianza: Un tipo genérico mantiene la relación de subtipos que tiene el tipo con el que trabaja. Es decir, si A es un subtipo de B, entonces Caja<A> será subtipo de Caja<B>.

Contravarianza: Ocurre cuando la relación de subtipos es inversa. Si A es subtipo de B, entonces Caja<B> es subtipo de Caja<A>.

Invarianza: No existe relación entre Caja<A> y Caja<B>, incluso si A es un subtipo de B.

En TypeScript, exiten tipos que son covariantes por defecto. Esto significa que si un tipo genérico tiene un subtipo, esa relación se mantiene con el tipo genérico.

Veamos un ejemplo sencillo para entender esto mejor:


class Animal {

    nombre: string;

    constructor(nombre: string) {

        this.nombre = nombre;

    }

}


class Perro extends Animal {

    ladrar(): void {

        console.log("Guau!");

    }

}


class Gato extends Animal {

    maullar(): void {

        console.log("Miau!");

    }

}


function imprimirAnimales(animales: Animal[]): void {

    animales.forEach(animal => console.log(animal.nombre));

}


const perros: Perro[] = [new Perro("Max"), new Perro("Rex")];

imprimirAnimales(perros); // Esto es válido, ya que Perro es subtipo de Animal


En este ejemplo, Perro[] es subtipo de Animal[], por lo que la función imprimirAnimales puede recibir una lista de perros sin problema. Esto es covarianza.

Contravarianza significa que un tipo más general (supertipo) puede ser pasado donde se espera un tipo más específico (subtipo). En TypeScript, esto es común al trabajar con funciones.


class Carnivoro extends Animal {}

class Herviboro extends Animal {}


function alimentarAnimal(fn: (a: Carnivoro) => void): void {

    const leon = new Carnivoro("León");

    fn(leon);

}


function alimentarCualquierAnimal(a: Animal): void {

    console.log(`Alimentando a un ${a.nombre}`);

}


alimentarAnimal(alimentarCualquierAnimal); // Funciona gracias a la contravarianza

Aquí, la función alimentarCualquierAnimal puede ser utilizada donde se espera una función que trabaje con Carnivoro, ya que Animal es un supertipo de Carnivoro. Esto es contravarianza.

Si un tipo es invariante, no puedes intercambiar subtipos y supertipos, incluso si existe una relación de herencia entre ellos. Este comportamiento es menos común en TypeScript.


Ejemplo de invarianza:


class Caja<T> {

    contenido: T;

    constructor(contenido: T) {

        this.contenido = contenido;

    }

}


const cajaAnimal: Caja<Animal> = new Caja(new Animal("Elefante"));

const cajaPerro: Caja<Perro> = new Caja(new Perro("Max"));


// Esto genera error porque Caja<Perro> no es subtipo de Caja<Animal>

// cajaAnimal = cajaPerro; 


En este caso, Caja<Perro> no es subtipo de Caja<Animal>, aunque Perro sea subtipo de Animal. Esta es la invarianza, donde no hay compatibilidad entre tipos genéricos con diferentes parámetros de tipo.

En TypeScript, no puedes hacer que Caja<Animal> sea compatible con Caja<Perro> directamente debido a la invarianza de los tipos genéricos. Sin embargo, hay formas de aproximar este comportamiento utilizando tipos genéricos más flexibles, como lo son los tipos comodín (similares a los de Java) o utilizando el modificador readonly para hacer la relación covariante.

Se puede hacer que Caja<T> sea covariante para las lecturas si el contenido de la caja solo es accesible de forma de lectura y no de escritura. Esto se logra declarando las propiedades como `readonly`.


class Caja<out T> {

    readonly contenido: T;

    constructor(contenido: T) {

        this.contenido = contenido;

    }

}


const cajaAnimal: Caja<Animal> = new Caja(new Animal("Elefante"));

const cajaPerro: Caja<Perro> = new Caja(new Perro("Max"));

// Como la propiedad es de solo lectura, es covariante

const otraCajaAnimal: Caja<Animal> = cajaPerro; // Funciona


En este caso, al usar readonly, puedes asignar una Caja<Perro> a una Caja<Animal> porque las cajas son covariantes en la lectura. No obstante, ya no podrías modificar el contenido de la caja.

Otra opción es usar la palabra clave extends para indicar que puedes trabajar con cualquier subtipo de Animal.


class Caja<T extends Animal> {

    contenido: T;

    constructor(contenido: T) {

        this.contenido = contenido;

    }

}


function procesarCajaAnimal(caja: Caja<Animal>) {

    console.log(`Animal: ${caja.contenido.nombre}`);

}


const cajaPerro: Caja<Perro> = new Caja(new Perro("Max"));

procesarCajaAnimal(cajaPerro); // Funciona


Aquí estamos diciendo que Caja<T> puede aceptar cualquier tipo que extienda de Animal. Esto permite que una Caja<Perro> sea pasada a una función que espera una Caja<Animal>.

DIPLOMATURA INTRODUCCIÓN A LA PROGRAMACIÓN EN PYTHON


La Diplomatura de Introducción a la Programación con Python es una propuesta diseñada para dar respuesta a una demanda específica de diferentes entornos públicos y privados, así como para proporcionar a los y las participantes una sólida base en programación utilizando el lenguaje Python. Con un enfoque accesible y versátil, la diplomatura busca democratizar el conocimiento tecnológico, preparando a quienes la realicen para enfrentar los desafíos digitales de la actualidad.

La diplomatura consta de 7 módulos con una duración horaria total de 220 horas reloj. Los y las participantes adquirirán habilidades prácticas en programación, aprenderán a desarrollar aplicaciones web simples, trabajarán con bases de datos y se familiarizarán con herramientas esenciales. El enfoque práctico del trayecto formativo se refuerza mediante proyectos reales, permitiendo a los y las estudiantes aplicar de inmediato sus conocimientos en escenarios del mundo real.


Dejo link: https://fcyt.uader.edu.ar/diplomaturapython/

sábado, 28 de septiembre de 2024

Clases Genéricas en TypeScript


Las clases genéricas en TypeScript nos permiten escribir código flexible y reutilizable. En lugar de definir clases específicas para cada tipo de dato, podemos crear clases genéricas que trabajen con múltiples tipos, manteniendo la seguridad de tipos que ofrece TypeScript.

Una clase genérica es aquella que acepta uno o más parámetros de tipo. Estos parámetros de tipo funcionan como marcadores de posición que representan un tipo de dato específico en el momento de la instanciación de la clase.

Veamos un ejemplo: 


class Caja<T> {

    contenido: T;


    constructor(contenido: T) {

        this.contenido = contenido;

    }


    obtenerContenido(): T {

        return this.contenido;

    }

}



En este ejemplo, T es un parámetro de tipo que se utiliza en la clase Caja. No sabemos qué tipo de dato es T hasta que se instancie la clase con un tipo específico.

Puedes instanciar la clase `Caja` con cualquier tipo de dato:


const cajaDeNumero = new Caja<number>(123);

console.log(cajaDeNumero.obtenerContenido()); // 123

const cajaDeTexto = new Caja<string>('Hola');

console.log(cajaDeTexto.obtenerContenido()); // Hola


El tipo `T` se reemplaza con el tipo proporcionado (`number` o `string`), manteniendo el control de tipos y evitando errores durante la compilación.

También es posible utilizar múltiples parámetros de tipo en una clase genérica. Por ejemplo:


class Par<K, V> {

    clave: K;

    valor: V;


    constructor(clave: K, valor: V) {

        this.clave = clave;

        this.valor = valor;

    }


    obtenerClave(): K {

        return this.clave;

    }


    obtenerValor(): V {

        return this.valor;

    }

}


const par = new Par<string, number>('edad', 30);

console.log(par.obtenerClave()); // 'edad'

console.log(par.obtenerValor()); // 30


En este caso, `Par` es una clase genérica que acepta dos parámetros de tipo: `K` para la clave y `V` para el valor. Luego, se instanció con `string` como clave y `number` como valor.

A veces queremos limitar los tipos que un parámetro genérico puede aceptar. Esto se hace usando la palabra clave `extends` para aplicar una restricción al parámetro de tipo.


class LimitarCaja<T extends number | string> {

    contenido: T;


    constructor(contenido: T) {

        this.contenido = contenido;

    }


    mostrarContenido(): void {

        console.log(`Contenido: ${this.contenido}`);

    }

}


const cajaTexto = new LimitarCaja('Hola');

cajaTexto.mostrarContenido(); // Contenido: Hola


const cajaNumero = new LimitarCaja(123);

cajaNumero.mostrarContenido(); // Contenido: 123


Aquí, la clase LimitarCaja solo acepta tipos que sean number o string, lo que añade una restricción al tipo T.

Como ventaja de las clases genericas podemos nombrar: 

  • Reutilización del código: Puedes usar la misma clase para diferentes tipos sin necesidad de duplicar código.
  • Seguridad de tipos: TypeScript asegura que se utilicen los tipos correctos, evitando errores en tiempo de ejecución.
  • Flexibilidad: Permiten que el código sea más flexible, ya que puedes trabajar con cualquier tipo sin perder el control del sistema de tipos.

Las clases genéricas en TypeScript son una herramienta poderosa para crear código flexible y reutilizable. Al permitir que las clases trabajen con múltiples tipos, puedes crear soluciones que se adapten a diferentes escenarios sin sacrificar la seguridad de tipos que hace que TypeScript sea tan útil.



viernes, 27 de septiembre de 2024

ToDo de Gleam


pub fn main() {

  todo as "I haven't written this code yet!"

}


pub fn todo_without_reason() {

  todo

}

warning: Todo found

  ┌─ /src/main.gleam:6:3

  │

6 │   todo

  │   ^^^^ This code is incomplete


This code will crash if it is run. Be sure to finish it before

running your program.


La palabra clave todo se utiliza para especificar que algún código aún no se ha implementado.

En el todo podemos especificar una descripción, aunque es posible que desee incluir el mensaje si tiene más de un bloque de código marcado como todo en su código.

Cuando se utiliza, el compilador de Gleam imprimirá una advertencia para recordarle que el código no está terminado y, si se ejecuta el código, el programa se bloqueará con el mensaje indicado.

miércoles, 25 de septiembre de 2024

Apache Geode: Almacenamiento de Datos Distribuido en Tiempo Real


En el mundo actual, donde las aplicaciones demandan respuestas inmediatas y alta disponibilidad de datos, es crucial contar con herramientas que permitan gestionar datos en tiempo real de manera distribuida. Apache Geode es una de esas soluciones que permiten construir aplicaciones escalables y altamente disponibles.

Apache Geode es un sistema de almacenamiento de datos en memoria distribuido que ofrece capacidades de procesamiento y almacenamiento en tiempo real. Originalmente desarrollado por GemStone bajo el nombre de GemFire, pasó a ser un proyecto de código abierto bajo el paraguas de la Apache Software Foundation. 

Apache Geode se caracteriza por su capacidad para almacenar datos en memoria distribuidos entre varios nodos, permitiendo así un acceso rápido y eficiente a grandes volúmenes de datos. Ofrece baja latencia, alta disponibilidad, y consistencia, lo que lo convierte en una opción ideal para aplicaciones críticas que requieren acceso en tiempo real a los datos.

Apache Geode distribuye los datos entre múltiples nodos (o servidores), formando un clúster donde cada nodo puede almacenar una parte de los datos. De esta forma, el sistema es capaz de escalar horizontalmente a medida que aumenta la demanda. Además, Geode puede replicar los datos entre los nodos para garantizar redundancia y alta disponibilidad.

La arquitectura de Apache Geode permite particionar y replicar datos de manera eficiente. Esto significa que cada partición de datos se almacena en un nodo del clúster, y estas particiones pueden replicarse en otros nodos para evitar pérdida de información en caso de fallos.

Además, Geode ofrece consistencia fuerte, lo que significa que los datos son siempre consistentes entre las réplicas, lo cual es fundamental en entornos de alta disponibilidad.

Características Principales de Apache Geode:

  1. Almacenamiento en Memoria: Apache Geode utiliza la memoria principal de los servidores para almacenar los datos, lo que reduce drásticamente la latencia de acceso en comparación con bases de datos tradicionales basadas en disco.
  2. Distribución y Replicación de Datos: Los datos en Geode se distribuyen entre varios nodos y pueden replicarse para garantizar redundancia y alta disponibilidad.
  3. Alta Disponibilidad y Tolerancia a Fallos: Al replicar los datos en diferentes nodos del clúster, Geode garantiza que los datos estarán disponibles incluso si uno o varios nodos fallan.
  4. Consistencia: Apache Geode asegura consistencia fuerte, es decir, cualquier cambio en los datos es inmediatamente visible en todos los nodos que almacenan copias del mismo dato.
  5. Procesamiento en Tiempo Real: Permite realizar consultas y operaciones sobre los datos en tiempo real, manteniendo la latencia baja incluso en sistemas con altos volúmenes de transacciones.
  6. Soporte para APIs de Java y Spring: Geode está profundamente integrado con Java y tiene un fuerte soporte para el ecosistema Spring, lo que facilita su integración en aplicaciones Java empresariales.
  7. Persistencia: Aunque su principal almacenamiento es en memoria, Geode permite configurar persistencia en disco para asegurar que los datos no se pierdan tras un reinicio o fallo catastrófico.

Y donde podemos utilizar Geode: 

  1. Aplicaciones Financieras: En sistemas de trading y banca, donde la baja latencia y la consistencia de datos son cruciales, Geode se utiliza para garantizar acceso rápido a los datos en tiempo real.
  2. eCommerce: Plataformas de comercio electrónico, donde es necesario manejar grandes cantidades de usuarios concurrentes y transacciones, pueden beneficiarse de la capacidad de escalado y alta disponibilidad de Geode.
  3. Sistemas de Telecomunicaciones: Las redes de telecomunicaciones requieren acceso constante a la información del usuario y deben procesar grandes volúmenes de datos en tiempo real, algo que Apache Geode maneja eficientemente.
  4. Monitorización en Tiempo Real: Para sistemas de monitoreo y análisis en tiempo real, Geode permite el procesamiento y la toma de decisiones rápidas basadas en datos en memoria, sin la necesidad de acceder a discos lentos.

Cuando comparamos a Apache Geode con otras soluciones de almacenamiento en memoria como Redis o Hazelcast, la diferencia radica en el soporte más amplio de Apache Geode para modelos de datos más complejos y la integración nativa con Spring, lo que facilita su adopción en entornos Java empresariales.

Por otro lado, en comparación con bases de datos tradicionales como MySQL o PostgreSQL, Geode ofrece una arquitectura distribuida en memoria, lo que reduce significativamente la latencia de acceso y permite una mayor escalabilidad.

Apache Geode es una herramienta poderosa para desarrollar aplicaciones críticas que requieren almacenamiento distribuido en memoria y acceso en tiempo real a los datos. Su integración con tecnologías como Java y Spring, junto con su capacidad de escalabilidad y alta disponibilidad, lo convierten en una opción excelente para sectores como finanzas, telecomunicaciones, y comercio electrónico.

Dejo link; https://geode.apache.org/

domingo, 22 de septiembre de 2024

Use de Gleam parte 2


La expresión use es una sintaxis simplificada para una llamada a una función normal y una función anónima.


Este código:

use a, b <- my_function

next(a)

next(b)


Es igual que este código:


my_function(fn(a, b) {

  next(a)

  next(b)

})


Para garantizar que la expresión use funcione y sea lo más comprensible posible, lo ideal es que el lado derecho sea una llamada de función en lugar de una secuencia de comandos u otra expresión, que suele ser más difícil de leer.

use es una expresión como todo lo demás en Gleam, por lo que se puede colocar dentro de bloques.


viernes, 20 de septiembre de 2024

Programación Reactiva con Spring


La programación reactiva es un paradigma orientado a manejar flujos de datos de manera asíncrona, lo cual resulta ideal para aplicaciones con alta carga de tráfico, como sistemas de microservicios. Con Spring, la programación reactiva se facilita mediante el módulo Spring WebFlux.

Spring WebFlux es el módulo reactivo de Spring que permite construir aplicaciones no bloqueantes y asíncronas. Está basado en el patrón Reactor, que sigue el estándar de Reactive Streams y utiliza Mono y Flux como las abstracciones principales para manejar uno o varios elementos, respectivamente.

  • Mono: Representa 0 o 1 elemento.
  • Flux: Representa 0 o N elementos.
  • Backpressure: Mecanismo para controlar la velocidad de emisión de los eventos.

Primero, crea un proyecto de Spring Boot en Spring Initializr. Asegúrate de agregar las dependencias de Spring Reactive Web.


<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-webflux</artifactId>

</dependency>


A continuación, implementamos un controlador que responde a peticiones HTTP de manera reactiva usando `Flux` y `Mono`.


import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Flux;

import reactor.core.publisher.Mono;


import java.time.Duration;


@RestController

public class ReactiveController {


    // Endpoint que devuelve un solo valor (Mono)

    @GetMapping("/mono/{id}")

    public Mono<String> getMono(@PathVariable String id) {

        return Mono.just("Valor recibido: " + id);

    }


    // Endpoint que devuelve un flujo continuo de datos (Flux)

    @GetMapping("/flux")

    public Flux<Integer> getFlux() {

        return Flux.range(1, 10)

                   .delayElements(Duration.ofSeconds(1));

    }

}


Una vez que hayas implementado el controlador, ejecuta la aplicación de Spring Boot. Puedes acceder a los endpoints:

  • /mono/{id}: Devuelve un único valor. Ejemplo: http://localhost:8080/mono/5
  • /flux: Devuelve un flujo de datos continuo que se emite cada segundo. Ejemplo: http://localhost:8080/flux
Como ventajas podemos ver: 
  • Eficiencia y Escalabilidad: Ideal para manejar aplicaciones que requieren alta concurrencia y asíncronas.
  • Control del Flujo: Con Reactive Streams puedes controlar el ritmo de procesamiento y evitar la sobrecarga del sistema.
  • Menos Bloqueo: No hay hilos bloqueados mientras se esperan respuestas, lo que mejora el rendimiento en sistemas de alta demanda.


martes, 17 de septiembre de 2024

Calculadora de notación polaca inversa en Erlang


La mayoría de las personas han aprendido a escribir expresiones aritméticas con los operadores entre los números ((2 + 2) / 5). Así es como la mayoría de las calculadoras te permiten insertar expresiones matemáticas y probablemente la notación con la que te enseñaron a contar en la escuela. Esta notación tiene la desventaja de que necesitas saber sobre la precedencia de los operadores: la multiplicación y la división son más importantes (tienen una precedencia más alta) que la suma y la resta.

Existe otra notación, llamada notación de prefijo o notación polaca, donde el operador va antes de los operandos. Bajo esta notación, (2 + 2) / 5 se convertiría en (/ (+ 2 2) 5). Si decidimos decir que + y / siempre toman dos argumentos, entonces (/ (+ 2 2) 5) puede escribirse simplemente como / + 2 2 5.

Sin embargo, nos centraremos en la notación polaca inversa (o simplemente RPN), que es lo opuesto a la notación de prefijo: el operador sigue a los operandos. El mismo ejemplo que el anterior en RPN se escribiría 2 2 + 5 /. Otras expresiones de ejemplo podrían ser 9 * 5 + 7 o 10 * 2 * (3 + 4) / 2 que se traducen a 9 5 * 7 + y 10 2 * 3 4 + * 2 /, respectivamente. Esta notación se utilizó mucho en los primeros modelos de calculadoras, ya que ocupaba poca memoria para su uso. 

En primer lugar, puede ser bueno entender cómo leer expresiones RPN. Una forma de hacerlo es encontrar los operadores uno por uno y luego reagruparlos con sus operandos por aridad:

10 4 3 + 2 * -

10 (4 3 +) 2 * -

10 ((4 3 +) 2 *) -

(10 ((4 3 +) 2 *) -)

(10 (7 2 *) -)

(10 14 -)

-4

Sin embargo, en el contexto de una computadora o una calculadora, una forma más sencilla de hacerlo es hacer una pila de todos los operandos tal como los vemos. Tomando la expresión matemática 10 4 3 + 2 * -, el primer operando que vemos es 10. Lo agregamos a la pila. Luego está el 4, así que también lo colocamos en la parte superior de la pila. En tercer lugar, tenemos el 3; coloquemos también ese en la pila. Nuestra pila ahora debería verse así:

Una pila que muestra los valores [3 4 10]

El siguiente carácter a analizar es un +. Esa es una función de aridad 2. Para poder usarla, necesitaremos alimentarla con dos operandos, que se tomarán de la pila.

Entonces tomamos 3 y 4 de la pila, utilizados en la expresión de sufijo '3 4 +' y que devuelve 7 y ponemos este valor en la parte superior de la pila

La pila ahora es [7,10] y lo que queda de la expresión es 2 * -. Podemos tomar el 2 y colocarlo en la parte superior de la pila. Luego vemos *, que necesita dos operandos para funcionar. Nuevamente, los tomamos de la pila. Los operandos 2 y 7 tomados de la pila, utilizados en '7 2 *', que devuelve 14. Y colocamos 14 de nuevo en la parte superior de nuestra pila. Todo lo que queda es -, que también necesita dos operandos. 

Dibuje los operandos 14 y 10 tomados de la pila en la operación '10 14 -' para el resultado '-4'

Y así tenemos nuestro resultado. Este enfoque basado en la pila es relativamente infalible y la poca cantidad de análisis que se necesita hacer antes de comenzar a calcular los resultados explica por qué era una buena idea que las calculadoras antiguas lo usaran.

Escribir esta solución en Erlang no es demasiado difícil una vez que hemos hecho las cosas complejas. Resulta que la parte difícil es averiguar qué pasos se deben realizar para obtener nuestro resultado final y eso es lo que acabamos de hacer. Creemos un archivo llamado calc.erl.

La primera parte de la que preocuparse es cómo vamos a representar una expresión matemática. Para simplificar las cosas, probablemente los ingresaremos como una cadena: "10 4 3 + 2 * -". Esta cadena tiene espacios en blanco, lo cual no es parte de nuestro proceso de resolución de problemas, pero es necesario para usar un tokenizador simple. Lo que sería utilizable entonces es una lista de términos de la forma ["10", "4", "3", "+", "2", "*", "-"] después de pasar por el tokenizador. Resulta que la función string:tokens/2 hace exactamente eso:

> string:tokens("10 4 3 + 2 * -", " ").

["10","4","3","+","2","*","-"]

Esa será una buena representación para nuestra expresión. La siguiente parte a definir es la pila. ¿Cómo vamos a hacer eso? Es posible que hayas notado que las listas de Erlang actúan de manera muy similar a una pila. El uso del operador cons (|) en [Head|Tail] se comporta de manera efectiva de la misma manera que colocar Head en la parte superior de una pila (Tail, en este caso). Usar una lista para una pila será suficiente.

Para leer la expresión, solo tenemos que hacer lo mismo que hicimos cuando resolvimos el problema a mano. Leer cada valor de la expresión, si es un número, colocarlo en la pila. Si es una función, extraer todos los valores que necesita de la pila y luego volver a colocar el resultado. Para generalizar, todo lo que necesitamos hacer es recorrer toda la expresión como un bucle solo una vez y acumular los resultados. ¡Suena como el trabajo perfecto para un fold!

Lo que necesitamos planificar es la función que lists:foldl/3 aplicará en cada operador y operando de la expresión. Esta función, como se ejecutará en un pliegue, necesitará tomar dos argumentos: el primero será el elemento de la expresión con el que se trabajará y el segundo será la pila.

Podemos comenzar a escribir nuestro código en el archivo calc.erl. Escribiremos la función responsable de todos los bucles y también de la eliminación de espacios en la expresión:


-module(calc).

-export([rpn/1]).

 

rpn(L) when is_list(L) -> 

    [Res] = lists:foldl(fun rpn/2, [], string:tokens(L, " ")), 

    Res.


Implementaremos rpn/2 teniendo en cuenta que, dado que cada operador y operando de la expresión termina colocándose en la parte superior de la pila, el resultado de la expresión resuelta estará en esa pila. Necesitamos sacar ese último valor de allí antes de devolvérselo al usuario. Es por eso que hacemos una coincidencia de patrones sobre [Res] y solo devolvemos Res.

Bien, ahora vamos a la parte más difícil. Nuestra función rpn/2 deberá manejar la pila para todos los valores que se le pasen. El encabezado de la función probablemente se verá como rpn(Op,Stack) y su valor de retorno como [NewVal|Stack]. Cuando obtenemos números regulares, la operación será:


rpn(X, Stack) -> [read(X)|Stack].


Aquí, read/1 es una función que convierte una cadena en un valor entero o de punto flotante. Lamentablemente, no hay una función incorporada para hacer esto en Erlang (solo una o la otra). La agregaremos nosotros mismos:


read(N) ->

case string:to_float(N) of

{error,no_float} -> list_to_integer(N);

{F,_} -> F

end.


Donde string:to_float/1 realiza la conversión de una cadena como "13.37" a su equivalente numérico. Sin embargo, si no hay forma de leer un valor de punto flotante, devuelve {error,no_float}. Cuando eso sucede, necesitamos llamar a list_to_integer/1 en su lugar.

Ahora volvamos a rpn/2. Todos los números que encontramos se agregan a la pila. Sin embargo, debido a que nuestro patrón coincide con cualquier cosa (consulte Coincidencia de patrones), los operadores también se colocarán en la pila. Para evitar esto, los colocaremos todos en cláusulas anteriores. La primera con la que intentaremos esto es la suma:


rpn("+", [N1,N2|S]) -> [N2+N1|S];

rpn(X, Stack) -> [read(X)|Stack].


Podemos ver que siempre que encontramos la cadena "+", tomamos dos números de la parte superior de la pila (N1,N2) y los sumamos antes de volver a colocar el resultado en esa pila. Esta es exactamente la misma lógica que aplicamos al resolver el problema a mano. Al probar el programa, podemos ver que funciona:


1> c(calc).

{ok,calc}

2> calc:rpn("3 5 +").

8

3> calc:rpn("7 3 + 5 +").

15

El resto es trivial, ya que solo hay que sumar todos los demás operadores:

rpn("+", [N1,N2|S]) -> [N2+N1|S];
rpn("-", [N1,N2|S]) -> [N2-N1|S];
rpn("*", [N1,N2|S]) -> [N2*N1|S];
rpn("/", [N1,N2|S]) -> [N2/N1|S];
rpn("^", [N1,N2|S]) -> [math:pow(N2,N1)|S];
rpn("ln", [N|S]) -> [math:log(N)|S];
rpn("log10", [N|S]) -> [math:log10(N)|S];
rpn(X, Stack) -> [read(X)|Stack].

Para asegurarnos de que todo esto funcione bien, escribiremos pruebas unitarias muy simples. El operador = de Erlang puede actuar como una función de aserción. Las aserciones deberían fallar siempre que encuentren valores inesperados, que es exactamente lo que necesitamos. Por supuesto, existen marcos de prueba más avanzados para Erlang, incluidos Common Test y EUnit. Los revisaremos más adelante, pero por ahora el = básico hará el trabajo:

rpn_test() ->
    5 = rpn("2 3 +"),
    87 = rpn("90 3 -"),
    -4 = rpn("10 4 3 + 2 * -"),
    -2.0 = rpn("10 4 3 + 2 * - 2 /"),
    ok = try
        rpn("90 34 12 33 55 66 + * - +")
    catch
        error:{badmatch,[_|_]} -> ok
    end,
    4037 = rpn("90 34 12 33 55 66 + * - + -"),
    8.0 =  rpn("2 3 ^"),
    true = math:sqrt(2) == rpn("2 0.5 ^"),
    true = math:log(2.7) == rpn("2.7 ln"),
    true = math:log10(2.7) == rpn("2.7 log10"),
    50 = rpn("10 10 10 20 sum"),
    10.0 = rpn("10 10 10 20 sum 5 /"),
    1000.0 = rpn("10 10 20 0.5 prod"),
    ok.

La función de prueba prueba todas las operaciones; si no se genera ninguna excepción, las pruebas se consideran exitosas. Las primeras cuatro pruebas verifican que las funciones aritméticas básicas funcionen correctamente. La quinta prueba especifica un comportamiento que aún no he explicado. La función try ... catch espera que se genere un error de coincidencia incorrecta porque la expresión no puede funcionar:


90 34 12 33 55 66 + * - +

90 (34 (12 (33 (55 66 +) *) -) +)


Al final de rpn/1, los valores -3947 y 90 se dejan en la pila porque no hay ningún operador que trabaje en el 90 que se queda colgado allí. Hay dos formas posibles de manejar este problema: ignorarlo y solo tomar el valor en la parte superior de la pila (que sería el último resultado calculado) o bloquearse porque la aritmética es incorrecta. Dado que la política de Erlang es dejar que se bloquee, es lo que se eligió aquí. La parte que realmente falla es la [Res] en rpn/1. Esta se asegura de que solo quede un elemento, el resultado, en la pila.

Las pocas pruebas que tienen la forma true = FunctionCall1 == FunctionCall2 están ahí porque no se puede tener una llamada de función en el lado izquierdo de =. Sigue funcionando como una aserción porque comparamos el resultado de la comparación con true.

También he añadido los casos de prueba para los operadores sum y prod para que puedan practicar su implementación. Si todas las pruebas son exitosas, deberían ver lo siguiente:

1> c(calc).
{ok,calc}
2> calc:rpn_test().
ok
3> calc:rpn("1 2 ^ 2 2 ^ 3 2 ^ 4 2 ^ sum 2 -").
28.0

Donde 28 es, de hecho, igual a suma(1² + 2² + 3² + 4²) - 2. Pruebe tantas como desee.

Una cosa que se podría hacer para mejorar nuestra calculadora sería asegurarse de que genere errores de badarith cuando se bloquea debido a operadores desconocidos o valores que quedan en la pila, en lugar de nuestro error de coincidencia incorrecta actual. Sin duda, facilitaría la depuración para el usuario del módulo calc.

domingo, 15 de septiembre de 2024

Use de Gleam


import gleam/io

import gleam/result


pub fn main() {

  let _ = io.debug(without_use())

  let _ = io.debug(with_use())

}


pub fn without_use() {

  result.try(get_username(), fn(username) {

    result.try(get_password(), fn(password) {

      result.map(log_in(username, password), fn(greeting) {

        greeting <> ", " <> username

      })

    })

  })

}


pub fn with_use() {

  use username <- result.try(get_username())

  use password <- result.try(get_password())

  use greeting <- result.map(log_in(username, password))

  greeting <> ", " <> username

}


// Here are some pretend functions for this example:


fn get_username() {

  Ok("alice")

}


fn get_password() {

  Ok("hunter2")

}


fn log_in(_username: String, _password: String) {

  Ok("Welcome")

}


El resultado es : 

Ok("Welcome, alice")
Ok("Welcome, alice")

Gleam carece de excepciones, macros, clases de tipos, retornos anticipados y una variedad de otras características, en lugar de eso, se centra en funciones de primera clase y coincidencia de patrones. Esto hace que el código de Gleam sea más fácil de entender, pero a veces puede resultar en una sangría excesiva.

La expresión use de Gleam ayuda en este caso al permitirnos escribir código que utiliza devoluciones de llamadas en un estilo sin sangría, como se muestra en el código anterior. 

La función de orden superior que se llama va del lado derecho del operador <-. Debe tomar una función de devolución de llamada como argumento final.

Los nombres de los argumentos para la función de devolución de llamada van del lado izquierdo del operador <-. La función puede tomar cualquier cantidad de argumentos, incluido cero.

Todo el código restante en el bloque {} que lo encierra se convierte en el cuerpo de la función de devolución de llamada.

Esta es una característica muy útil y capaz, pero la aplicación excesiva de use puede resultar en un código poco claro, especialmente para principiantes. ¡Por lo general, la sintaxis de llamada de función regular da como resultado un código más accesible!

sábado, 14 de septiembre de 2024

Programemos una función que nos indique si una palabra es palíndromo en Erlang


Con el titulo explique todo. Entonces programemos: 

 

-module(palindrome).

-export([is_palindrome/1]).


is_palindrome(Word) ->

    NormalizedWord = string:to_lower(Word), % Convertir a minúsculas para evitar errores por mayúsculas

    NormalizedWord == lists:reverse(NormalizedWord).


Y si lo probamos: 

1> c(palindrome).
{ok,palindrome}
2> palindrome:is_palindrome("radar").
true
3> palindrome:is_palindrome("hello").
false


Como vemos anda muy bien, el tema es que tenemos que dar vuelta la palabra para comparar, lo podemos hacer un poquito más eficiente. Podriamos comparar el primer caracter con el ultimo, el segundo con el anteultimo y así ...

-module(palindrome).
-export([is_palindrome/1]).

is_palindrome(Word) ->
    NormalizedWord = string:to_lower(Word), % Convertir a minúsculas
    check_palindrome(NormalizedWord).

check_palindrome([]) -> true;  % Caso base: una palabra vacía es palíndroma
check_palindrome([_]) -> true; % Caso base: una palabra de un solo carácter es palíndroma
check_palindrome(Word) ->
    case lists:nth(1, Word) == lists:nth(length(Word), Word) of
        true -> check_palindrome(lists:sublist(Word, 2, length(Word)-2));
        false -> false
    end.

Y si lo probamos: 

1> c(palindrome).
{ok,palindrome}
2> palindrome:is_palindrome("radar").
true
3> palindrome:is_palindrome("hello").
false
4> palindrome:is_palindrome("Aibohphobia").
true

Este enfoque es más eficiente en términos de memoria porque no genera una nueva cadena invertida, sino que trabaja directamente comparando los extremos y reduciendo la longitud de la palabra.


¿Qué es ANTLR?



ANTLR es un generador de analizadores. Un analizador toma un fragmento de texto y lo transforma en una estructura organizada, un árbol de análisis, también conocido como árbol de sintaxis abstracta (AST). Puedes pensar en el AST como una historia que describe el contenido del código, o también como su representación lógica, creada al juntar las distintas piezas. 

Qué debes hacer para obtener un AST:

  1. definir una gramática lexer y analizador
  2. invocar ANTLR: generará un lexer y un analizador en su lenguaje de destino (por ejemplo, Java, Python, C#, JavaScript)
  3. use el lexer y el analizador generados: los invoca pasando el código para reconocer y le devuelven un árbol de análisis

Por lo tanto, debe comenzar definiendo una gramática lexer y analizador para lo que está analizando. Normalmente la “cosa” es un lenguaje, pero también podría ser un formato de datos, un diagrama o cualquier tipo de estructura que se represente con texto.

Tenga en cuenta que técnicamente lo que obtiene de ANTLR es un árbol de análisis en lugar de un AST. La diferencia es que un árbol de análisis es exactamente lo que sale del analizador, mientras que AST es una versión más refinada del árbol de análisis. El AST se crea manipulando el árbol de análisis para obtener algo que sea más fácil de usar en las partes posteriores de su programa. Estos cambios a veces son necesarios porque un árbol de análisis puede estar organizado de manera que el análisis sea más fácil o tenga un mejor rendimiento. Sin embargo, es posible que prefieras algo más fácil de usar en el resto del programa.

La distinción es discutible en los ejemplos que se muestran aquí, dado que son bastante simples, por lo que aquí usamos los términos indistintamente. Sin embargo, es algo a tener en cuenta al leer otros documentos.

Dejo link: https://www.antlr.org/