Translate

Mostrando las entradas con la etiqueta Scala. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Scala. Mostrar todas las entradas

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.


viernes, 30 de agosto de 2024

Primary constructors in C# 12 very similar to Scala constructors


With the arrival of C# 12, the language has introduced several new features that make programming more concise and expressive. Among them, Primary Constructors stand out for simplifying the way classes initialize their members. This feature is similar to what Scala has long offered with its primary constructors.

Primary Constructors allow a constructor to be defined directly in the class declaration, which reduces the need for repetitive code and simplifies the definition of immutable classes.


public class Person(string name, int age)

{

    public string Name { get; } = name;

    public int Age { get; } = age;

}


In this example, the `Person` class has a primary constructor that takes two parameters: name and age. And the Name and Age properties are initialized directly from the constructor parameters, making it cleaner and more concise.

Scala has offered a similar concept since its earliest versions. In Scala, the main constructor parameters are defined along with the class and can be used to initialize the class members directly.


class Person(val name: String, val age: Int)


Both C# 12 and Scala eliminate the need to define a separate constructor and assign the parameters to the class properties manually.

But in C#, properties are assigned inside the class body using an explicit assignment (= name;), while in Scala, this assignment is implicit. And in Scala, you can control the visibility of constructor parameters (val or var) more directly. In C#, the default pattern is to create immutable properties with get; only.

Scala, being a more functional programming oriented language, offers features such as eliminating the need for {} braces for simple class bodies, while C# remains more verbose in its syntax.

In conclusion, the addition of Primary Constructors in C# 12 is a step in the right direction, making the language more expressive and less verbose, approaching the simplicity that Scala has offered for years. This parallel not only demonstrates the influence of functional languages on more traditional languages like C#, but also highlights the trend toward more concise, declarative programming.

And with each passing day I see C# becoming more like Scala ...

viernes, 16 de agosto de 2024

Constructores primarios en C# 12 muy parecidos a los constructores de Scala


Con la llegada de C# 12, el lenguaje ha introducido varias características nuevas que hacen que la programación sea más concisa y expresiva. Entre ellas, los Primary Constructors destacan por simplificar la forma en que las clases inicializan sus miembros. Esta característica es similar a lo que Scala ha ofrecido desde hace tiempo con sus constructores primarios.

Los constructores primarios permiten definir un constructor directamente en la declaración de la clase, lo que reduce la necesidad de código repetitivo y simplifica la definición de clases inmutables.


public class Person(string name, int age)

{

    public string Name { get; } = name;

    public int Age { get; } = age;

}


En este ejemplo, la clase `Person` tiene un constructor primario que toma dos parámetros: `name` y `age`. Y las propiedades `Name` y `Age` se inicializan directamente desde los parámetros del constructor, haciéndolo más limpio y conciso.

Scala ha ofrecido un concepto similar desde sus primeras versiones. En Scala, los parámetros del constructor principal se definen junto con la clase y se pueden utilizar para inicializar los miembros de la clase de forma directa.


class Person(val name: String, val age: Int)


Tanto C# 12 como Scala eliminan la necesidad de definir un constructor separado y asignar los parámetros a las propiedades de la clase manualmente.

Pero en C#, las propiedades se asignan dentro del cuerpo de la clase usando una asignación explícita (`= name;`), mientras que en Scala, esta asignación es implícita. Y en Scala, se puede controlar la visibilidad de los parámetros del constructor (`val` o `var`) más directamente. En C#, el patrón predeterminado es crear propiedades inmutables con `get;` solamente.

Scala, siendo un lenguaje más orientado a la programación funcional, ofrece características como la eliminación de la necesidad de llaves `{}` para cuerpos de clase simples, mientras que C# sigue siendo más detallado en su sintaxis.

En conclusión, la incorporación de Primary Constructors en C# 12 es un paso en la dirección correcta, haciendo que el lenguaje sea más expresivo y menos verboso, acercándose a la simplicidad que Scala ha ofrecido durante años. Este paralelismo no solo demuestra la influencia de los lenguajes funcionales en lenguajes más tradicionales como C#, sino que también resalta la tendencia hacia una programación más concisa y declarativa.

Y cada día que pasa veo a C# más parecido a Scala ... 

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, 9 de mayo de 2024

Que es un Higher-Kinded Type de Scala?


En Scala, un Higher-Kinded Type (HKT) es un tipo parametrizado que en sí mismo toma otro tipo parametrizado. Esto permite a los tipos ser abstractos sobre otros tipos parametrizados, lo que proporciona una mayor flexibilidad y abstracción en el diseño de bibliotecas y abstracciones de programación.

Para entender mejor qué es un HKT, es útil revisar algunos conceptos básicos:

Tipo parametrizado: En Scala, un tipo parametrizado es un tipo que toma uno o más parámetros de tipo. Por ejemplo, List[A] es un tipo parametrizado que toma un parámetro de tipo A.

Tipo de orden superior (Higher-Order Type): Un tipo de orden superior es un tipo que acepta otros tipos como parámetros. Por ejemplo, en el contexto de las funciones, una función de orden superior es una función que toma otra función como argumento.

Higher-Kinded Type (HKT): Un HKT es un tipo parametrizado que en sí mismo toma otro tipo parametrizado. En Scala, se denota utilizando el operador [_] o [*]. Por ejemplo, Option[_] o F[_] son HKTs, ya que pueden tomar tipos parametrizados como Option[Int] o Option[String].

Los HKTs son útiles en el contexto de la programación funcional y el diseño de bibliotecas genéricas. Permiten escribir código genérico que puede trabajar con diferentes tipos de datos sin conocer los detalles específicos de esos tipos. Por ejemplo, muchas bibliotecas de efectos en Scala, como Cats o Scalaz, utilizan HKTs para proporcionar abstracciones sobre diferentes tipos de efectos o contenedores de datos. Esto permite a los desarrolladores escribir código genérico que puede manipular efectos de diferentes tipos sin necesidad de modificar el código para cada tipo específico.

Vamos a crear una abstracción genérica para trabajar con contenedores de datos, independientemente de su tipo específico:


// Definición de un Higher-Kinded Type (HKT) F[_]

trait Container[F[_]] {

  def put[A](value: A): F[A]

  def get[A](container: F[A]): A

}


// Implementación de Container para List

object ListContainer extends Container[List] {

  def put[A](value: A): List[A] = List(value)

  def get[A](container: List[A]): A = container.head

}


// Implementación de Container para Option

object OptionContainer extends Container[Option] {

  def put[A](value: A): Option[A] = Some(value)

  def get[A](container: Option[A]): A = container.getOrElse(throw new NoSuchElementException("Empty container"))

}


object Main {

  def main(args: Array[String]): Unit = {

    // Uso de ListContainer

    val list = ListContainer.put(42)

    println("Value in list: " + ListContainer.get(list)) // Imprime: Value in list: 42

    

    // Uso de OptionContainer

    val option = OptionContainer.put(42)

    println("Value in option: " + OptionContainer.get(option)) // Imprime: Value in option: 42

  }

}

En este ejemplo, Container[F[_]] es un HKT que representa un contenedor genérico. La interfaz Container define métodos put y get que permiten poner y obtener valores de un contenedor F de tipo F[_]. Luego, proporcionamos implementaciones específicas de Container para diferentes tipos de contenedores: List y Option.

Esta abstracción nos permite escribir código genérico que funciona con cualquier tipo de contenedor, sin necesidad de conocer los detalles internos de cada uno. Por ejemplo, podemos utilizar ListContainer para trabajar con listas y OptionContainer para trabajar con opciones, todo usando la misma interfaz genérica Container. Esto proporciona una gran flexibilidad y reutilización de código en nuestras aplicaciones.

En Haskell, el concepto de Higher-Kinded Type (HKT) se manifiesta a través de los tipos de datos parametrizados que también son constructores de tipos. Esto permite definir abstracciones genéricas que pueden trabajar con diferentes tipos de datos sin conocer los detalles específicos de esos tipos.

Supongamos que queremos definir una abstracción genérica para trabajar con estructuras de datos que actúan como contenedores. Podemos utilizar un tipo de dato parametrizado f, donde f es un tipo de constructor de tipo, para representar nuestro contenedor genérico. Luego, definimos funciones genéricas que operan en este contenedor:


haskell

Copy code

-- Definición de un tipo de dato parametrizado f

class Container f where

  put :: a -> f a

  get :: f a -> a


-- Implementación de Container para List

instance Container [] where

  put x = [x]

  get (x:_) = x

  get _     = error "Empty list"


-- Implementación de Container para Maybe

instance Container Maybe where

  put = Just

  get (Just x) = x

  get Nothing  = error "Nothing"


-- Ejemplo de uso

main :: IO ()

main = do

  let list = put 42 :: [Int]

  putStrLn $ "Value in list: " ++ show (get list) -- Imprime: Value in list: 42

  

  let maybeVal = put 42 :: Maybe Int

  putStrLn $ "Value in Maybe: " ++ show (get maybeVal) -- Imprime: Value in Maybe: 42

En este ejemplo, Container es una clase de tipo que representa nuestra abstracción genérica para contenedores. La función put toma un valor y lo coloca en el contenedor, mientras que get extrae un valor del contenedor. Luego, proporcionamos instancias de Container para tipos específicos de contenedores como [] (lista) y Maybe.

Usando esta abstracción, podemos escribir código genérico que funciona con cualquier tipo de contenedor sin preocuparnos por los detalles internos de cada uno. Esto proporciona una gran flexibilidad y reutilización de código en nuestras aplicaciones Haskell.

miércoles, 8 de mayo de 2024

Que es la Covarianza, Contravarianza y Invarianza?


Ya sé que estos son temas super basicos pero siempre que hablan de eso me los confundo. Por eso voy a hacer este post para ver si porfin puedo fijar las ideas. 

La varianza y la covarianza son conceptos importantes en el contexto de los tipos genérico. Estos conceptos se refieren a cómo se relacionan los tipos genéricos cuando se consideran subtipos y super tipos. Veamos una explicación de cada uno:

Covarianza: La covarianza se refiere a la relación entre tipos genéricos donde la relación de subtipos se mantiene en la misma dirección que la relación de subtipos de los parámetros de tipo. En otras palabras, si tenemos un tipo T y otro tipo U donde U es un subtipo de T, entonces podemos decir que List<U> es un subtipo de List<T>. Esto significa que podemos asignar una lista de subtipos a una lista de supertipos sin necesidad de conversión explícita. La covarianza se utiliza típicamente en situaciones donde solo se lee de una estructura de datos, como en secuencias o enumeraciones.

Veamos un ejemplo en C#: 

using System;

using System.Collections.Generic;


class Program

{

    static void Main()

    {

        // Covarianza en IEnumerable<T>

        IEnumerable<string> strings = new List<string> { "hello", "world" };

        PrintItems(strings);

    }


    static void PrintItems(IEnumerable<object> items)

    {

        foreach (var item in items)

        {

            Console.WriteLine(item);

        }

    }

}

Veamos un ejemplo en Java: 

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // Covarianza en List<? extends Number>
        List<Integer> integers = new ArrayList<>();
        integers.add(1);
        integers.add(2);
        printNumbers(integers);
    }

    static void printNumbers(List<? extends Number> numbers) {
        for (Number number : numbers) {
            System.out.println(number);
        }
    }
}

Y en scala: 

class Main {
  def main(args: Array[String]): Unit = {
    // Covarianza en List[+A]
    val strings: List[String] = List("hello", "world")
    printItems(strings)
  }

  def printItems(items: List[Any]): Unit = {
    items.foreach(println)
  }
}

Contravarianza: La contravarianza es el opuesto de la covarianza. Se refiere a la relación entre tipos genéricos donde la relación de subtipos se invierte en comparación con la relación de subtipos de los parámetros de tipo. En otras palabras, si tenemos un tipo T y otro tipo U donde U es un subtipo de T, entonces podemos decir que Action<T> es un subtipo de Action<U>. Esto significa que podemos asignar una función que acepta supertipos a una variable de función que acepta subtipos. La contravarianza se utiliza típicamente en situaciones donde solo se escribe en una estructura de datos, como en consumidores de datos o comparadores.

Veamos un ejemplo en Java: 


import java.util.function.Consumer;


public class Main {

    public static void main(String[] args) {

        // Contravarianza en Consumer<? super String>

        Consumer<Object> printString = Main::printString;

        printString.accept("hello");

    }


    static void printString(Object s) {

        System.out.println(s);

    }

}


Y en Scala: 

class Main {
  def main(args: Array[String]): Unit = {
    // Contravarianza en Function1[-T, +R]
    val printString: Any => Unit = Main.printString
    printString("hello")
  }
}

object Main {
  def printString(s: String): Unit = {
    println(s)
  }
}

Invarianza: La invarianza es el tercer escenario, donde no hay una relación de subtipos entre tipos genéricos. En otras palabras, List<T> no es ni un subtipo ni un supertipo de List<U> si T y U son tipos diferentes, incluso si U es un subtipo de T o viceversa. En este caso, necesitamos una conversión explícita para asignar entre los dos tipos.

En C#, la covarianza y la contravarianza se expresan a través de modificadores como out y in en declaraciones de tipo genérico. En Java, estos conceptos se implementan a través de la notación <? extends T> para la covarianza y <? super T> para la contravarianza en tipos genéricos. Es importante entender estos conceptos para escribir código genérico seguro y comprensible.

No recuerdo haber utilizado la contravarianza pero no se me hace algo tan común. Y ustedes, han utilizado estas técnicas? 


martes, 26 de septiembre de 2023

Explorando la Programación Funcional en Scala


Los quiero invitar a una charla que voy a dar para scaLatin, sobre programación funcional. 

Exploraremos el mundo de la programación funcional en Scala a través de escenarios concretos de resolución de problemas. Nos sumergiremos en la elegancia de las funciones lambda y aprenderemos cómo crearlas y aplicarlas para optimizar nuestro código.

Luego aplicaremos pattern matching y descubriremos cómo simplifica el manejo de datos complejos y mejora la legibilidad del código.

A lo largo de nuestro viaje, aprovecharemos el poder de la recursividad para resolver problemas de manera eficiente y demostraremos cómo las mónadas pueden ayudarnos a controlar la complejidad de nuestros programas.

Ya sea que sea nuevo en estos conceptos o busque conocimientos avanzados, esta sesión promete equiparlo con habilidades prácticas y una comprensión más profunda de la programación funcional en Scala.

Dejo links: 

https://www.linkedin.com/events/7112416236554137600/

https://www.meetup.com/scalatin/events/296351847/

martes, 18 de julio de 2023

Una lista clasificada de increíbles bibliotecas y herramientas de open source de Scala.


Encontre una lista de 380 proyectos open source de scala con un total de 370 000 estrellas agrupadas en 23 categorías. Todos los proyectos se clasifican según un puntaje de calidad del proyecto, que se calcula en función de varias métricas recopiladas automáticamente de GitHub y diferentes administradores de paquetes. 

Dejo link: https://github.com/stkeky/best-of-scala

jueves, 8 de junio de 2023

Primeros pasos con ZIO parte 4


ZIO puede convertir cualquier código (como una llamada a algún método) en un efecto, ya sea que ese código se llame síncrono (que devuelve directamente un valor) o asíncrono (que pasa un valor a las devoluciones de llamada).

Si se hace correctamente, cuando convierte el código en un efecto ZIO, este código se almacenará dentro del efecto para que ZIO pueda administrarlo y beneficiarse de funciones como reintentos, tiempos de espera y registro automático de errores.

Las funciones de conversión que tiene ZIO le permiten utilizar sin problemas todas las funciones de ZIO con código que no es de ZIO escrito en Scala o Java, incluidas las bibliotecas de terceros.

El código síncrono se puede convertir en un efecto ZIO usando ZIO.attempt:


import scala.io.StdIn

val readLine: ZIO[Any, Throwable, String] = ZIO.attempt(StdIn.readLine())


El tipo de error del efecto resultante siempre será Throwable, porque el código síncrono puede generar excepciones con cualquier valor de tipo Throwable.

Si sabe con certeza que algún código no arroja excepciones (excepto quizás excepciones en tiempo de ejecución), puede convertir el código en un efecto ZIO usando ZIO.succeed:


def printLine(line: String): UIO[Unit] =  ZIO.succeed(println(line))


A veces, es posible que sepa que el código arroja un tipo de excepción específico, y es posible que desee reflejar esto en el parámetro de error de su efecto ZIO.

Para ello, puede utilizar el método ZIO#refineToOrDie:


import java.io.IOException


val readLine2: ZIO[Any, IOException, String] =

  ZIO.attempt(StdIn.readLine()).refineToOrDie[IOException]


El código asíncrono que expone una API basada en devolución de llamada se puede convertir en un efecto ZIO usando ZIO.async:

object legacy {

  def login(

    onSuccess: User => Unit,

    onFailure: AuthError => Unit): Unit = ???

}


val login: ZIO[Any, AuthError, User] =

  ZIO.async[Any, AuthError, User] { callback =>

    legacy.login(

      user => callback(ZIO.succeed(user)),

      err  => callback(ZIO.fail(err))

    )

  }


Los efectos asincrónicos son mucho más fáciles de usar que las API basadas en devolución de llamadas y se benefician de las características de ZIO como la interrupción, la seguridad de los recursos y la gestión de errores.

Algunos códigos sincrónicos pueden participar en el llamado bloqueo de E/S, que pone un subproceso en un estado de espera, mientras espera que se complete alguna llamada del sistema operativo. Para obtener el máximo rendimiento, este código no debe ejecutarse en el grupo de subprocesos principal de su aplicación, sino en un grupo de subprocesos especial dedicado a operaciones de bloqueo.

ZIO tiene un grupo de subprocesos de bloqueo integrado en el tiempo de ejecución y le permite ejecutar efectos allí con ZIO.blocking:


import scala.io.{ Codec, Source }


def download(url: String) =

  ZIO.attempt {

    Source.fromURL(url)(Codec.UTF8).mkString

  }


def safeDownload(url: String) =

  ZIO.blocking(download(url))


Como alternativa, si desea convertir el código de bloqueo directamente en un efecto ZIO, puede usar el método ZIO.attemptBlocking:


val sleeping =

  ZIO.attemptBlocking(Thread.sleep(Long.MaxValue))


El efecto resultante se ejecutará en el grupo de subprocesos de bloqueo de ZIO.

Si tiene algún código síncrono que responderá a Thread.interrupt de Java (como Thread.sleep o código basado en bloqueo), entonces puede convertir este código en un efecto ZIO interrumpible usando el método ZIO.attemptBlockingInterrupt.

Algunos códigos síncronos solo se pueden cancelar invocando algún otro código, que es responsable de cancelar el cálculo en ejecución. Para convertir dicho código en un efecto ZIO, puede usar el método ZIO.attemptBlockingCanceble:


import java.net.ServerSocket

import zio.UIO


def accept(l: ServerSocket) =

  ZIO.attemptBlockingCancelable(l.accept())(ZIO.succeed(l.close()))


lunes, 5 de junio de 2023

Pekko: El fork open source de Akka que impulsa la escalabilidad en sistemas concurrentes


En el mundo de la programación concurrente y distribuida, Akka ha sido una herramienta ampliamente reconocida y utilizada. Sin embargo, recientemente hubo un cambio de licencia que no cayo nada bien pero por suerte aparecio Pekko, un fork open source de Akka que busca llevar la escalabilidad de sistemas concurrentes a un nuevo nivel. 

¿Qué es Pekko? Pekko es un fork open source de Akka, un framework de programación concurrente y distribuida basado en el modelo de actores. Al igual que Akka, Pekko proporciona una forma elegante y eficiente de construir sistemas altamente concurrentes y escalables. 

Arquitectura de bajo acoplamiento: Pekko ha sido diseñado con una arquitectura de bajo acoplamiento que permite un mejor aislamiento y desacoplamiento entre los actores. Esto se traduce en una mayor flexibilidad y capacidad de escalar los sistemas de forma eficiente.

Rendimiento mejorado: Pekko ha optimizado el rendimiento en comparación con Akka, especialmente en escenarios de alto tráfico y carga intensiva. Gracias a su implementación eficiente y algoritmos optimizados, Pekko es capaz de manejar un mayor número de mensajes por segundo y reducir la latencia en las comunicaciones.

Escalabilidad elástica: Pekko ha introducido un enfoque innovador para la escalabilidad elástica, permitiendo que los sistemas se adapten automáticamente a las demandas cambiantes de carga. Esto se logra mediante la capacidad de ajustar dinámicamente la cantidad de recursos asignados a los actores en función de la demanda, lo que garantiza un rendimiento óptimo en todo momento.

Mejoras en la tolerancia a fallos: Pekko ha fortalecido los mecanismos de tolerancia a fallos, lo que garantiza una mayor robustez en entornos distribuidos. Mediante la implementación de estrategias de recuperación y supervisión mejoradas, Pekko ayuda a gestionar los errores y a mantener la estabilidad del sistema, incluso en situaciones adversas.

Facilidad de migración: Pekko ha sido diseñado para facilitar la migración desde Akka. Los desarrolladores pueden aprovechar su experiencia previa con Akka y adaptarse rápidamente a Pekko sin mayores obstáculos.

Comunidad activa: Pekko cuenta con una comunidad open source vibrante y activa. Esto implica que los desarrolladores pueden colaborar, compartir conocimientos y contribuir a la mejora continua del proyecto.

Documentación exhaustiva: Pekko ofrece una documentación completa y bien estructurada, que incluye guías, tutoriales y ejemplos de código. Esto facilita la adopción y el aprendizaje de la herramienta.

Por ahora Pekko esta en incubador de apache pero creo que tiene futuro. 

Pekko representa un emocionante avance en el mundo de la programación concurrente y distribuida. Con su enfoque en la escalabilidad, rendimiento y facilidad de uso, este fork open source de Akka ofrece una alternativa sólida y poderosa para desarrolladores y arquitectos de sistemas. Si estás interesado en construir sistemas concurrentes altamente escalables, Pekko definitivamente merece tu atención.

Dejo link : https://pekko.apache.org/

Mi experiencia con la programación funcional en Scala


Creo que si leen este blog saben que me encanta la programación funcional y investigar y aprender. Y en eso estoy...


Me gustaría contar un poco mi evolución y mis altibajos para que la gente que sabe me aconseje y los que no saben puedan aprender de mis exitos y derrotas. 


Bueno, todo empezó con un curso de scala de coursera y quede encantado con el lenguaje y la programación funcional. De a poco tambien me interiorice en otros lenguajes como erlang, haskell, Clojure, Elixir, F#, etc ... pero siempre en la volvía a Scala por gusto nomas. Y me iba bastante bien...


Hasta que empece a quedarme corto en scala, quería progresar y decidí ver algunos framework funcionales, leí un libro de Cats y la verdad es que si bien esta bueno, me frustre mucho... porque no sabia porque se hacían ciertas cosas, no entendía porque tan complejo o si bien entendía desconfiaba ( no hay otra forma de hacerlo más fácil) y bueno ... 


Cambie a Akka, y me pareció bueno y me sentí más en mi mundo, pero justo justo cuando volvia el entuciasmo, cambio de licencia y bueno... Se fue el entusiasmo, ahora sé que existe un fork open source llamado pekko (https://github.com/apache/incubator-pekko) y es de apache. Pero lo veo verde, y no sé cuanto va a crecer ...


Y ahora estoy con ZIO y por ahora todo va bien, por ende la conclusión es si quieren progresar en su conocimiento en Scala, y no vienen del mundo funcional como Haskell, ZIO esta bueno. 

Si son nuevos en el mundo funcional y scala, yo haría lo siguiente: 

1. Estudiar Scala (curso de scala de coursera, el libro rojo de programación funcional en scala, etc ...)

2. Leer y estudiar ZIO 

3. Leer y estudiar Cats


Pero no perder de vista Pekko, para ver como progresa. 


Que opinan? 


domingo, 4 de junio de 2023

Primeros pasos con ZIO parte 3


Vamos a crear efectos funcionales de ZIO, y podemos hacerlo a partir de valores, cálculos y tipos de datos comunes de Scala.

Usando el método ZIO.succeed, se puede crear un efecto que, cuando se ejecute, tendrá éxito con el valor especificado:

val s1 = ZIO.succeed(42)

El método de succeed toma un parámetro, que garantiza que si le pasa al método algún código para ejecutar, este código se almacenará dentro del efecto ZIO para que ZIO pueda administrarlo y beneficiarse de características como reintentos, tiempos de espera y registro automático de errores.

Usando el método ZIO.fail, puede crear un efecto que, cuando se ejecuta, fallará con el valor especificado:

val f1 = ZIO.fail("Uh oh!")

Para el tipo de datos ZIO, no hay restricción en el tipo de error. Se puede usar cadenas, excepciones o tipos de datos personalizados.

Podemos usar excepciones :

val f2 = ZIO.fail(new Exception("Uh oh!"))


La biblioteca estándar de Scala contiene varios tipos de datos que se pueden convertir en efectos ZIO.

Un Option se puede convertir en un efecto ZIO usando ZIO.fromOption:


val zoption: IO[Option[Nothing], Int] = ZIO.fromOption(Some(2))


Option[Nothing], lo que significa que si dicho efecto falla, fallará con el valor Nothing (que tiene el tipo Opción[Nothing]).

Puede transformar una falla en algún otro valor de error usando orElseFail, uno de los muchos métodos que proporciona ZIO para la gestión de errores:


val zoption2: ZIO[Any, String, Int] = zoption.orElseFail("It wasn't there!")


ZIO tiene una variedad de otros operadores diseñados para facilitar el manejo de Option. En el siguiente ejemplo, los operadores Some y asSomeError se utilizan para facilitar la interfaz con los métodos que devuelven Option, similar al tipo OptionT en algunas bibliotecas de Scala.


val maybeId: ZIO[Any, Option[Nothing], String] = ZIO.fromOption(Some("abc123"))

def getUser(userId: String): ZIO[Any, Throwable, Option[User]] = ???

def getTeam(teamId: String): ZIO[Any, Throwable, Team] = ???


val result: ZIO[Any, Throwable, Option[(User, Team)]] = (for {

  id   <- maybeId

  user <- getUser(id).some

  team <- getTeam(user.teamId).asSomeError 

} yield (user, team)).unsome 


Un Some se puede convertirse en un efecto ZIO usando ZIO.fromEither:


val zeither: ZIO[Any, Nothing, String] = ZIO.fromEither(Right("Success!"))


El tipo de error del efecto resultante será el del caso Izquierdo (Left), mientras que el tipo de éxito será el del caso Derecho (Right).

Un valor Try se puede convertir en un efecto ZIO usando ZIO.fromTry:


import scala.util.Try


val ztry = ZIO.fromTry(Try(42 / 0))


El tipo de error del efecto resultante siempre será Throwable porque Try solo puede fallar con valores de tipo Throwable.


Un Scala Future se puede convertir en un efecto ZIO usando ZIO.fromFuture:


import scala.concurrent.Future


lazy val future = Future.successful("Hello!")

val zfuture: ZIO[Any, Throwable, String] =  ZIO.fromFuture { implicit ec =>

    future.map(_ => "Goodbye!")

  }


La función pasada a fromFuture recibe un ExecutionContext, que permite a ZIO administrar dónde se ejecuta Future (por supuesto, puede ignorar este ExecutionContext).

El tipo de error del efecto resultante siempre será Throwable, porque los valores Future solo pueden fallar con valores de tipo Throwable.






miércoles, 31 de mayo de 2023

Primeros pasos con ZIO parte 2


ZIO es framework para crear aplicaciones nativas en la nube. Con un núcleo funcional amigable para principiantes pero poderoso, ZIO permite a los desarrolladores crear rápidamente aplicaciones con las mejores prácticas que son altamente escalables, comprobables, robustas, resistentes, seguras para los recursos, eficientes y observables.

En el corazón de ZIO se encuentra un poderoso tipo de datos llamado ZIO, que es el bloque de construcción fundamental para cada aplicación ZIO.

El tipo de datos ZIO se denomina efecto funcional y representa una unidad de cálculo dentro de una aplicación ZIO. Al igual que un modelo o un flujo de trabajo, los efectos funcionales son planes precisos que describen un cálculo o una interacción. Cuando se ejecuta una aplicación ZIO, un efecto funcional fallará con algún tipo de error o tendrá éxito con algún tipo de valor.

Al igual que el tipo de datos List, el tipo de datos ZIO es un tipo de datos genérico y usa parámetros de tipo para mejorar la seguridad de tipos. El tipo de datos de Lista tiene un solo parámetro de tipo, que representa el tipo de elemento que se almacena en la Lista. El tipo de datos ZIO tiene tres parámetros de tipo: ZIO[R, E, A].

Los parámetros de tipo del tipo de datos ZIO tienen los siguientes significados:

  • R - Tipo de entorno. El parámetro de tipo de entorno representa el tipo de datos contextuales que requiere el efecto antes de que se pueda ejecutar. Por ejemplo, algunos efectos pueden requerir una conexión a una base de datos, mientras que otros pueden requerir una solicitud HTTP y otros pueden requerir una sesión de usuario. Si el parámetro de tipo de entorno es Any, entonces el efecto no tiene requisitos, lo que significa que el efecto se puede ejecutar sin proporcionarle primero un contexto específico.
  • E - Tipo de falla. El parámetro tipo de falla representa el tipo de error con el que el efecto puede fallar cuando se ejecuta. Aunque Exception o Throwable son tipos de falla comunes en las aplicaciones ZIO, ZIO no impone ningún requisito sobre el tipo de error y, a veces, es útil definir tipos de error comerciales o de dominio personalizados para diferentes partes de una aplicación. Si el parámetro de tipo de error es Nothing, significa que el efecto no puede fallar.
  • A - Tipo de éxito. El parámetro de tipo de éxito representa el tipo de éxito con el que el efecto puede tener éxito cuando se ejecuta. Si el parámetro de tipo de éxito es Unit, significa que el efecto no produce información útil (similar a un método de devolución de vacío), mientras que si es Nothing, significa que el efecto se ejecuta para siempre, a menos que falle.

Como varios ejemplos de cómo interpretar los tipos de efectos ZIO:

  • Un efecto de tipo ZIO[Any, IOException, Byte] no tiene requisitos y, cuando se ejecuta, dicho efecto puede fallar con un valor de tipo IOException o puede tener éxito con un valor de tipo Byte.
  • Un efecto de tipo ZIO[Connection, SQLException, ResultSet] requiere una conexión y, cuando se ejecuta, dicho efecto puede fallar con un valor de tipo SQLException o puede tener éxito con un valor de tipo ResultSet.
  • Un efecto de tipo ZIO[HttpRequest, HttpFailure, HttpSuccess] requiere una HttpRequest y, cuando se ejecuta, dicho efecto puede fallar con un valor de tipo HttpFailure o puede tener éxito con un valor de tipo HttpSuccess.

El parámetro de tipo de entorno es un parámetro de tipo compuesto porque, a veces, un solo efecto puede requerir varios valores de diferentes tipos. Si ve que un efecto tiene un tipo de ZIO[UserSession with HttpRequest, E, A] (Scala 2.x) o ZIO[UserSession & HttpRequest, E, A] (Scala 3.x), significa que el efecto requiere múltiples valores contextuales antes de que pueda ejecutarse.

Aunque esta analogía no es precisa, se puede pensar en un efecto ZIO como una función:


R => Either[E, A]


Esta función requiere una R y produce una falla de tipo E o un valor de éxito de tipo A.

Los efectos ZIO no son en realidad funciones, por supuesto, porque modelan cálculos e interacciones complejos, que pueden ser asincrónicos, concurrentes o ingeniosos.

El tipo de datos ZIO es el único tipo de efecto en ZIO. Sin embargo, hay una familia de alias de tipo que reducen la necesidad de escribir:

  • UIO[A]: un alias de tipo para ZIO[Any, Nothing, A], que representa un efecto que no tiene requisitos, no puede fallar y puede tener éxito con una A.
  • URIO[R, A]: un alias de tipo para ZIO[R, Nothing, A], que representa un efecto que requiere una R, no puede fallar y puede tener éxito con una A.
  • Task[A]: un alias de tipo para ZIO[Any, Throwable, A], que representa un efecto que no tiene requisitos, puede fallar con un valor Throwable o tener éxito con una A.
  • RIO[R, A]: un alias de tipo para ZIO[R, Throwable, A], que representa un efecto que requiere una R, puede fallar con un valor Throwable o tener éxito con una A.
  • IO[E, A]: un alias de tipo para ZIO[Any, E, A], que representa un efecto que no tiene requisitos, puede fallar con una E o tener éxito con una A.

Si es nuevo en los efectos funcionales, le recomendamos que comience con el tipo de tarea, que tiene un solo parámetro de tipo y se corresponde más con los tipos de datos futuros integrados en las bibliotecas estándar de Scala y Java.

Si está utilizando bibliotecas Cats Effect, puede encontrar útil el tipo RIO, ya que le permite enhebrar el contexto a través de bibliotecas de terceros.

Independientemente del tipo de alias que utilice en su aplicación, UIO puede ser útil para describir efectos infalibles, incluidos los que resultan del manejo de todos los errores.

Finalmente, si es un programador funcional experimentado, se recomienda el uso directo del tipo de datos ZIO, aunque puede resultarle útil crear su propia familia de alias de tipo en diferentes partes de su aplicación.

Si se siente cómodo con el tipo de datos ZIO y su familia de alias de tipo, el siguiente paso es aprender a crear efectos ...

lunes, 29 de mayo de 2023

Primeros pasos con ZIO


Empecemos desde el principio, incluyamos ZIO en nuestro proyecto agregando la siguiente dependencia en el build.sbt:


libraryDependencies += "dev.zio" %% "zio" % "2.0.13"


Para hacer una aplicación ZIO podemos hacer que nuestra aplicación extienda de ZIOAppDefault, que permite escribir todo el programa usando ZIO:


import zio._

import zio.Console._


object MyApp extends ZIOAppDefault {


  def run = myAppLogic


  val myAppLogic =

    for {

      _    <- printLine("Hello! What is your name?")

      name <- readLine

      _    <- printLine(s"Hello, ${name}, welcome to ZIO!")

    } yield ()

}


El método run debemos devolver un valor ZIO que tiene todos sus errores manejados, que, en la jerga de ZIO, es un valor ZIO no excepcional.

Una forma de hacer esto es invocar un pliegue sobre un valor ZIO, para obtener otro valor ZIO no excepcional. Eso requiere dos funciones de controlador: de E => B (el controlador de errores) y de A => B (el controlador de éxito). Si myAppLogic falla, habrá un 1; si tiene éxito, habrá un 0.

Si la aplicación que queremos hacer es una aplicación existente, utilizando inyección de dependencia o no controla su función principal, entonces podemos crear un sistema para ejecutar sus programas ZIO:

import zio._


object IntegrationExample {

  val runtime = Runtime.default


  Unsafe.unsafe { implicit unsafe =>

    runtime.unsafe.run(ZIO.attempt(println("Hello World!"))).getOrThrowFiberFailure()

  }

}

Idealmente, la aplicación debería tener un solo runtime, porque cada runtime tiene sus propios recursos (incluido el grupo de subprocesos y el informador de errores no controlados).

ZIO proporciona un módulo para interactuar con la consola. Si necesita imprimir texto en la consola, puede usar print e printLine:

import zio._


// Print without trailing line break

Console.print("Hello World")

// Print string and include trailing line break

Console.printLine("Hello World")


Si necesita leer la entrada desde la consola, puede usar readLine:

import zio._

val echo = Console.readLine.flatMap(line => Console.printLine(line))



lunes, 13 de marzo de 2023

Vamos a implementar un framework de procesamiento paralelo simple con Cats parte 2

Dado que en el post anterios refrescamos nuestra memoria de Futures, veamos cómo podemos dividir el trabajo en lotes. Podemos consultar la cantidad de CPU disponibles en nuestra máquina mediante una llamada API desde la biblioteca estándar de Java:

Runtime.getRuntime.availableProcessors

// res11: Int = 2

Podemos particionar una secuencia (en realidad cualquier cosa que implemente Vector) usando el método agrupado. Usaremos esto para dividir lotes de trabajo para cada CPU:


(1 to 10).toList.grouped(3).toList

// res12: List[List[Int]] = List(

//List(1, 2, 3),

//List(4, 5, 6),

//List(7, 8, 9),

//List(10)

// )


Implemente una versión paralela de foldMap llamada parallelFoldMap:


def parallelFoldMap[A, B: Monoid] (values: Vector[A]) (func: A => B): Future[B] = {

    // Calculate the number of items to pass to each CPU:

    val numCores = Runtime.getRuntime.availableProcessors

    val groupSize = (1.0 * values.size / numCores).ceil.toInt

    // Create one group for each CPU:

    val groups: Iterator[Vector[A]] = values.grouped(groupSize)

    // Create a future to foldMap each group:

    val futures: Iterator[Future[B]] =

        groups map { group =>

            Future {

                group.foldLeft(Monoid[B].empty)(_ |+| func(_))

            }

    }


    // foldMap over the groups to calculate a final result:

    Future.sequence(futures) map { iterable =>

    iterable.foldLeft(Monoid[B].empty)(_ |+| _)

    }

}

val result: Future[Int] = parallelFoldMap((1 to 1000000).toVector)(identity)

Await.result(result, 1.second)

// res14: Int = 1784293664


Aunque implementamos foldMap nosotros mismos arriba, el método también está disponible como parte de la clase de tipo Foldable:


import cats.Monoid

import cats.instances.int._

// for Monoid

import cats.instances.future._ // for Applicative and Monad

import cats.instances.vector._ // for Foldable and Traverse

import cats.syntax.foldable._// for combineAll and foldMap

import cats.syntax.traverse._// for traverse

import scala.concurrent._

import scala.concurrent.duration._

import scala.concurrent.ExecutionContext.Implicits.global


def parallelFoldMap[A, B: Monoid](values: Vector[A]) (func: A => B): Future[B] = {

    val numCores = Runtime.getRuntime.availableProcessors

    val groupSize = (1.0 * values.size / numCores).ceil.toInt

    values.grouped(groupSize).toVector.traverse(group => Future(group.toVector.foldMap(func)))

        .map(_.combineAll)

}

val future: Future[Int] = parallelFoldMap((1 to 1000).toVector)(_ * 1000)

Await.result(future, 1.second)

// res18: Int = 500500000

Vamos a implementar un framework de procesamiento paralelo simple con Cats

Vamos a implementar un framework de procesamiento paralelo simple pero poderoso utilizando Monoids, Functors y una serie de otras cosas.

Si ha utilizado Hadoop o Spark o ha trabajado en "grandes datos", habrá oído hablar de MapReduce, que es un modelo de programación para realizar el procesamiento de datos en paralelo en grupos de máquinas (también conocidos como "nodos"). Como sugiere el nombre, el modelo se construye alrededor de una fase de map, que es la misma función de map que conocemos de Scala y la clase de tipo Functor, y una fase de reducción, que generalmente llamamos plegar o fold en Scala.

Recuerde que la firma general para el mapa es aplicar una función A => B a una F[A], devolviendo una F[B]

map transforma cada elemento individual en una secuencia de forma independiente. Podemos paralelizar fácilmente porque no hay dependencias entre las transformaciones aplicadas a diferentes elementos (la firma de tipo de la función A => B).

Nuestro paso de reducción se convierte en un pliegue a la izquierda sobre los resultados del map distribuido.

Al distribuir el paso de reducción, perdemos el control sobre el orden de recorrido. Es posible que nuestra reducción general no sea completamente de izquierda a derecha: podemos reducir de izquierda a derecha en varias subsecuencias y luego combinar los resultados. Para asegurar la corrección necesitamos una operación de reducción que sea asociativa:

reduce(a1, reduce(a2, a3)) == reduce(reduce(a1, a2), a3)

Si tenemos asociatividad, podemos distribuir arbitrariamente el trabajo entre nuestros nodos siempre que las subsecuencias en cada nodo permanezcan en el mismo orden que el conjunto de datos inicial.

Nuestra operación de plegado nos obliga a sembrar el cálculo con un elemento de tipo B. Dado que el plegado se puede dividir en un número arbitrario de pasos paralelos, la semilla no debería afectar el resultado del cálculo. Esto naturalmente requiere que la semilla sea un elemento de identidad:

reduce(seed, a1) == reduce(a1, seed) == a1

En resumen, nuestro pliegue paralelo dará los resultados correctos si:

• requerimos que la función reductora sea asociativa;

• Sembramos el cálculo con la identidad de esta función.

¿Cómo suena este patrón? Así es, hemos completado el círculo de regreso a Monoid. El patrón de diseño monoide para trabajos de reducción de mapas está en el centro de los sistemas de big data recientes, como Summingbird de Twitter.

En este proyecto vamos a implementar un map-reduce de una sola máquina muy simple. Comenzaremos implementando un método llamado foldMap para modelar el flujo de datos que necesitamos.

Vimos foldMap es una de las operaciones derivadas que se encuentra encima de foldLeft y foldRight. Sin embargo, en lugar de usar Foldable, volveremos a implementar foldMap aquí nosotros mismos, ya que proporcionará información útil sobre la estructura de map‐reduce. Comience escribiendo la firma de foldMap. Debe aceptar los siguientes parámetros:

• una secuencia de tipo Vector[A];

• una función de tipo A => B, donde hay un Monoide para B;

Deberá agregar parámetros implícitos o límites de contexto para completar la firma de tipo.

import cats.Monoid

/** Single-threaded map-reduce function.

* Maps `func` over `values` and reduces using a `Monoid[B]`.

*/

def foldMap[A, B: Monoid](values: Vector[A])(func: A => B): B =

???

Ahora implemente el cuerpo de foldMap:

  1. comenzar con una secuencia de elementos de tipo A;
  2. mapear sobre la lista para producir una secuencia de elementos de tipo B;
  3. usa el Monoide para reducir los elementos a una sola B.

Aquí hay algunos resultados de muestra para referencia:


import cats.instances.int._ // for Monoid

foldMap(Vector(1, 2, 3))(identity)

// res1: Int = 6

import cats.instances.string._ // for Monoid

// Mapping to a String uses the concatenation monoid:

foldMap(Vector(1, 2, 3))(_.toString + "! ")

// res2: String = "1! 2! 3! "

// Mapping over a String to produce a String:

foldMap("Hello world!".toVector)(_.toString.toUpperCase)

// res3: String = "HELLO WORLD!"


Veamos la implementación: 


import cats.Monoid

import cats.syntax.semigroup._ // for |+|

def foldMap[A, B : Monoid](as: Vector[A])(func: A => B): B =

as.map(func).foldLeft(Monoid[B].empty)(_ |+| _)


Podemos hacer una pequeña alteración a este código para hacer todo en un solo paso:


def foldMap[A, B : Monoid](as: Vector[A])(func: A => B): B =

as.foldLeft(Monoid[B].empty)(_ |+| func(_))


Ahora que tenemos una implementación funcional de subproceso único de foldMap, veamos cómo distribuir el trabajo para que se ejecute en paralelo. Usaremos nuestra versión de subproceso único de foldMap como bloque de construcción. Escribiremos una implementación de CPU múltiple que simule la forma en que distribuiríamos el trabajo en un clúster de reducción de mapa:

  1. comenzamos con una lista inicial de todos los datos que necesitamos procesar;
  2. dividimos los datos en lotes, enviando un lote a cada CPU;
  3. las CPU ejecutan una fase de mapa a nivel de lote en paralelo;
  4. Las CPU ejecutan una fase de reducción de nivel de lote en paralelo, produciendo un local resultado de cada lote;
  5. reducimos los resultados de cada lote a un único resultado final.

Scala proporciona algunas herramientas simples para distribuir el trabajo entre subprocesos. Podríamos usar la biblioteca de colecciones paralelas para implementar una solución, pero desafiémonos a nosotros mismos profundizando un poco más e implementando el algoritmo nosotros mismos usando Futures.

Ya sabemos bastante sobre la naturaleza monádica de Futures. Tomemos un momento para un resumen rápido y para describir cómo se programan los futuros de Scala detrás de escena.

Los futuros se ejecutan en un grupo de subprocesos, determinado por un parámetro ExecutionContext implícito.

Cada vez que creamos un futuro, ya sea a través de una llamada a Future.apply o algún otro combinador, debemos tener un ExecutionContext implícito en el alcance:

import scala.concurrent.Future

import scala.concurrent.ExecutionContext.Implicits.global

val future1 = Future {

     (1 to 100).toList.foldLeft(0)(_ + _)

}

// future1: Future[Int] = Future(Success(5050))

val future2 = Future {

    (100 to 200).toList.foldLeft(0)(_ + _)

}

// future2: Future[Int] = Future(Success(15150))


En este ejemplo, hemos importado un ExecutionContext.Implicits.global. Este contexto predeterminado asigna un grupo de subprocesos con un subproceso por CPU en nuestra máquina. Cuando creamos un futuro, ExecutionContext lo programa para su ejecución. Si hay un subproceso libre en el grupo, Future comienza a ejecutarse de inmediato. La mayoría de las máquinas modernas tienen al menos dos CPU, por lo que en nuestro ejemplo es probable que future1 y future2 se ejecuten en paralelo.

Algunos combinadores crean nuevos Futuros que programan el trabajo en función de los resultados de otros Futuros. Los métodos map y flatMap, por ejemplo, programan cálculos que se ejecutan tan pronto como se calculan sus valores de entrada y hay una CPU disponible:

val future3 = future1.map(_.toString)

// future3: Future[String] = Future(Success(5050))

val future4 = for {

    a <- future1

    b <- future2

} yield a + b

// future4: Future[Int] = Future(Success(20200))


Podemos convertir una List[Future[A]] en una Future[List[A]] usando Future.sequence:


Future.sequence(List(Future(1), Future(2), Future(3)))

// res6: Future[List[Int]] = Future(Success(List(1, 2, 3)))


Podemos convertir una List[Future[A]] en una Future[List[A]] usando Future.sequence:


import cats.instances.future._ // for Applicative

import cats.instances.list._// for Traverse

import cats.syntax.traverse._// for sequence

List(Future(1), Future(2), Future(3)).sequence

// res7: Future[List[Int]] = Future(Success(List(1, 2, 3)))


Se requiere un ExecutionContext en cualquier caso. Finalmente, podemos usar Await.result para bloquear un futuro hasta que haya un resultado disponible:


import scala.concurrent._

import scala.concurrent.duration._

Await.result(Future(1), 1.second) // wait for the result

// res8: Int = 1


También hay implementaciones de Monad y Monoid para Future disponibles en cats.instances.future:


import cats.{Monad, Monoid}

import cats.instances.int._

// for Monoid

import cats.instances.future._ // for Monad and Monoid

Monad[Future].pure(42)

Monoid[Future[Int]].combine(Future(1), Future(2))


Me quedo medio largo el post, así que vamos a seguir en el proximo post. 



viernes, 10 de marzo de 2023

Recursos sobre scala


 Queres empezar en scala y no sabes de donde sacar info? 

Te paso una lista de recursos: 

Platforms

Community

Coding Tools:

Programming Environments

Build Tools

Code Formatting / Linting

Free Books, Tutorials and Guides:

Non-free Books:

Advanced!:

Free Scala Courses: * Functional Programming Principles in Scala
Functional Program Design in Scala
Parallel Programming * Big Data Analysis with Scala and Spark
Introduction to Programming with Dependent Types in Scala (advanced)

Non-Free Courses:

Scala Conferences: * Functional Scala (UK/Remote)
LambdaConf (USA) * Typelevel Summits (Misc.) * Scala by the Bay (USA) * flatMap (Norway) * Scala Up North (Canada) * Scala Days (USA, Europe)
Scala World (UK)
Scala Swarm (Portugal)
Scala.io (France)
Scalar (Central Europe) * Scala Sphere (Poland)
nescala (USA)
LX SCALA (South-West Europe)
ScalaConf (Russia)

Podcasts:

Scala Jobs:

Scala Libraries:

Web Development and Microservices
ZIO HTTP * Caliban (https://github.com/ghostdogpr/caliban) * Play
Akka HTTP
Lagom * Sttp (HTTP Client) * http4s * Finch * Udash - Frontend and Backend
Lift
Scalatra
Skinny
Vert.x
Sangria - GraphQL

Web Front End (Scala.js)

Database Access

Functional Programming

Concurrency / Parallelism

Mathematics

Distributed Computing

Blockchain

Miscellaneous:

Open Source Applications written in Scala

Related Communities:

Blogs/Periodicals: