Translate

sábado, 1 de junio de 2024

Recursion en erlang

La recursividad se puede explicar con la ayuda de conceptos y funciones matemáticas. Una función matemática básica como el factorial de un valor es un buen ejemplo de una función que se puede expresar de forma recursiva. El factorial de un número n es el producto de la secuencia 1 x 2 x 3 x ... x n, o alternativamente n x (n-1) x (n-2) x ... x 1. Para dar algunos ejemplos, el factorial de 3 es 3! = 3 x 2 x 1 = 6. ¡El factorial de 4 sería 4! = 4 x 3 x 2 x 1 = 24. Dicha función se puede expresar de la siguiente manera en notación matemática:


Lo que esto nos dice es que si el valor de n que tenemos es 0, devolvemos el resultado 1. Para cualquier valor superior a 0, devolvemos n multiplicado por el factorial de n-1, que se desarrolla hasta llegar a 1:

4! = 4 x 3!

4! = 4x3x2!

4! = 4x3x2x1!

4! = 4 x 3 x 2 x 1 x 1

¿Cómo se puede traducir una función así de la notación matemática a Erlang? La conversión es bastante simple. Eche un vistazo a las partes de la notación: n!, 1 y n((n-1)!) y luego los ifs. Lo que tenemos aquí es un nombre de función (n!), guardias (los if) y el cuerpo de la función (1 y n((n-1)!)). ¡Cambiaremos el nombre de n! a fac(N) para restringir un poco nuestra sintaxis y luego obtenemos lo siguiente:

-module(recursive).

-export([fac/1]).

 

fac(N) when N == 0 -> 1;

fac(N) when N > 0  -> N*fac(N-1).


¡Y esta función factorial ya está hecha! En realidad, es bastante similar a la definición matemática. Con la ayuda de la coincidencia de patrones, podemos acortar un poco la definición:


fac(0) -> 1;

fac(N) when N > 0 -> N*fac(N-1).


Una definición de recursividad podría abreviarse diciendo "una función que se llama a sí misma". Sin embargo, necesitamos tener una condición de parada (el término real es el caso base), porque de lo contrario haríamos un bucle infinito. En nuestro caso, la condición de detención es cuando n es igual a 0. En ese momento ya no le decimos a nuestra función que se llame a sí misma y detiene su ejecución allí mismo.


Recursión en Gleam



import gleam/io


pub fn main() {

  io.debug(factorial(5))

  io.debug(factorial(7))

}


// A recursive functions that calculates factorial

pub fn factorial(x: Int) -> Int {

  case x {

    // Base case

    0 -> 1

    1 -> 1


    // Recursive case

    _ -> x * factorial(x - 1)

  }

}


Gleam no tiene bucles, sino que la iteración se realiza mediante recursividad, es decir, mediante funciones de nivel superior que se llaman a sí mismas con diferentes argumentos.

Una función recursiva debe tener al menos un caso base y al menos un caso recursivo. Un caso base devuelve un valor sin volver a llamar a la función. Un caso recursivo vuelve a llamar a la función con diferentes entradas, volviendo a realizar un bucle.

La biblioteca estándar de Gleam tiene funciones para varios patrones de bucles comunes, algunos de los cuales se introducirán en lecciones posteriores; sin embargo, para bucles más complejos, la recursividad manual suele ser la forma más clara de escribirlo.

La recursividad puede parecer desalentadora o confusa al principio si estás más familiarizado con los lenguajes que tienen características especiales de bucle, ¡pero mantente firme! Con el tiempo, resultará tan familiar y cómodo como cualquier otra forma de iteración.

lunes, 27 de mayo de 2024

Buscar el mayor y el menor con erlang.


Retomando el ejercicio de buscar el mayor con erlang.

Vamos a buscar el menor y el mayor, pero estos algoritmos son muy similares solo hay que cambiar el mayor por el menor. 

El mayor y menor se pueden ver como una función que dado dos objetos retorne el mayor o el menor según corresponda y esta función se la podemos enviar a una función que nos permita encontrar el mayor o menor. Algo así:   


-module(test).

-export([max/1, min/1]).


max(L) -> buscar(L, fun(X, M) -> X > M end).

min(L) -> buscar(L, fun(X, M) -> X < M end).


buscar([X], _) -> X;

buscar([X|T], FX) -> 

    M = buscar(T, FX),

    COND = FX(X, M),

    if COND -> X;  

    true -> M

    end.


Y a compilarlo y probarlo : 

21> c(test).

{ok,test}

22> test:max([1,2,3,4,5]).

5

23> test:min([1,2,3,4,5]).

1

24> 


sábado, 25 de mayo de 2024

Buscar el mayor con erlang.


Vamos a buscar el mayor en una lista con Erlang. Ojo la idea de este algoritmo es practicar recursividad. Seguro que existen funciones que hacen esto super más eficiente. 

El razonamiento es si la lista tiene un elemento, ese elemento es el máximo. Y si tiene más de un elemento el máximo es el primer elemento (si es mayor al máximo del resto) o el máximo del resto si es mayor al primer elemento. 


max([X]) -> X;

max([X|T]) -> 

    M = max(T),

    if X > M -> X;  

    true -> M

    end.


Si probamos : 

> test:max([1,2,3,4,5]).

5

> test:max([2]).

2

> test:max([]). 

** exception error: no function clause matching test:max([]) (test.erl, line 4)

Cuando utilizamos max con una lista vacía nos lanza error, dado que no es posible calcular el máximo de una lista vacía. 


Para proteger un tipo de datos en erlang


Los tipos de datos básicos de Erlang son fáciles de detectar visualmente: las tuplas tienen llaves, las listas tienen corchetes, las cadenas están entre comillas dobles, etc. Por lo tanto, hacer cumplir un determinado tipo de datos ha sido posible con la coincidencia de patrones: una función head/1 tomar una lista solo podría aceptar listas porque de lo contrario, la coincidencia ([H|_]) habría fallado.

Sin embargo, tuvimos un problema con los valores numéricos porque no podíamos especificar rangos. En consecuencia, utilizamos guardias en funciones sobre la temperatura, la edad para conducir, etc. Ahora nos topamos con otro obstáculo. ¿Cómo podríamos escribir una protección que garantice que los patrones coincidan con datos de un tipo específico, como números, átomos o cadenas de bits?

Hay funciones dedicadas a esta tarea. Tomarán un único argumento y devolverán verdadero si el tipo es correcto, falso en caso contrario. Son parte de las pocas funciones permitidas en las expresiones de protección y se denominan BIF de prueba de tipo:


is_atom/1           is_binary/1        

is_bitstring/1      is_boolean/1        is_builtin/3       

is_float/1          is_function/1       is_function/2      

is_integer/1        is_list/1           is_number/1        

is_pid/1            is_port/1           is_record/2        

is_record/3         is_reference/1      is_tuple/1         


Se pueden utilizar como cualquier otra expresión de guardia, siempre que se permitan expresiones de guardia. Quizás te preguntes por qué no existe una función que simplemente proporcione el tipo del término que se está evaluando (algo parecido a type_of(X) -> Type). La respuesta es bastante simple. Erlang se trata de programar para los casos correctos: solo programa para lo que sabe que sucederá y lo que espera. Todo lo demás debería provocar errores lo antes posible. Aunque esto pueda parecer una locura, es de esperar que las explicaciones que obtendrá en Errores y excepciones aclaren las cosas. 

Los BIF de prueba de tipo constituyen más de la mitad de las funciones permitidas en las expresiones de protección. El resto también son BIF, pero no representan pruebas de tipo. Estos son:
abs(Number), bit_size(Bitstring), byte_size(Bitstring), element(N, Tuple), float(Term), hd(List), length(List), node(), node(Pid|Ref|Port), round(Number), self(), size(Tuple|Bitstring), tl(List), trunc(Number), tuple_size(Tuple).

Las funciones nodo/1 y self/0 están relacionadas con Erlang distribuido y procesos/actores. Eventualmente los usaremos, pero todavía tenemos otros temas que cubrir antes de eso.

Puede parecer que las estructuras de datos de Erlang son relativamente limitadas, pero las listas y tuplas suelen ser suficientes para construir otras estructuras complejas sin preocuparse por nada. Como ejemplo, el nodo básico de un árbol binario podría representarse como {nodo, Valor, Izquierda, Derecha}, donde Izquierda y Derecha son nodos similares o tuplas vacías. También podría representarme como:

{person, {name, <<"Fred T-H">>},
{qualities, ["handsome", "smart", "honest", "objective"]},
{faults, ["liar"]},
{skills, ["programming", "bass guitar", "underwater breakdancing"]}}.

Lo que muestra que al anidar tuplas y enumerarlas y llenarlas con datos, podemos obtener estructuras de datos complejas y construir funciones para operar con ellas.

En la versión R13B04 se agregó el BIF binario_to_term/2, que le permite deserializar datos de la misma manera que lo haría binario_to_term/1, excepto que el segundo argumento es una lista de opciones. Si pasa [seguro], el binario no se decodificará si contiene átomos desconocidos o funciones anónimas, lo que podría agotar la memoria.


viernes, 24 de mayo de 2024

Patrones de lista en Gleam

 


import gleam/int

import gleam/io

import gleam/list


pub fn main() {

  let x = list.repeat(int.random(5), times: int.random(3))

  io.debug(x)


  let result = case x {

    [] -> "Empty list"

    [1] -> "List of just 1"

    [4, ..] -> "List starting with 4"

    [_, _] -> "List of 2 elements"

    _ -> "Some other list"

  }

  io.debug(result)

}

Las listas y los valores que contienen pueden coincidir con patrones en expresiones de caso.

Los patrones de lista coinciden en longitudes específicas de lista. El patrón [] coincide con una lista vacía y el patrón [_] coincide con una lista con un elemento. No coincidirán en listas con otras longitudes.

El patrón de distribución... se puede utilizar para que coincida con el resto de la lista. El patrón [1, ..] coincide con cualquier lista que comience con 1. El patrón [_, _, ..] coincide con cualquier lista que tenga al menos dos elementos.


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 ... 




domingo, 19 de mayo de 2024

Patrones en texto de Gleam


import gleam/io


pub fn main() {

  io.debug(get_name("Hello, Joe"))

  io.debug(get_name("Hello, Mike"))

  io.debug(get_name("System still working?"))

}


fn get_name(x: String) -> String {

  case x {

    "Hello, " <> name -> name

    _ -> "Unknown"

  }

}

Se puede utilizar patrones en cadenas de caracteres, el operador <> se puede usar para hacer coincidir cadenas con un prefijo específico.

El patrón "Hello, " <> nombre coincide con cualquier cadena que comience con "Hello, " y asigna el resto de la cadena al nombre de la variable.

jueves, 16 de mayo de 2024

Hacer un servicio con gRPC y go-zero


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

syntax = "proto3";


package hello;


option go_package = "./hello";


message Request {

}


message Response {

  string msg = 1;

}


service Hello {

  rpc Ping(Request) returns(Response);

}


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

Done.

luego hacemos : 

cd server

go mod tidy 


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


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

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

}


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


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

y por ultimo corremos el proyecto: 

go run hello.go



martes, 14 de mayo de 2024

Conversiones de tipos en Erlang


Erlang, como muchos lenguajes, cambia el tipo de un término al convertirlo en otro. Esto se hace con la ayuda de funciones integradas, ya que muchas de las conversiones no se pudieron implementar en Erlang. Cada una de estas funciones toma la forma <tipo>_to_<tipo> y se implementa en el módulo erlang. Éstos son algunos de ellos:


1> erlang:list_to_integer("54").

54

2> erlang:integer_to_list(54).

"54"

3> erlang:list_to_integer("54.32").

** exception error: bad argument

     in function  list_to_integer/1

        called as list_to_integer("54.32")

4> erlang:list_to_float("54.32").

54.32

5> erlang:atom_to_list(true).

"true"

6> erlang:list_to_bitstring("hi there").

<<"hi there">>

7> erlang:bitstring_to_list(<<"hi there">>).

"hi there"


Etcétera. Estamos topando con un problema de lenguaje: debido a que se usa el esquema <tipo>_to_<tipo>, cada vez que se agrega un nuevo tipo al lenguaje, es necesario agregar una gran cantidad de BIF de conversión. Aquí está la lista:


atom_to_binary/2, atom_to_list/1, binary_to_atom/2, binary_to_existing_atom/2, binary_to_list/1, bitstring_to_list/1, binary_to_term/1, float_to_list/1, fun_to_list/1, integer_to_list/1, integer_to_list/2, iolist_to_binary/1, iolist_to_atom/1, list_to_atom/1, list_to_binary/1, list_to_bitstring/1, list_to_existing_atom/1, list_to_float/1, list_to_integer/2, list_to_pid/1, list_to_tuple/1, pid_to_list/1, port_to_list/1, ref_to_list/1, term_to_binary/1, term_to_binary/2 and tuple_to_list/1.



lunes, 13 de mayo de 2024

Tipos en Erlang


Erlang se escribe dinámicamente: cada error se detecta en tiempo de ejecución y el compilador no siempre le gritará al compilar módulos donde las cosas pueden resultar en fallas.

Un punto de fricción clásico entre los defensores de los lenguajes de tipado estático y dinámico tiene que ver con la seguridad del software que se escribe. Una idea sugerida con frecuencia es que los buenos sistemas de tipo estático con compiladores que los aplican con fervor detectarán la mayoría de los errores que esperan ocurrir antes de que puedas ejecutar el código. Como tal, los lenguajes escritos estáticamente deben considerarse más seguros que sus homólogos dinámicos. Si bien esto podría ser cierto en comparación con muchos lenguajes dinámicos, Erlang no está de acuerdo y ciertamente tiene un historial que lo demuestra. El mejor ejemplo son los nueve nueves (99,9999999%) de disponibilidad que se ofrecen en los conmutadores ATM Ericsson AXD 301, que constan de más de 1 millón de líneas de código Erlang. Tenga en cuenta que esto no es una indicación de que ninguno de los componentes de un sistema basado en Erlang haya fallado, sino que un sistema de conmutación general estuvo disponible el 99,9999999 % del tiempo, incluidas las interrupciones planificadas. Esto se debe en parte a que Erlang se basa en la noción de que una falla en uno de los componentes no debería afectar a todo el sistema. Se tienen en cuenta los errores provenientes del programador, fallas de hardware o [algunas] fallas de red: el lenguaje incluye características que le permitirán distribuir un programa a diferentes nodos, manejar errores inesperados y nunca dejar de ejecutarse.

Para abreviar, mientras que la mayoría de los lenguajes y sistemas de tipos tienen como objetivo hacer que un programa esté libre de errores, Erlang utiliza una estrategia en la que se supone que los errores ocurrirán de todos modos y se asegura de cubrir estos casos: El sistema de tipos dinámicos de Erlang no es una barrera para confiabilidad y seguridad de los programas. Esto suena como un montón de palabras proféticas, pero verás cómo se hace.

Históricamente se eligió la escritura dinámica por razones simples; Aquellos que implementaron Erlang al principio procedían en su mayoría de lenguajes escritos dinámicamente y, como tal, tener Erlang dinámico era la opción más natural para ellos.

Erlang también está fuertemente tipado. Un lenguaje débilmente tipado haría conversiones de tipos implícitas entre términos. Si Erlang tuviera un tipo débil, posiblemente podríamos hacer la operación 6 = 5 + "1". mientras que en la práctica, se lanzará una excepción:


1> 6 + "1".

** exception error: bad argument in an arithmetic expression

     in operator  +/2

        called as 6 + "1"


Por supuesto, hay ocasiones en las que es posible que desee convertir un tipo de datos en otro: cambiar cadenas normales en cadenas de bits para almacenarlas o un número entero en un número de punto flotante. La biblioteca estándar de Erlang proporciona una serie de funciones para hacerlo.



domingo, 12 de mayo de 2024

Patrones variables en Gleam

 


import gleam/int

import gleam/io


pub fn main() {

  let result = case int.random(5) {

    // Match specific values

    0 -> "Zero"

    1 -> "One"


    // Match any other value and assign it to a variable

    other -> "It is " <> int.to_string(other)

  }

  io.debug(result)

Los patrones en expresiones de caso también pueden asignar variables.

Cuando se utiliza un nombre de variable en un patrón, el valor con el que se compara se asigna a ese nombre y se puede utilizar en el cuerpo de esa cláusula. Como en other en el ejemplo. 

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

Expresiones de casos en Gleam


import gleam/int

import gleam/io


pub fn main() {

  let x = int.random(5)

  io.debug(x)


  let result = case x {

    // Match specific values

    0 -> "Zero"

    1 -> "One"


    // Match any other value

    _ -> "Other"

  }

  io.debug(result)

}


La expresión de caso es el tipo más común de control de flujo en el código Gleam. Es similar al switch en otros lenguajes, pero más poderoso que la mayoría.

Permite al programador decir "si los datos tienen esta forma, entonces ejecute este código", un proceso llamado coincidencia de patrones.

Gleam realiza una verificación exhaustiva para garantizar que los patrones en una expresión de caso cubran todos los valores posibles. Con esto, puede tener la confianza de que su lógica está actualizada para el diseño de los datos con los que está trabajando.