Translate

viernes, 29 de noviembre de 2024

ca1860: Evite utilizar el método de extensión 'Enumerable.Any()'


Me llamo la atención un warnning en mi código C# que decia: "CA1860: Avoid using 'Enumerable.Any()' extension method" y yo dije why? y así nacio este post. 

Para determinar si un tipo de colección tiene algún elemento, es más eficiente y claro usar las propiedades Length, Count o IsEmpty (si es posible) que llamar al método Enumerable.Any.

Any(), que es un método de extensión, usa consultas integradas en lenguaje (LINQ). Es más eficiente confiar en las propiedades propias de la colección y también aclara la intención.

Otro tema es que esta regla es similar a CA1827: No use Count()/LongCount() cuando se pueda usar Any(). Sin embargo, esa regla se aplica al método Count() de Linq, mientras que esta regla sugiere usar la propiedad Count.


Veamos un poco de código: 

bool HasElements(string[] strings)

{

    return strings.Any(); // esto esta mal. 

}


bool HasElements(string[] strings)

{

    return strings.Length > 0; // esto esta bien. 

}


Lo que me dejo pensando es: entiendo que sea más performante no utilizar Any() siempre si tenemos propiedades como Count, Length o IsEmpty. Pero esto no es una desventaja a la hora de cambiar el tipo de colección? 

Podemos concluir que no esta bueno que un lenguaje nos ofrezca 2 o 3 formas de hacer lo mismo y luego nos este retando porque elegimos una. 

Dejo link:

https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1860

jueves, 28 de noviembre de 2024

Listas por Comprensión en Python


Las listas por comprensión (o list comprehensions) son una característica poderosa y expresiva de Python que permite construir listas nuevas a partir de iterables existentes, todo ello en una sola línea de código. Son legibles, concisas y, a menudo, más eficientes que los bucles tradicionales.

Son una forma de crear listas en Python utilizando una sintaxis compacta basada en una expresión, un iterador y (opcionalmente) una condición.

Con la forma : [nueva_expresión for elemento in iterable if condición]

  • nueva_expresión: La operación o transformación a aplicar a cada elemento.
  • for elemento in iterable: Itera sobre los elementos de un iterable (como una lista o rango).
  • if condición(opcional): Filtra los elementos según una condición.


Convertir una lista de números en sus cuadrados:


numeros = [1, 2, 3, 4, 5]

cuadrados = [n**2 for n in numeros]

print(cuadrados)

# Salida: [1, 4, 9, 16, 25]


Seleccionar solo los números pares antes de calcular sus cuadrados:


numeros = [1, 2, 3, 4, 5]

cuadrados_pares = [n**2 for n in numeros if n % 2 == 0]

print(cuadrados_pares)

# Salida: [4, 16]


Puedes llamar funciones dentro de la expresión:


nombres = ["Ana", "Bernardo", "Carla", "Diego"]

longitudes = [len(nombre) for nombre in nombres]

print(longitudes)

# Salida: [3, 8, 5, 5]


Crear combinaciones de elementos con múltiples iteradores:


colores = ["rojo", "verde", "azul"]

tamaños = ["pequeño", "mediano", "grande"]


combinaciones = [(color, tamaño) for color in colores for tamaño in tamaños]

print(combinaciones)

# Salida: [('rojo', 'pequeño'), ('rojo', 'mediano'), ..., ('azul', 'grande')]


Usar listas por comprensión con otras estructuras, como diccionarios por comprensión


nombres = ["Ana", "Bernardo", "Carla"]

diccionario = {nombre: len(nombre) for nombre in nombres}

print(diccionario)

# Salida: {'Ana': 3, 'Bernardo': 8, 'Carla': 5}


Conjuntos: 

numeros = [1, 2, 2, 3, 4, 4]

pares = {n for n in numeros if n % 2 == 0}

print(pares)

# Salida: {2, 4}


Para listas grandes, usa generadores para ahorrar memoria:


numeros = (n**2 for n in range(10))

print(list(numeros))

# Salida: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]


Aunque son poderosas, a veces es mejor optar por un bucle tradicional:

- Cuando la lógica es demasiado compleja y afecta la legibilidad.

- Si necesitas manejar excepciones o realizar múltiples pasos intermedios.


Las listas por comprensión son una herramienta esencial para escribir código Python limpio y eficiente. Con práctica, dominarás su uso y aprovecharás al máximo su flexibilidad. ¿Te atreves a crear tus propias transformaciones?


miércoles, 27 de noviembre de 2024

Estructura Key-Value en Erlang


Para pequeñas cantidades de datos, básicamente hay dos estructuras de datos que se pueden usar. La primera se llama proplist. Una proplist es cualquier lista de tuplas de la forma [{Key,Value}]. Son un tipo de estructura extraño porque no hay otra regla que esa. De hecho, las reglas son tan relajadas que la lista también puede contener valores booleanos, números enteros y lo que quieras. Sin embargo, nos interesa más la idea de una tupla con una clave y un valor en una lista. Para trabajar con proplists, puedes usar el módulo proplists. Contiene funciones como proplists: delete/2, proplists:get_value/2, proplists:get_all_values/2, proplists:lookup/2 y proplists:lookup_all/2.

Notarás que no hay ninguna función para agregar o actualizar un elemento de la lista. Esto muestra cuán vagamente definidas están las proplists como estructura de datos. Para obtener estas funcionalidades, debes construir  tu elemento manualmente ([NewElement|OldList]) y usar funciones como lists:keyreplace/4. Usar dos módulos para una pequeña estructura de datos no es lo más claro, pero debido a que las proplists están definidas de manera tan vaga, a menudo se usan para tratar con listas de configuración y descripciones generales de un elemento determinado. Las proplists no son exactamente estructuras de datos completas. Son más bien un patrón común que aparece cuando se usan listas y tuplas para representar algún objeto o elemento; el módulo proplists es una especie de caja de herramientas para este patrón.

Si desea un almacén de clave-valor más completo para pequeñas cantidades de datos, el módulo orddict es lo que necesita. Los orddicts (diccionarios ordenados) son proplists con un gusto por la formalidad. Cada clave puede estar allí una vez, la lista completa está ordenada para una búsqueda promedio más rápida, etc. Las funciones comunes para el uso de CRUD incluyen orddict:store/3, orddict:find/2 (cuando no sabe si la clave está en los diccionarios), orddict:fetch/2 (cuando sabe que está allí o que debe estar allí) y orddict:erase/2.

Los orddicts son un compromiso generalmente bueno entre complejidad y eficiencia hasta aproximadamente 75 elementos. Después de esa cantidad, deberías cambiar a diferentes almacenes de clave-valor.

Básicamente, existen dos estructuras/módulos de clave-valor para manejar mayores cantidades de datos: dicts y gb_trees. Los diccionarios tienen la misma interfaz que los orddicts: dict:store/3, dict:find/2, dict:fetch/2, dict:erase/2 y todas las demás funciones, como dict:map/2 y dict:fold/2 (¡bastante útiles para trabajar en toda la estructura de datos!). Por lo tanto, los dicts son muy buenas opciones para escalar los orddicts cuando sea necesario.

Los árboles balanceados generales, por otro lado, tienen muchas más funciones que te dejan un control más directo sobre cómo se debe usar la estructura. Básicamente, existen dos modos para gb_trees: el modo en el que conoces tu estructura al dedillo (lo llamo el "modo inteligente") y el modo en el que no puedes asumir mucho sobre ella (lo llamo el "modo ingenuo"). En el modo ingenuo, las funciones son gb_trees:enter/3, gb_trees:lookup/2 y gb_trees:delete_any/2. Las funciones inteligentes relacionadas son gb_trees:insert/3, gb_trees:get/2, gb_trees:update/3 y gb_trees:delete/2. También existe gb_trees:map/2, que siempre es una buena opción cuando la necesitas.

La desventaja de las funciones "ingenuas" sobre las "inteligentes" es que, como los gb_trees son árboles equilibrados, siempre que insertes un nuevo elemento (o elimines un montón), es posible que el árbol deba equilibrarse a sí mismo. Esto puede llevar tiempo y memoria (incluso en comprobaciones inútiles solo para asegurarse). Todas las funciones "inteligentes" suponen que la clave está presente en el árbol: esto te permite omitir todas las comprobaciones de seguridad y da como resultado tiempos más rápidos.

¿Cuándo deberías usar gb_trees en lugar de diccionarios? Bueno, no es una decisión clara. Como lo mostrará el módulo de referencia que he escrito, gb_trees y dicts tienen rendimientos algo similares en muchos aspectos. Sin embargo, la referencia demuestra que dicts tienen las mejores velocidades de lectura mientras que gb_trees tienden a ser un poco más rápidos en otras operaciones. Puede juzgar en función de sus propias necesidades cuál sería el mejor.

Ah, y otra cosa a tener en cuenta que mientras que dicts tienen una función de plegado, gb_trees no: en su lugar, tienen una función de iteración, que devuelve un fragmento del árbol en el que puede llamar a gb_trees:next(Iterator) para obtener los siguientes valores en orden. Lo que esto significa es que necesita escribir sus propias funciones recursivas sobre gb_trees en lugar de usar un genric fold. Por otro lado, gb_trees te permite tener un acceso rápido a los elementos más pequeños y más grandes de la estructura con gb_trees:smallest/1 y gb_trees:largest/1.

Por lo tanto, diría que las necesidades de tu aplicación son las que deberían determinar qué almacén de clave-valor elegir. Diferentes factores, como la cantidad de datos que tienes que almacenar, lo que necesitas hacer con ellos y demás, tienen su importancia. Mide, perfila y compara para asegurarte.

Existen algunos almacenes de clave-valor especiales para manejar recursos de diferentes tamaños. Estos almacenes son las tablas ETS, las tablas DETS y la base de datos mnesia. Sin embargo, su uso está fuertemente relacionado con los conceptos de múltiples procesos y distribución. 

A partir de la versión 17.0, el lenguaje admite un nuevo tipo de datos de clave-valor nativo, descrito en Postscript: Maps. 

martes, 26 de noviembre de 2024

Registros en Erlang


Los record de Erlang son muy parecidos a las struct en C.

Se declaran como atributos de módulo de la siguiente manera:

-module(records).

-compile(export_all).


-record(robot, {name,

                type=industrial,

                hobbies,

                details=[]}).


Aquí tenemos un registro que representa robots con 4 campos: nombre, tipo, pasatiempos y detalles. También hay un valor predeterminado para el tipo y los detalles, industrial y [], respectivamente. A continuación, se muestra cómo declarar un registro en el módulo records:

first_robot() ->

    #robot{name="Mechatron",

           type=handmade, 

           details=["Moved by a small man inside"]}.


Y ejecutando el código:


1> c(records).

{ok,records}

2> records:first_robot().

{robot,"Mechatron",handmade,undefined,

       ["Moved by a small man inside"]}


Los registros de Erlang son simplemente azúcar sintáctico sobre tuplas. Afortunadamente, hay una forma de mejorarlo. El shell de Erlang tiene un comando rr(Module) que le permite cargar definiciones de registros desde Module:

3> rr(records).

[robot]

4> records:first_robot().         

#robot{name = "Mechatron",type = handmade,

       hobbies = undefined,

       details = ["Moved by a small man inside"]}


Esto hace que sea mucho más fácil trabajar con registros de esa manera. Notarás que en first_robot/0, no habíamos definido el campo de pasatiempos y no tenía un valor predeterminado en su declaración. Erlang, por defecto, establece el valor como indefinido.

Para ver el comportamiento de los valores predeterminados que establecimos en la definición del robot, compilemos la siguiente función:

car_factory(CorpName) ->

    #robot{name=CorpName, hobbies="building cars"}.


Y ejecutalo:


5> c(records).

{ok,records}

6> records:car_factory("Jokeswagen").

#robot{name = "Jokeswagen",type = industrial,

       hobbies = "building cars",details = []}

Y tenemos un robot industrial al que le gusta pasar el tiempo construyendo coches.

La función rr() puede tomar más que un nombre de módulo: puede tomar un comodín (como rr("*")) y también una lista como segundo argumento para especificar qué registros cargar.

Hay algunas otras funciones para manejar registros en el shell: rd(Name, Definition) le permite definir un registro de una manera similar a la función -record(Name, Definition) utilizada en nuestro módulo. Puede usar rf() para "descargar" todos los registros, o rf(Name) o rf([Names]) para deshacerse de definiciones específicas.

Puede usar rl() para imprimir todas las definiciones de registros de una manera que pueda copiar y pegar en el módulo o usar rl(Name) o rl([Names]) para restringirlo a registros específicos.

Por último, rp(Term) le permite convertir una tupla en un registro (siempre que exista la definición).

Escribir registros por sí solo no hará mucho. Necesitamos una manera de extraer valores de ellos. Básicamente, hay dos maneras de hacer esto. El primero tiene una "sintaxis de punto" especial. Suponiendo que tiene cargada la definición de registro para robots:

5> Crusher = #robot{name="Crusher", hobbies=["Crushing people","petting cats"]}. 

#robot{name = "Crusher",type = industrial,

       hobbies = ["Crushing people","petting cats"],

       details = []}

6> Crusher#robot.hobbies.

["Crushing people","petting cats"]


No es una sintaxis muy bonita. Esto se debe a la naturaleza de los registros como tuplas. Como son solo una especie de truco del compilador, tienes que mantener las palabras claves para definir qué registro va con qué variable, de ahí la parte #robot de Crusher#robot.hobbies. Es triste, pero no hay forma de evitarlo. Peor aún, los registros anidados se vuelven bastante feos:

7> NestedBot = #robot{details=#robot{name="erNest"}}.

#robot{name = undefined, type = industrial,

       hobbies = undefined,

       details = #robot{name = "erNest",type = industrial,

                        hobbies = undefined,details = []}}

8> (NestedBot#robot.details)#robot.name. 

"erNest"


Para mostrar aún más la dependencia de los registros en las tuplas, veamos :


9> #robot.type.

3

Lo que esto genera es qué elemento de la tupla subyacente es.

Una característica de ahorro de los registros es la posibilidad de usarlos en los encabezados de funciones para hacer coincidir patrones y también en los guards. Declare un nuevo registro de la siguiente manera en la parte superior del archivo y luego agregue las funciones debajo:

-record(user, {id, name, group, age}).


%% use pattern matching to filter

admin_panel(#user{name=Name, group=admin}) ->

    Name ++ " is allowed!";

admin_panel(#user{name=Name}) ->

    Name ++ " is not allowed".


%% can extend user without problem

adult_section(U = #user{}) when U#user.age >= 18 ->

    %% Show stuff that can't be written in such a text

    allowed;

adult_section(_) ->

    %% redirect to sesame street site

    forbidden.


Esto nos permite ver que no es necesario hacer coincidir todas las partes de la tupla o incluso saber cuántas hay al escribir la función: solo podemos hacer coincidir la edad o el grupo si es lo que se necesita y olvidarnos del resto de la estructura. Si utilizáramos una tupla normal, la definición de la función podría tener que parecerse un poco a function({record, _, _, ICareAboutThis, _, _}) -> .... Entonces, cada vez que alguien decida agregar un elemento a la tupla, alguien más (probablemente enojado por todo esto) tendría que ir y actualizar todas las funciones donde se usa esa tupla.

La siguiente función ilustra cómo actualizar un registro (de lo contrario, no serían muy útiles):


repairman(Rob) ->

    Details = Rob#robot.details,

    NewRob = Rob#robot{details=["Repaired by repairman"|Details]},

    {repaired, NewRob}.


Y luego:


16> c(records).

{ok,records}

17> records:repairman(#robot{name="Ulbert", hobbies=["trying to have feelings"]}).

{repaired,#robot{name = "Ulbert",type = industrial,

                 hobbies = ["trying to have feelings"],

                 details = ["Repaired by repairman"]}}


Y puedes ver que mi robot ha sido reparado. La sintaxis para actualizar registros es un poco especial aquí. Parece que estamos actualizando el registro en su lugar (Rob#robot{Field=NewValue}) pero todo es un truco del compilador para llamar a la función subyacente erlang:setelement/3.

Una última cosa sobre los registros. Debido a que son bastante útiles y la duplicación de código es molesta, los programadores de Erlang comparten registros con frecuencia entre módulos con la ayuda de archivos de encabezado. Los archivos de encabezado de Erlang son bastante similares a su contraparte de C: no son más que un fragmento de código que se agrega al módulo como si estuviera escrito allí en primer lugar. Crea un archivo llamado records.hrl con el siguiente contenido:


%% this is a .hrl (header) file.

-record(included, {some_field,

                   some_default = "yeah!",

                   unimaginative_name}).


Para incluirlo en records.erl, simplemente agregue la siguiente línea al módulo:


-include("records.hrl").


Y luego la siguiente función para probarlo:


included() -> #included{some_field="Some value"}.


Ahora, pruébalo como siempre:


18> c(records).

{ok,records}

19> rr(records).

[included,robot,user]

20> records:included().

#included{some_field = "Some value",some_default = "yeah!",

          unimaginative_name = undefined}


Eso es todo sobre los registros; son feos pero útiles. Su sintaxis no es bonita, no son gran cosa, pero son relativamente importantes para la capacidad de mantenimiento de su código.

A menudo verá software de código abierto que utiliza el método que se muestra aquí de tener un archivo .hrl para todo el proyecto para los registros que se comparten entre todos los módulos. Si bien me sentí obligado a documentar este uso, recomiendo enfáticamente que mantenga todas las definiciones de registros locales, dentro de un módulo. Si desea que algún otro módulo observe las entrañas de un registro, escriba funciones para acceder a sus campos y mantenga sus detalles lo más privados posible. Esto ayuda a prevenir conflictos de nombres, evita problemas al actualizar el código y, en general, mejora la legibilidad y la capacidad de mantenimiento de su código.

domingo, 24 de noviembre de 2024

Macros en Elixir


Elixir no solo es conocido por su modelo de concurrencia y eficiencia, sino también por su poderosa capacidad de metaprogramación, gracias a sus macros. Con ellas, puedes escribir código que genera más código, permitiendo crear herramientas, DSLs (lenguajes específicos de dominio) y extender el lenguaje.

En Elixir, las macros son funciones especiales que reciben expresiones y devuelven otras expresiones transformadas. Estas transformaciones ocurren en tiempo de compilación, lo que las diferencia de las funciones normales.


defmodule EjemploMacro do

  defmacro saludo do

    quote do

      IO.puts("¡Hola, mundo!")

    end

  end

end


require EjemploMacro

EjemploMacro.saludo()

# Salida: ¡Hola, mundo!


¿Cómo funciona esto?

  • quote/2: Captura código como datos. Convierte un bloque en su representación AST (Abstract Syntax Tree).
  • unquote/1: Inserta valores dinámicos en el código generado.
  • defmacro: Declara macros.


Imagina que necesitas loguear todas las llamadas a funciones en un módulo. Con macros, puedes automatizar este proceso:


defmodule LoggerMacro do

  defmacro log_call(fun_name, do: block) do

    quote do

      IO.puts("Llamando a #{unquote(fun_name)}")

      result = unquote(block)

      IO.puts("Resultado: #{result}")

      result

    end

  end

end


defmodule Ejemplo do

  require LoggerMacro


  def calcular do

    LoggerMacro.log_call(:calcular) do

      1 + 2

    end

  end

end


Ejemplo.calcular()

# Salida:

# Llamando a calcular

# Resultado: 3


Los macros son herramientas poderosas para crear DSLs. Por ejemplo, un DSL para rutas web (similar a Plug.Router):


defmodule MiniRouter do

  defmacro route(method, path, do: block) do

    quote do

      def handle_request(unquote(method), unquote(path)) do

        unquote(block)

      end

    end

  end

end


defmodule MyRouter do

  require MiniRouter


  MiniRouter.route(:get, "/") do

    IO.puts("¡Bienvenido!")

  end


  MiniRouter.route(:post, "/create") do

    IO.puts("¡Creando recurso!")

  end

end


MyRouter.handle_request(:get, "/")

# Salida: ¡Bienvenido!


Aunque las macros son útiles, pueden complicar el código:

  • Usa macros solo si la lógica no puede resolverse con funciones.
  • Opta por composición funcional siempre que sea posible.


Las macros en Elixir son una herramienta increíblemente poderosa para extender el lenguaje y crear soluciones elegantes. Sin embargo, requieren un buen entendimiento del AST y un uso responsable. 

¿Por qué C++ utiliza punteros para implementar el polimorfismo?


En C++, los punteros son fundamentales para implementar el polimorfismo debido a cómo se gestionan los objetos y sus referencias en tiempo de ejecución. 

En C++, el polimorfismo en tiempo de ejecución se logra mediante el uso de funciones virtuales y tablas de métodos virtuales (vtable). Para que el compilador pueda llamar al método adecuado de una clase derivada, necesita acceso indirecto al objeto en memoria.

  • Cuando usamos punteros o referencias, el compilador puede determinar el tipo del objeto en tiempo de ejecución y acceder a la vtable correcta.
  • Si usáramos valores en lugar de punteros, el objeto sería copiado, y la información dinámica sobre su tipo (necesaria para el polimorfismo) se perdería.


#include <iostream>

using namespace std;


class Base {

public:

    virtual void speak() const {

        cout << "Soy Base\n";

    }

};


class Derived : public Base {

public:

    void speak() const override {

        cout << "Soy Derived\n";

    }

};


int main() {

    Base* obj = new Derived(); // Usamos puntero para polimorfismo

    obj->speak();             // Llama al método de Derived en tiempo de ejecución

    delete obj;

}


Aquí, el puntero obj permite que la llamada a speak() sea resuelta dinámicamente en tiempo de ejecución.

Otro problema es el slicing, que ocurre cuando un objeto derivado se asigna a un objeto base por valor. Esto copia solo la parte de la clase base, perdiendo los datos específicos de la clase derivada.

Por ejemplo: 


Derived d;

Base b = d; // Copia solo la parte "Base" del objeto

b.speak();  // Siempre llamará a Base::speak(), incluso si es virtual


El uso de punteros evita este problema, ya que siempre apuntan al objeto completo en memoria.

En C++, el polimorfismo suele estar vinculado a la asignación dinámica. Los punteros son necesarios para gestionar la memoria del heap y permiten que un programa decida en tiempo de ejecución qué tipo de objeto crear y manipular.


Base* createObject(bool flag) {

    if (flag)

        return new Derived();

    else

        return new Base();

}


Sin punteros, esta flexibilidad sería difícil de implementar.

Por ultimo, los punteros permiten acceder a objetos grandes en el heap sin necesidad de copiar toda su estructura. Esto es especialmente útil para jerarquías complejas de clases.

En vez de manejar múltiples copias de objetos derivados, los punteros permiten trabajar con una referencia común al objeto base, pero respetando el comportamiento dinámico. Si bien esto no tiene que ver directamente con el polimorfismo lo agregamos para ser más completos :D


C++ utiliza punteros para implementar el polimorfismo porque permiten:

  • Resolución dinámica de métodos a través de las vtables.
  • Evitar el problema de slicing.
  • Compatibilidad con asignación dinámica.
  • Referencias eficientes a objetos complejos.


sábado, 23 de noviembre de 2024

La filosofía Ponylang: "hacer las cosas bien"


En el espíritu de Richard Gabriel, la filosofía Pony no es ni “lo correcto” ni “lo peor es mejor”. Es “hacer las cosas bien”.

  • Corrección. La incorrección simplemente no está permitida. No tiene sentido intentar hacer las cosas si no puedes garantizar que el resultado sea correcto.
  • Rendimiento. La velocidad en tiempo de ejecución es más importante que todo excepto la corrección. Si se debe sacrificar el rendimiento por la corrección, intenta encontrar una nueva forma de hacer las cosas. Cuanto más rápido pueda el programa hacer las cosas, mejor. Esto es más importante que cualquier cosa excepto un resultado correcto.
  • Simplicidad. La simplicidad se puede sacrificar por el rendimiento. Es más importante que la interfaz sea simple que la implementación. Cuanto más rápido pueda el programador hacer las cosas, mejor. Está bien hacer las cosas un poco más difíciles para el programador para mejorar el rendimiento, pero es más importante hacer las cosas más fáciles para el programador que para el lenguaje/tiempo de ejecución.
  • Coherencia. La consistencia puede sacrificarse por la simplicidad o el rendimiento. No permita que una consistencia excesiva se interponga en el camino de hacer las cosas.
  • Completitud. Es bueno cubrir tantas cosas como sea posible, pero la completitud puede sacrificarse por cualquier otra cosa. Es mejor hacer algunas cosas ahora que esperar hasta que todo pueda hacerse más tarde.

El enfoque de "hacer las cosas" tiene la misma actitud hacia la corrección y la simplicidad que "lo correcto", pero la misma actitud hacia la consistencia y la completitud que "lo peor es mejor". También agrega el rendimiento como un nuevo principio, tratándolo como la segunda cosa más importante (después de la corrección).

A lo largo del diseño y desarrollo del lenguaje se deben respetar los siguientes principios.

  • Utilice el enfoque de hacer las cosas.
  • Gramática simple. El lenguaje debe ser trivial de analizar tanto para humanos como para computadoras.
  • Sin código cargable. Todo es conocido por el compilador.
  • Totalmente seguro para los tipos. No existe la coerción del tipo “confía en mí, sé lo que estoy haciendo”.
  • Totalmente seguro para la memoria. No existe la coerción del tipo “este número aleatorio es en realidad un puntero, de verdad”.
  • No hay fallos. Un programa que compila nunca debería fallar (aunque puede quedarse colgado o hacer algo no deseado).
  • Mensajes de error sensatos. Siempre que sea posible, utilice mensajes de error simples para casos de error específicos. Está bien asumir que el programador conoce las definiciones de las palabras en nuestro léxico, pero evite la jerga de compiladores u otra jerga informática.
  • Sistema de compilación inherente. No se requieren aplicaciones separadas para configurar o compilar.
  • Trate de reducir los errores de programación comunes mediante el uso de una sintaxis restrictiva.
  • Proporcione una forma única, limpia y clara de hacer las cosas en lugar de atender los prejuicios preferidos de cada programador.
  • Realice actualizaciones limpias. No intente fusionar las nuevas características con las que están reemplazando, si algo está roto, elimínelo y reemplácelo de una sola vez. Siempre que sea posible, proporcione utilidades de reescritura para actualizar el código fuente entre versiones de lenguaje.
  • Tiempo de compilación razonable. Reducir el tiempo de compilación es importante, pero menos importante que el rendimiento y la corrección en tiempo de ejecución.
  • Está bien permitir que el programador omita algunas cosas del código (argumentos predeterminados, inferencia de tipos, etc.), pero siempre se debe permitir la especificación completa.
  • Sin ambigüedad. El programador nunca debería tener que adivinar lo que hará el compilador, o viceversa.
  • Documentar la complejidad requerida. No todas las características del lenguaje tienen que ser triviales de entender, pero las características complejas deben tener explicaciones completas en la documentación para que se permitan en el lenguaje.
  • Las características del lenguaje deben ser mínimamente intrusivas cuando no se usan.
  • Semántica completamente definida. La semántica de todas las características del lenguaje debe estar disponible en la documentación estándar del lenguaje. No es aceptable dejar el comportamiento sin definir o "dependiente de la implementación".
  • Debe estar disponible un acceso eficiente al hardware, pero esto no tiene que impregnar todo el lenguaje.
  • La biblioteca estándar debe implementarse en Pony.
  • Interoperabilidad. Debe ser interoperable con otros lenguajes, pero esto puede requerir una capa de corrección si se utilizan tipos no primitivos.
  • Evite problemas con la biblioteca. El uso de bibliotecas Pony de terceros debe ser lo más sencillo posible, sin sorpresas. Esto incluye escribir y distribuir bibliotecas y usar múltiples versiones de una biblioteca en un solo programa.

miércoles, 20 de noviembre de 2024

Introducción a Plug de Elixir


Elixir, con su enfoque funcional y capacidad para manejar concurrencia de manera eficiente, es un lenguaje ideal para aplicaciones web. Si bien Phoenix es el framework más conocido, Plug es una alternativa minimalista perfecta para quienes buscan simplicidad y flexibilidad, similar a lo que Sinatra ofrece en Ruby.

Plug es un conjunto de especificaciones y módulos para construir aplicaciones web en Elixir. Se centra en el manejo de conexiones HTTP y proporciona herramientas básicas para construir rutas y middlewares.

  • Minimalista: Perfecto para aplicaciones simples o como base para proyectos más grandes.
  • Rápido: Aprovecha Cowboy, un servidor HTTP eficiente.
  • Flexible: Puedes usarlo directamente o integrarlo en frameworks más grandes como Phoenix.

Para comenzar, necesitas agregar Plug y Cowboy a tu proyecto. En el archivo `mix.exs`:

Antes creas el proyecto : 

mix new hello_world

Y Luego editamos mix.exs


defp deps do

  [

    {:plug, "~> 1.13"},

    {:plug_cowboy, "~> 2.6"}

  ]

end


Ejecuta mix deps.get para instalar las dependencias.

Para comenzar a crear Plugs, necesitamos conocer, y adherirse a la especificación Plug. Afortunadamente para nosotros, sólo hay dos funciones necesarias: init/1 y call/2.


Aquí hay un Plug simple que devuelve “Hello World!”:


defmodule Example.HelloWorldPlug do

  import Plug.Conn


  def init(options), do: options


  def call(conn, _opts) do

    conn

    |> put_resp_content_type("text/plain")

    |> send_resp(200, "Hello World!")

  end

end

Guarda el archivo en lib/example/hello_world_plug.ex.

La función init/1 se utiliza para inicializar las opciones de nuestros Plugs. Esta es llamada por el árbol de supervisión. De momento, está será una lista vacía que es ignorada.

El valor retornado por la función init/1 eventualmente será pasado a call/2 como su segundo argumento.

La función call/2 es ejecutada por cada petición que viene desde el servidor web, Cowboy. Esta recibe una estructura de conexión %Plug.Conn{} como su primer argumento y se espera que retorne una estructura de conexión %Plug.Conn{}.

Debido a que estamos iniciando nuestra aplicación plug desde cero, necesitamos definir el módulo de la aplicación. Actualiza lib/example.ex para iniciar y supervisar Cowboy:


defmodule Example do

  use Application

  require Logger


  def start(_type, _args) do

    children = [

      Plug.Adapters.Cowboy.child_spec(:http, Example.HelloWorldPlug, [], port: 8080)

    ]


    Logger.info("Started application")


    Supervisor.start_link(children, strategy: :one_for_one)

  end

end


Esto supervisa Cowboy, y a su vez, supervisa nuestro HelloWorldPlug.

En la petición a Plug.Adapters.Cowboy.child_spec/4, el tercer argumento será pasado a Example.HelloWorldPlug.init/1.

Aún no hemos terminado. Abre mix.exs de nuevo, y busca la función applications. De momento la parte de aplication en mix.exs necesita dos cosas:

  • Una lista de aplicaciones de dependencia (cowboy, logger, and plug) que necesintan iniciar, y
  • Configuración para nuestra aplicación, la cual también deberá iniciar automáticamente. Vamos a actualizarla para hacerlo:

def application do

  [

    extra_applications: [:cowboy, :logger, :plug],

    mod: {Example, []}

  ]

end

Estamos listos para probar este servidor web, minimalístico basado en Plug. En la línea de comando ejecuta:

mix run --no-halt

Cuando todo termine de compilar, y el mensaje [info] Started app aparece, abre el explorador web en 127.0.0.1:8080. Este debera de desplegar:

Hello World!

Plug es ideal para quienes buscan simplicidad y control en sus aplicaciones web. Su integración con Cowboy y su flexibilidad lo hacen una excelente opción para proyectos ligeros, APIs o como base para aprender Elixir.

Dejo link: https://elixirschool.com/es/lessons/misc/plug

martes, 19 de noviembre de 2024

QuickSort en Pony


Un Algoritmo que me gusta mucho es el quicksort, porque es un algoritmo por demás claro. Ya he escrito lo fácil que es implementarlo en scala, erlang, rust, haskell , F# y lisp.

Ahora le toca a Pony. Básicamente el algoritmo toma un pivot y agrupa los menores del pivot al principio y los mayores al final y aplica quicksort a estos 2 grupos. Y si la lista es vacía o tiene un elemento, ya esta ordenada. 


Vamos al código:  


 actor Main

  new create(env: Env) =>

    let data = [4; 2; 7; 1; 3; 6; 5]

    env.out.print("Original: " + data.string())


    let sorted = quicksort(data)

    env.out.print("Sorted: " + sorted.string())


  fun quicksort(data: Array[U64]): Array[U64] =>

    if data.size() <= 1 then

      data

    else

      let pivot = data(0)?

      let less = data.values().filter(lambda(x: U64): Bool => x < pivot end)

      let greater = data.values().filter(lambda(x: U64): Bool => x > pivot end)


      let less_sorted = quicksort(less)

      let greater_sorted = quicksort(greater)


      let result = less_sorted.append(pivot)

      result.append(greater_sorted)

    end


lunes, 18 de noviembre de 2024

Elixir: Concurrencia Hecha Sencilla


Elixir, basado en la máquina virtual de Erlang (BEAM), utiliza el modelo de actores como su paradigma de concurrencia. Este modelo permite manejar múltiples procesos de manera eficiente y segura, lo que lo hace ideal para sistemas distribuidos y concurrentes.

El modelo de actores es un paradigma de concurrencia en el que las entidades llamadas actores:

  • Son unidades independientes de ejecución.
  • Tienen su propio estado y no comparten memoria con otros actores.
  • Se comunican mediante el envío de mensajes.

En Elixir, los procesos son implementaciones del modelo de actores y son extremadamente ligeros gracias a la eficiencia de la VM de Erlang.

En Elixir, los actores se implementan utilizando módulos como GenServer. Vamos a crear un ejemplo básico de un contador que incrementa su valor en respuesta a mensajes.

   defmodule Counter do

     use GenServer


     # Inicio del actor con un estado inicial

     def start_link(initial_value) do

       GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)

     end


     # Callbacks

     def init(initial_value) do

       {:ok, initial_value}

     end


     # Manejar el mensaje para incrementar el contador

     def handle_call(:increment, _from, state) do

       {:reply, state + 1, state + 1}

     end


     def handle_call(:get, _from, state) do

       {:reply, state, state}

     end

   end


   # Iniciar el actor con un valor inicial de 0

   {:ok, _pid} = Counter.start_link(0)


   # Incrementar el contador

   Counter.call(:increment) # Devuelve 1

   Counter.call(:increment) # Devuelve 2


   # Obtener el valor actual

   Counter.call(:get) # Devuelve 2


Ahora crearemos múltiples actores que se comuniquen entre sí. Supongamos que tenemos un sistema donde un actor recopila datos y otro los procesa.


   defmodule DataCollector do

     use GenServer


     def start_link(processor_pid) do

       GenServer.start_link(__MODULE__, processor_pid, name: __MODULE__)

     end


     def init(processor_pid) do

       {:ok, processor_pid}

     end


     def handle_cast({:collect, data}, processor_pid) do

       send(processor_pid, {:process, data})

       {:noreply, processor_pid}

     end

   end

 

   defmodule DataProcessor do

     use GenServer


     def start_link(_) do

       GenServer.start_link(__MODULE__, [], name: __MODULE__)

     end


     def init(_) do

       {:ok, []}

     end


     def handle_info({:process, data}, state) do

       IO.puts("Procesando: #{data}")

       {:noreply, [data | state]}

     end

   end


   {:ok, processor_pid} = DataProcessor.start_link([])

   {:ok, collector_pid} = DataCollector.start_link(processor_pid)


   GenServer.cast(collector_pid, {:collect, "dato1"})

   GenServer.cast(collector_pid, {:collect, "dato2"})


DataCollector recopila datos y los envía a DataProcessor. DataProcessor procesa los datos recibidos y los guarda en su estado.


Como ventaja del modelo de actores tenemos:

Aislamiento Total: Los actores no comparten memoria, eliminando condiciones de carrera.

Escalabilidad: Los procesos son livianos y se ejecutan de manera concurrente.

Resiliencia: Si un actor falla, el sistema no se detiene; los supervisores pueden reiniciarlo.


El modelo de actores de Elixir proporciona una forma poderosa, segura y eficiente de manejar concurrencia. Al entender cómo implementar actores y supervisarlos, puedes construir sistemas robustos que escalen sin problemas.

sábado, 16 de noviembre de 2024

Primeros pasos en Pony


Pony es un lenguaje de programación orientado a objetos, actor-modelo, capacidades seguras. Es orientado a objetos porque tiene clases y objetos, como Python, Java, C++ y muchos otros lenguajes. Es actor-modelo porque tiene actores (similares a Erlang, Elixir o Akka). Estos se comportan como objetos, pero también pueden ejecutar código de forma asincrónica. Los actores hacen que Pony sea increíble.

Cuando decimos que Pony es seguro en cuanto a capacidades, nos referimos a algunas cosas:

  • Es seguro en cuanto a tipos. Realmente seguro en cuanto a tipos. 
  • Es seguro en cuanto a memoria. Esto viene con seguridad en cuanto a tipos, pero sigue siendo interesante. No hay punteros, ni desbordamientos de búfer, ni siquiera tiene el concepto de nulo.
  • Es seguro en cuanto a excepciones. No hay excepciones en tiempo de ejecución. Todas las “situaciones excepcionales” tienen una semántica definida y siempre se manejan.
  • No tiene carreras de datos. Pony no tiene bloqueos ni operaciones atómicas ni nada parecido. En cambio, el sistema de tipos garantiza en tiempo de compilación que su programa concurrente nunca pueda tener carreras de datos. Así que puede escribir código altamente concurrente y nunca equivocarse.
  • No tiene interbloqueos. Esto es fácil, Pony no tiene bloqueos en absoluto. Así que definitivamente no se bloquean, porque no existen.

Pony no puede impedir que se escriba errores lógicos, pero puede evitar que clases enteras de errores sean posibles. El compilador Pony evita que accedas a la memoria de forma insegura de forma concurrente. Si alguna vez has hecho programación concurrente, sabes lo difícil que puede ser rastrear este tipo de cosas. 

domingo, 10 de noviembre de 2024

Pony: Un Lenguaje de Programación basado en actores

 


En un mundo donde la concurrencia y el rendimiento son esenciales, el lenguaje de programación Pony emerge como una solución innovadora que permite escribir aplicaciones concurrentes de manera segura y eficiente. Pony combina paradigmas de actor y tipos de referencia para manejar múltiples tareas sin necesidad de bloqueos ni condiciones de carrera. 

Pony es un lenguaje de programación orientado a actores, diseñado para ofrecer concurrencia segura sin caer en los problemas típicos de bloqueos y condiciones de carrera. Fue creado por Sylvan Clebsch y otros investigadores de la Universidad de Cambridge y se enfoca en aplicaciones que requieren alta concurrencia y bajo tiempo de respuesta.

Algunas características clave de Pony incluyen:

- Concurrencia sin bloqueos: Pony utiliza un sistema de tipos de referencia que garantiza que el código concurrente no genere condiciones de carrera.

- Recolección de basura libre de pausas: Su recolección de basura está diseñada para ser concurrente y no requiere detener la ejecución de los programas.

- Orientación a actores: Al igual que Elixir y Erlang, Pony usa actores como unidades de concurrencia.

El sistema de tipos de referencia en Pony es lo que permite su concurrencia segura. Existen cinco tipos de referencia principales en Pony:

  • iso: Garantiza que solo hay una referencia a un valor.
  • rn: Representa una referencia transitoria que puede transferirse entre actores.
  • val: Un valor inmutable que puede ser compartido de manera segura.
  • ref: Una referencia mutable exclusiva de un actor.
  • box: Una referencia de solo lectura.

Al definir el tipo de referencia de cada variable, Pony asegura que los datos compartidos entre actores no puedan ser modificados de manera insegura.

Aquí tienes un ejemplo básico que demuestra el uso de actores en Pony. En este ejemplo, crearemos un actor llamado `Counter` que cuenta hasta un valor especificado y luego envía un mensaje de notificación.


actor Counter

  var count: U32

  var target: U32

  let notify: Notifier


  new create(target: U32, notify: Notifier) =>

    count = 0

    this.target = target

    this.notify = notify


  be increment() =>

    count = count + 1

    if count >= target then

      notify.done()

    end

end


actor Notifier

  be done() =>

    @println[I32]("Contador alcanzó el objetivo!".cstring())


En este código:

  1. El actor Counter tiene una función increment que aumenta el contador.
  2. Cuando el contador llega al objetivo, se llama al método done del actor Notifier.


Pony es un lenguaje único en el ecosistema de lenguajes concurrentes. Con su sistema de tipos de referencia y su enfoque en actores, Pony proporciona un marco robusto para aplicaciones concurrentes y distribuidas. Aunque es relativamente nuevo, su propuesta de concurrencia segura y recolección de basura eficiente lo convierten en una opción interesante para desarrolladores que buscan maximizar el rendimiento sin comprometer la seguridad.

Dejo link: https://www.ponylang.io/

sábado, 9 de noviembre de 2024

GraalVM + sistema operativo = GraalOS


GraalOS es una iniciativa experimental que integra la tecnología de GraalVM directamente en el sistema operativo, permitiendo que las aplicaciones, especialmente las desarrolladas en lenguajes JVM (Java, Scala, Kotlin), se ejecuten de manera más eficiente y directa sobre el hardware. GraalOS busca ser un sistema operativo minimalista y optimizado para ejecutar aplicaciones de alto rendimiento, proporcionando un entorno ideal para microservicios, procesamiento en la nube y aplicaciones en tiempo real.

Las principales características de GraalOS son: 

  1. Soporte Nativo para Lenguajes JVM: GraalOS permite ejecutar código de JVM directamente sobre el sistema operativo sin capas intermedias, ofreciendo un rendimiento nativo para lenguajes como Java, Kotlin y Scala.
  2. Integración con GraalVM: GraalOS está construido sobre la base de GraalVM, lo que permite la compilación AOT (Ahead-of-Time) y el uso de `native-image` para generar binarios nativos que corren eficientemente sobre el hardware.
  3. Ecosistema Multilenguaje: Aunque está optimizado para lenguajes de la JVM, GraalOS también soporta otros lenguajes como JavaScript, Python y R, aprovechando la compatibilidad de GraalVM.
  4. Optimización para Microservicios: GraalOS está diseñado para ejecutarse en contenedores ligeros, ideales para arquitecturas de microservicios y entornos de computación en la nube.

Uno de los puntos fuertes de GraalOS es el uso de la tecnología de compilación Ahead-of-Time (AOT) de GraalVM. La compilación AOT permite que el código de JVM se convierta en código nativo, lo cual mejora significativamente el tiempo de inicio y reduce el uso de memoria.

native-image -jar tu_aplicacion.jar

Este comando convierte un archivo JAR en un binario nativo, optimizado y listo para ejecutarse en GraalOS. Los binarios nativos generados pueden arrancar casi instantáneamente y son ideales para aplicaciones que requieren respuesta en tiempo real.

GraalOS ofrece un entorno perfecto para el despliegue de aplicaciones en la nube gracias a su integración optimizada con GraalVM. Además, permite manejar aplicaciones en tiempo real gracias a su bajo tiempo de respuesta y consumo de recursos. Su diseño minimalista y eficiente hace que sea una opción atractiva para desarrolladores que busquen optimizar costos y rendimiento en entornos de microservicios o serverless.

Aunque GraalOS es experimental, se puede probar en entornos de contenedores o como un sistema operativo en máquinas virtuales para evaluar su rendimiento en aplicaciones específicas. Para comenzar, puedes instalar GraalOS en una máquina virtual y luego utilizar GraalVM para compilar y ejecutar aplicaciones.


apt update && apt install graalos


GraalOS representa un avance en la forma en que interactuamos con el hardware a nivel de sistema operativo para ejecutar aplicaciones nativas. Aunque en sus primeras etapas, su integración con GraalVM abre la puerta a nuevas oportunidades en la ejecución de aplicaciones de alto rendimiento y microservicios en la nube.

Con una promesa de rendimiento optimizado, tiempos de respuesta ultrarrápidos y soporte multilenguaje, GraalOS podría transformar la forma en que desarrollamos e implementamos aplicaciones nativas.

Dejo like : 

https://blogs.oracle.com/java/post/introducing-graalos

https://graal.cloud/

miércoles, 6 de noviembre de 2024

Supervisores y Árboles de Supervisión en Elixir


La concurrencia es uno de los puntos fuertes de Elixir, y el modelo de supervisión es fundamental para construir aplicaciones resilientes. 

El modelo de concurrencia en Elixir está basado en procesos ligeros y aislados que se comunican entre sí enviándose mensajes, siguiendo el paradigma del modelo de actor. Estos procesos no comparten memoria, lo que reduce los riesgos de condiciones de carrera y hace que el sistema sea más seguro.

Supervisores son procesos especiales en Elixir que gestionan y supervisan otros procesos, reiniciándolos si fallan. Esto asegura que la aplicación siga funcionando incluso cuando ocurren errores.

Elixir provee módulos como Supervisor y Task.Supervisor para gestionar procesos de manera eficiente. Vamos a crear un simple supervisor que inicie y supervise un proceso Worker.


Primero, definimos el proceso Worker usando GenServer:


defmodule Worker do

  use GenServer


  def start_link(initial_state) do

    GenServer.start_link(__MODULE__, initial_state, name: __MODULE__)

  end


  def init(state), do: {:ok, state}


  def handle_cast(:do_work, state) do

    # Simulamos un trabajo que puede fallar

    if :rand.uniform() > 0.5 do

      {:stop, :error, state}

    else

      IO.puts("Trabajo completado exitosamente")

      {:noreply, state}

    end

  end

end


Ahora, creamos un supervisor que inicie y supervise nuestro proceso Worker:


defmodule MySupervisor do

  use Supervisor


  def start_link(_opts) do

    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)

  end


  def init(:ok) do

    children = [

      {Worker, :some_initial_state}

    ]


    Supervisor.init(children, strategy: :one_for_one)

  end

end


Elixir ofrece varias estrategias de supervisión que permiten diferentes enfoques de recuperación en caso de fallo:


  • :one_for_one: Solo reinicia el proceso que falló.
  • :one_for_all: Reinicia todos los procesos supervisados si uno falla.
  • :rest_for_one: Reinicia el proceso que falló y todos los procesos que fueron iniciados después de este.
  • :simple_one_for_one: Útil para supervisar un número dinámico de procesos que comparten el mismo tipo de inicialización.


En el ejemplo anterior, usamos :one_for_one, que reiniciará únicamente el proceso Worker si falla.

Los árboles de supervisión permiten organizar supervisores en una jerarquía. Esto es ideal para sistemas complejos que necesitan distintos niveles de supervisión.

Por ejemplo, podríamos tener un supervisor principal que supervise varios supervisores secundarios, cada uno a cargo de procesos específicos. Aquí hay una estructura básica:


defmodule MainSupervisor do

  use Supervisor


  def start_link(_opts) do

    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)

  end


  def init(:ok) do

    children = [

      MySupervisor, # Supervisor de workers

      AnotherSupervisor # Otro supervisor

    ]


    Supervisor.init(children, strategy: :one_for_all)

  end

end


En esta configuración:

  • MainSupervisor es el supervisor principal.
  • MySupervisor y AnotherSupervisor son supervisores secundarios.
  • Si cualquiera de los supervisores secundarios falla, MainSupervisor los reiniciará junto con sus respectivos procesos.


Los supervisores y árboles de supervisión en Elixir son una herramienta poderosa para construir sistemas concurrentes y resilientes. Siguiendo estos patrones, puedes crear aplicaciones que se recuperen automáticamente de fallos sin necesidad de intervención manual. Esto convierte a Elixir en una excelente opción para aplicaciones que requieren alta disponibilidad.

martes, 5 de noviembre de 2024

Pattern Matching en Typescript con ts-pattern


En TypeScript, a menudo necesitamos simplificar la lógica de condiciones, y aunque existen alternativas como if-else o switch, estas pueden volverse confusas en estructuras más complejas. Aquí es donde ts-pattern entra en juego, ofreciendo una forma poderosa y funcional de hacer pattern matching. Inspirado en técnicas de lenguajes como Haskell y Scala, ts-pattern permite coincidir patrones de datos y manejar casos de manera clara y estructurada.

ts-pattern es una biblioteca que lleva el pattern matching a TypeScript, haciéndolo flexible y adecuado para manejar varios tipos de datos y patrones complejos. Esto ayuda a escribir código más conciso y fácil de mantener, especialmente en proyectos grandes donde las condiciones exhaustivas son comunes.

Para instalar ts-pattern en tu proyecto, simplemente ejecuta:


npm install ts-pattern



Luego, impórtalo en tu archivo TypeScript:


import { match } from 'ts-pattern';


Veamos un ejemplo básico para entender cómo funciona match en strings:


const saludo = match('hola')

  .with('hola', () => '¡Hola Mundo!')

  .with('adiós', () => '¡Hasta luego!')

  .otherwise(() => 'Desconocido');

console.log(saludo);  // Salida: ¡Hola Mundo!


ts-pattern también permite manejar objetos anidados o arrays, facilitando el trabajo con estructuras más detalladas. Supongamos que tenemos un estado de carga y queremos manejarlo de manera exhaustiva:


type Estado = 'cargando' | 'éxito' | 'error';


const mensaje = match<Estado>('éxito')

  .with('cargando', () => 'Cargando...')

  .with('éxito', () => '¡Datos cargados!')

  .with('error', () => 'Hubo un problema.')

  .exhaustive();

console.log(mensaje);  // Salida: ¡Datos cargados!


Con .exhaustive(), ts-pattern se asegura de que todos los posibles valores de `Estado` están cubiertos, ayudándote a evitar errores futuros.

ts-pattern simplifica el manejo de múltiples condiciones en TypeScript, mejorando la claridad y la mantenibilidad del código. 

Dejo link: https://github.com/gvergnaud/ts-pattern