Translate

martes, 11 de febrero de 2025

Concurrencia en Erlang parte 12


Una vez entendidos los enlaces y los monitores, queda otro problema por resolver. Utilicemos las siguientes funciones del módulo linkmon.erl:


start_critic() ->

    spawn(?MODULE, critic, []).


judge(Pid, Band, Album) ->

    Pid ! {self(), {Band, Album}},

    receive

        {Pid, Criticism} -> Criticism

    after 2000 ->

        timeout

    end.


critic() ->

    receive

        {From, {"Rage Against the Turing Machine", "Unit Testify"}} ->

            From ! {self(), "They are great!"};

        {From, {"System of a Downtime", "Memoize"}} ->

            From ! {self(), "They're not Johnny Crash but they're good."};

        {From, {"Johnny Crash", "The Token Ring of Fire"}} ->

            From ! {self(), "Simply incredible."};

        {From, {_Band, _Album}} ->

            From ! {self(), "They are terrible!"}

    end,

    critic().


Ahora vamos a fingir que vamos a las tiendas a comprar música. Hay algunos álbumes que suenan interesantes, pero nunca estamos del todo seguros. Decides llamar a tu amigo, el crítico.


1> c(linkmon).                         

{ok,linkmon}

2> Critic = linkmon:start_critic().

<0.47.0>

3> linkmon:judge(Critic, "Genesis", "The Lambda Lies Down on Broadway").

"They are terrible!"


Debido a una tormenta solar (estoy tratando de encontrar algo realista aquí), la conexión se interrumpe:


4> exit(Critic, solar_storm).

true

5> linkmon:judge(Critic, "Genesis", "A trick of the Tail Recursion").

timeout


Es molesto. Ya no podemos recibir críticas por los álbumes. Para mantener viva la crítica, escribiremos un proceso básico de "supervisión" cuya única función es reiniciarlo cuando deje de funcionar:


start_critic2() ->

    spawn(?MODULE, restarter, []).


restarter() ->

    process_flag(trap_exit, true),

    Pid = spawn_link(?MODULE, critic, []),

    receive

        {'EXIT', Pid, normal} -> % not a crash

            ok;

        {'EXIT', Pid, shutdown} -> % manual termination, not a crash

            ok;

        {'EXIT', Pid, _} ->

            restarter()

    end.


Aquí, el reiniciador será su propio proceso. A su vez, iniciará el proceso del crítico y, si alguna vez muere por una causa anormal, restarter/0 se repetirá y creará un nuevo crítico. Tenga en cuenta que agregué una cláusula para {'EXIT', Pid, ​​shutoff} como una forma de matar manualmente al crítico si alguna vez lo necesitamos.

El problema con nuestro enfoque es que no hay forma de encontrar el Pid del crítico y, por lo tanto, no podemos llamarlo para conocer su opinión. Una de las soluciones que Erlang tiene para resolver esto es dar nombres a los procesos.

El acto de dar un nombre a un proceso le permite reemplazar el pid impredecible por un átomo. Este átomo puede usarse exactamente como un Pid al enviar mensajes. Para darle un nombre a un proceso, se usa la función erlang:register/2. Si el proceso muere, perderá automáticamente su nombre o también puede usar unregister/1 para hacerlo manualmente. Puede obtener una lista de todos los procesos registrados con register/0 o una lista más detallada con el comando de shell regs(). Aquí podemos reescribir la función restarter/0 de la siguiente manera:

restarter() ->

    process_flag(trap_exit, true),

    Pid = spawn_link(?MODULE, critic, []),

    register(critic, Pid),

    receive

        {'EXIT', Pid, normal} -> % not a crash

            ok;

        {'EXIT', Pid, shutdown} -> % manual termination, not a crash

            ok;

        {'EXIT', Pid, _} ->

            restarter()

    end. 

Como puede ver, register/2 siempre le dará a nuestro crítico el nombre 'critic', sin importar cuál sea el Pid. Lo que debemos hacer es eliminar la necesidad de pasar un Pid desde las funciones de abstracción. Probemos esto:


judge2(Band, Album) ->

    critic ! {self(), {Band, Album}},

    Pid = whereis(critic),

    receive

        {Pid, Criticism} -> Criticism

    after 2000 ->

        timeout

    end.


Aquí, la línea Pid = whereis(critic) se utiliza para encontrar el identificador de proceso del crítico con el fin de realizar una comparación de patrones con él en la expresión de recepción. Queremos hacer una comparación con este pid, porque nos asegura que encontraremos el mensaje correcto (¡podría haber 500 de ellos en el buzón mientras hablamos!). Sin embargo, esto puede ser la fuente de un problema. El código anterior supone que el pid del crítico seguirá siendo el mismo entre las dos primeras líneas de la función. Sin embargo, es completamente plausible que suceda lo siguiente:


  1. critic ! Message

                        2. critic receives

                        3. critic replies

                        4. critic dies

  5. whereis fails

                        6. critic is restarted

  7. code crashes


O bien, también es una posibilidad:


  1. critic ! Message

                           2. critic receives

                           3. critic replies

                           4. critic dies

                           5. critic is restarted

  6. whereis picks up

     wrong pid

  7. message never matches


La posibilidad de que las cosas salgan mal en un proceso diferente puede hacer que salga mal otro si no hacemos las cosas bien. En este caso, el valor del átomo crítico se puede ver desde varios procesos. Esto se conoce como estado compartido. El problema aquí es que el valor del átomo crítico puede ser accedido y modificado por diferentes procesos prácticamente al mismo tiempo, lo que da como resultado información inconsistente y errores de software. El término común para este tipo de cosas es condición de carrera. Las condiciones de carrera son particularmente peligrosas porque dependen del momento en que ocurren los eventos. En casi todos los lenguajes concurrentes y paralelos que existen, este momento depende de factores impredecibles, como qué tan ocupado está el procesador, a dónde van los procesos y qué datos está procesando su programa.

Es posible que haya escuchado que Erlang generalmente no tiene condiciones de carrera ni interbloqueos y hace que el código paralelo sea seguro. Esto es cierto en muchas circunstancias, pero nunca suponga que su código es realmente tan seguro. Los procesos nombrados son solo un ejemplo de las múltiples formas en que el código paralelo puede salir mal.

Otros ejemplos incluyen el acceso a archivos en la computadora (para modificarlos), la actualización de los mismos registros de la base de datos desde muchos procesos diferentes, etc.

Afortunadamente para nosotros, es relativamente fácil corregir el código anterior si no asumimos que el proceso nombrado sigue siendo el mismo. En su lugar, utilizaremos referencias (creadas con make_ref()) como valores únicos para identificar mensajes. Necesitaremos reescribir la función critic/0 en critic2/0 y judge/3 en judge2/2:


judge2(Band, Album) ->

    Ref = make_ref(),

    critic ! {self(), Ref, {Band, Album}},

    receive

        {Ref, Criticism} -> Criticism

    after 2000 ->

        timeout

    end.


critic2() ->

    receive

        {From, Ref, {"Rage Against the Turing Machine", "Unit Testify"}} ->

            From ! {Ref, "They are great!"};

        {From, Ref, {"System of a Downtime", "Memoize"}} ->

            From ! {Ref, "They're not Johnny Crash but they're good."};

        {From, Ref, {"Johnny Crash", "The Token Ring of Fire"}} ->

            From ! {Ref, "Simply incredible."};

        {From, Ref, {_Band, _Album}} ->

            From ! {Ref, "They are terrible!"}

    end,

    critic2().


Y luego cambia restarter/0 para que se ajuste haciendo que genere critic2/0 en lugar de critic/0. Ahora las otras funciones deberían seguir funcionando bien. El usuario no notará ninguna diferencia. Bueno, la notará porque cambiamos el nombre de las funciones y cambiamos la cantidad de parámetros, pero no sabrá qué detalles de implementación se cambiaron y por qué fue importante. Todo lo que verá es que su código se simplificó y ya no necesita enviar un PID en las llamadas a funciones:


6> c(linkmon).

{ok,linkmon}

7> linkmon:start_critic2().

<0.55.0>

8> linkmon:judge2("The Doors", "Light my Firewall").

"They are terrible!"

9> exit(whereis(critic), kill).

true

10> linkmon:judge2("Rage Against the Turing Machine", "Unit Testify").     

"They are great!"


Y ahora, aunque eliminamos al crítico, uno nuevo volvió instantáneamente para resolver nuestros problemas. Esa es la utilidad de los procesos nombrados. Si hubiera intentado llamar a linkmon:judge/2 sin un proceso registrado, el operador ! dentro de la función habría arrojado un error de argumento incorrecto, lo que garantizaría que los procesos que dependen de los nombrados no puedan ejecutarse sin ellos.

Los átomos se pueden usar en una cantidad limitada (aunque alta). Nunca debería crear átomos dinámicos. Esto significa que los procesos nombrados deben reservarse para servicios importantes exclusivos de una instancia de la máquina virtual y procesos que deberían estar allí durante todo el tiempo que se ejecuta su aplicación.

Si necesita procesos nombrados pero son transitorios o no hay ninguno que pueda ser exclusivo de la máquina virtual, puede significar que deben representarse como un grupo. Vincularlos y reiniciarlos juntos si fallan puede ser la opción más sensata, en lugar de intentar usar nombres dinámicos.




lunes, 10 de febrero de 2025

Bloques de Texto en Java


Con la introducción de los Bloques de Texto en Java 13 y su estandarización en Java 15, la manipulación de cadenas multilínea se ha vuelto mucho más sencilla y legible. Esta característica permite definir textos sin necesidad de escapar caracteres especiales o concatenar múltiples líneas, lo que mejora la claridad del código.

Los bloques de texto son literales de cadena que pueden abarcar múltiples líneas, definidos usando tres comillas dobles ("""). Se utilizan para escribir fragmentos de texto extensos sin necesidad de concatenaciones o caracteres de escape innecesarios.

Antes de los bloques de texto, una cadena multilínea en Java debía escribirse así:

String json = "{\n" +

              "    \"nombre\": \"Juan\",\n" +

              "    \"edad\": 25\n" +

              "}";

Con los bloques de texto, el mismo código se simplifica de la siguiente manera:


String json = """

    {

        "nombre": "Juan",

        "edad": 25

    }

    """;


Los bloques de texto son especialmente útiles en consultas SQL y plantillas HTML. Por ejemplo:


String query = """

    SELECT * FROM usuarios

    WHERE edad > 18

    ORDER BY nombre;

    """;


Otro ejemplo con HTML:


String html = """

    <html>

        <body>

            <h1>Bienvenido</h1>

        </body>

    </html>

    """;

Java mantiene la indentación de los bloques de texto, pero puedes usar `stripIndent()` para eliminar espacios innecesarios.

Puedes utilizar formatted() para reemplazo de valores dinámicos dentro del bloque.

Los bloques de texto en Java ofrecen una solución elegante para manejar cadenas multilínea de forma clara y eficiente. Su uso simplifica la lectura y escritura de código, mejorando la productividad de los desarrolladores.


domingo, 9 de febrero de 2025

Interceptores en C#


Con la llegada de C# 12, una de las características más interesantes introducidas es la capacidad de interceptar llamadas a métodos mediante Interceptores. Esta funcionalidad permite modificar o analizar la ejecución de un método antes o después de su invocación, lo que resulta útil para aspectos como logging, validación, caching y manejo de excepciones.

Los interceptores son una característica que permite redirigir llamadas a métodos a una lógica personalizada en tiempo de compilación. Esto brinda un alto nivel de control sin necesidad de modificar el código fuente original.

Para implementar interceptores en C#, se usa el atributo [InterceptsLocation], que indica que un método debe reemplazar otro en una ubicación específica del código.

Supongamos que tenemos un método que realiza una operación matemática simple:


public static class MathOperations

{

    public static int Add(int a, int b) => a + b;

}


Podemos definir un interceptor que intercepte esta llamada y agregue una funcionalidad adicional, como logging:



using System.Runtime.CompilerServices;


public static class MathInterceptor

{

    [InterceptsLocation("MathOperations.cs", line: 5, column: 5)]

    public static int Add(int a, int b)

    {

        Console.WriteLine($"Interceptando llamada a Add({a}, {b})");

        return a + b;

    }

}


Para tener en cuenta: 

  • Los interceptores funcionan en tiempo de compilación y no en ejecución.
  • Solo pueden ser usados en métodos con ubicaciones explícitas dentro del código fuente.
  • Requieren compatibilidad con la infraestructura de compilación adecuada.


En mi opinión es una forma de programación por aspecto o se podria implementar con esta nueva feature. 


martes, 4 de febrero de 2025

Haciendo fácil el calculo de hashing en archivoa con java.security.MessageDigest


Java 12 introdujo una API que facilita el cálculo de resúmenes de archivos (hashing) de manera eficiente y sencilla mediante la clase java.security.MessageDigest. Esta API permite generar hashes de archivos utilizando algoritmos como SHA-256 o MD5 sin necesidad de manejar manualmente la lectura de bytes y el procesamiento del hash.

Un hash de archivo es un valor único derivado de su contenido, generado por una función hash criptográfica. Es ampliamente utilizado para:

  • Verificar la integridad de archivos.
  • Comparar grandes volúmenes de datos de manera eficiente.
  • Validar la autenticidad de descargas.

Java 12 simplificó el proceso de generación de hash de archivos mediante el uso de `MessageDigest` junto con `Files.newInputStream`.

Veamos un ejemplo de uso con SHA-256:


import java.io.IOException;

import java.nio.file.Files;

import java.nio.file.Path;

import java.security.DigestInputStream;

import java.security.MessageDigest;

import java.security.NoSuchAlgorithmException;

import java.util.HexFormat;


public class FileHashingExample {

    public static void main(String[] args) throws IOException, NoSuchAlgorithmException {

        Path filePath = Path.of("archivo.txt");

        

        String hash = calculateFileHash(filePath, "SHA-256");

        System.out.println("Hash SHA-256: " + hash);

    }

    

    public static String calculateFileHash(Path path, String algorithm) throws IOException, NoSuchAlgorithmException {

        MessageDigest digest = MessageDigest.getInstance(algorithm);

        

        try (DigestInputStream dis = new DigestInputStream(Files.newInputStream(path), digest)) {

            while (dis.read() != -1) { } // Leer completamente el archivo

        }

        

        byte[] hashBytes = digest.digest();

        return HexFormat.of().formatHex(hashBytes);

    }

}


Otra mejora en Java 12 es el método Files.mismatch, que permite comparar dos archivos y determinar la primera posición donde difieren. Esto es útil para verificaciones de integridad.


import java.nio.file.Files;

import java.nio.file.Path;

import java.io.IOException;


public class FileComparisonExample {

    public static void main(String[] args) throws IOException {

        Path file1 = Path.of("archivo1.txt");

        Path file2 = Path.of("archivo2.txt");

        

        long mismatch = Files.mismatch(file1, file2);

        

        if (mismatch == -1) {

            System.out.println("Los archivos son idénticos.");

        } else {

            System.out.println("Los archivos difieren en la posición: " + mismatch);

        }

    }

}


Esta API facilita el cálculo de hashes y la comparación de archivos, mejorando la eficiencia y seguridad del procesamiento de datos. Estas mejoras hacen que Java sea una opción aún más atractiva para tareas de integridad de archivos y validación criptográfica.


lunes, 3 de febrero de 2025

Concurrencia en Erlang parte 11


Los monitores son un tipo especial de enlace con dos diferencias:

  • son unidireccionales;
  • se pueden apilar.

Los monitores son lo que necesitas cuando un proceso quiere saber qué está pasando con un segundo proceso, pero ninguno de ellos es realmente vital para el otro.

Otra razón, como se mencionó anteriormente, es apilar las referencias. Ahora bien, esto puede parecer inútil a primera vista, pero es genial para escribir bibliotecas que necesitan saber qué está pasando con otros procesos.

Verás, los enlaces son más una construcción organizacional. Cuando diseñas la arquitectura de tu aplicación, determinas qué proceso hará qué trabajos y qué dependerá de qué. Algunos procesos supervisarán a otros, otros no podrían vivir sin un proceso gemelo, etc. Esta estructura suele ser algo fijo, conocido de antemano. Los enlaces son útiles para eso y no necesariamente deberían usarse fuera de ella.

Pero, ¿qué sucede si tienes 2 o 3 bibliotecas diferentes a las que llamas y todas necesitan saber si un proceso está activo o no? Si usaras enlaces para esto, rápidamente te encontrarías con un problema cada vez que necesitaras desvincular un proceso. Ahora bien, los enlaces no son apilables, por lo que en el momento en que desvinculas uno, los desvinculas a todos y arruinas todas las suposiciones realizadas por las otras bibliotecas. Eso es bastante malo. Por lo tanto, necesitas enlaces apilables, y los monitores son tu solución. Se pueden eliminar individualmente. Además, ser unidireccional es útil en las bibliotecas porque otros procesos no deberían tener que estar al tanto de dichas bibliotecas.

Entonces, ¿cómo se ve un monitor? Bastante fácil, configuremos uno. La función es erlang:monitor/2, donde el primer argumento es el proceso atom y el segundo es el pid:


1> erlang:monitor(process, spawn(fun() -> timer:sleep(500) end)).

#Ref<0.0.0.77>

2> flush().

Shell got {'DOWN',#Ref<0.0.0.77>,process,<0.63.0>,normal}

ok


Cada vez que un proceso que monitorizas deja de funcionar, recibirás un mensaje como este. El mensaje es {'DOWN', MonitorReference, process, Pid, ​​Reason}. La referencia está ahí para permitirte demostrar el proceso. Recuerda, los monitores son apilables, por lo que es posible dejar fuera de servicio más de uno. Las referencias te permiten rastrear cada uno de ellos de una manera única. También ten en cuenta que, al igual que con los enlaces, hay una función atómica para generar un proceso mientras lo monitorizas, spawn_monitor/1-3:


3> {Pid, Ref} = spawn_monitor(fun() -> receive _ -> exit(boom) end end).

{<0.73.0>,#Ref<0.0.0.100>}

4> erlang:demonitor(Ref).

true

5> Pid ! die.

die

6> flush().

ok


En este caso, demostramos el otro proceso antes de que se bloqueara y, por lo tanto, no teníamos rastros de que se hubiera detenido. La función demonitor/2 también existe y brinda un poco más de información. El segundo parámetro puede ser una lista de opciones. Solo existen dos, info y flush:


7> f().

ok

8> {Pid, Ref} = spawn_monitor(fun() -> receive _ -> exit(boom) end end). 

{<0.35.0>,#Ref<0.0.0.35>}

9> Pid ! die.

die

10> erlang:demonitor(Ref, [flush, info]).

false

11> flush().

ok


La opción info le indica si existía o no un monitor cuando intentó eliminarlo. Por eso la expresión 10 devolvió falso. El uso de flush como opción eliminará el mensaje DOWN del buzón si existía, lo que hará que flush() no encuentre nada en el buzón del proceso actual.




domingo, 2 de febrero de 2025

Procesamiento simultáneo de datos con Streams gracias a colectores de Teeing en Java


Java 12 introdujo una nueva funcionalidad en la API de Streams: el colector teeing, el cual permite combinar dos colectores en una sola operación y fusionar sus resultados en un solo valor. Esta característica proporciona una forma elegante y eficiente de realizar dos operaciones de recolección en paralelo sobre un mismo flujo de datos.

El colector teeing se encuentra en la clase Collectors y permite procesar un Stream<T> en dos colectores distintos. Luego, combina los resultados mediante una función de fusión.

El metodo sería el siguiente: 


public static <T, R1, R2, R> Collector<T, ?, R> teeing(

    Collector<? super T, ?, R1> downstream1,

    Collector<? super T, ?, R2> downstream2,

    BiFunction<? super R1, ? super R2, R> merger)


Los parametros son: 

- downstream1: Primer colector que procesará el flujo de datos.

- downstream2: Segundo colector que operará sobre el mismo flujo.

- merger: Función que combina los resultados de ambos colectores en un solo valor.


Supongamos que queremos calcular simultáneamente el promedio y el mínimo de una lista de números.


import java.util.List;

import java.util.stream.Collectors;


public class TeeingExample {

    public static void main(String[] args) {

        List<Integer> numbers = List.of(3, 5, 7, 2, 8, 10);

        

        var result = numbers.stream().collect(

            Collectors.teeing(

                Collectors.averagingDouble(i -> i),

                Collectors.minBy(Integer::compareTo),

                (average, min) -> "Promedio: " + average + ", Mínimo: " + min.orElseThrow()

            )

        );

        

        System.out.println(result);

    }

}

Y la salida sería: 

Promedio: 5.833333333333333, Mínimo: 2


El colector teeing en Java 12 proporciona una forma eficiente de combinar dos colectores en una sola operación de Stream, evitando iteraciones adicionales y haciendo el código más legible. Su versatilidad lo convierte en una herramienta valiosa para el procesamiento de datos en Java moderno.

sábado, 1 de febrero de 2025

Concurrencia en Erlang parte 10


Ahora volvamos a los links y procesos que mueren. La propagación de errores entre procesos se realiza a través de un proceso similar al paso de mensajes, pero con un tipo especial de mensaje llamado señales o signals. Las señales de salida son mensajes "secretos" que actúan automáticamente sobre los procesos, matándolos en la acción.

Ya he mencionado muchas veces que para ser confiable, una aplicación necesita poder matar y reiniciar un proceso rápidamente. En este momento, los enlaces están bien para hacer la parte de matar. Lo que falta es el reinicio.

Para reiniciar un proceso, necesitamos una forma de saber primero que murió. Esto se puede hacer agregando una capa sobre los enlaces con un concepto llamado procesos del sistema. Los procesos del sistema son básicamente procesos normales, excepto que pueden convertir señales de salida en mensajes normales. Esto se hace llamando a process_flag(trap_exit, true) en un proceso en ejecución. Nada dice tanto como un ejemplo, así que lo usaremos. Simplemente volveré a hacer el ejemplo de la cadena con un proceso del sistema al principio:


1> process_flag(trap_exit, true).

true

2> spawn_link(fun() -> linkmon:chain(3) end).

<0.49.0>

3> receive X -> X end.

{'EXIT',<0.49.0>,"chain dies here"}


Ahora las cosas se ponen interesantes. Volviendo a nuestros dibujos, lo que sucede ahora es más bien así:


[shell] == [3] == [2] == [1] == [0]

[shell] == [3] == [2] == [1] == *dead*

[shell] == [3] == [2] == *dead*

[shell] == [3] == *dead*

[shell] <-- {'EXIT,Pid,"chain dies here"} -- *dead*

[shell] <-- still alive!


Y este es el mecanismo que permite reiniciar rápidamente los procesos. Al escribir programas que utilizan procesos del sistema, es fácil crear un proceso cuyo único papel sea comprobar si algo muere y luego reiniciarlo cuando falle.

Por ahora, quiero volver a las funciones de excepción que vimos anteriormente y mostrar cómo se comportan en torno a los procesos que atrapan salidas. Primero, establezcamos las bases para experimentar sin un proceso del sistema. Mostraré sucesivamente los resultados de lanzamientos, errores y salidas no atrapados en procesos vecinos:

Exception source: spawn_link(fun() -> ok end)
Untrapped Result: - nothing -
Trapped Result{'EXIT', <0.61.0>, normal}
The process exited normally, without a problem. Note that this looks a bit like the result of catch exit(normal), except a PID is added to the tuple to know what processed failed.
Exception source: spawn_link(fun() -> exit(reason) end)
Untrapped Result** exception exit: reason
Trapped Result{'EXIT', <0.55.0>, reason}
The process has terminated for a custom reason. In this case, if there is no trapped exit, the process crashes. Otherwise, you get the above message.
Exception source: spawn_link(fun() -> exit(normal) end)
Untrapped Result: - nothing -
Trapped Result{'EXIT', <0.58.0>, normal}
This successfully emulates a process terminating normally. In some cases, you might want to kill a process as part of the normal flow of a program, without anything exceptional going on. This is the way to do it.
Exception source: spawn_link(fun() -> 1/0 end)
Untrapped ResultError in process <0.44.0> with exit value: {badarith, [{erlang, '/', [1,0]}]}
Trapped Result{'EXIT', <0.52.0>, {badarith, [{erlang, '/', [1,0]}]}}
The error ({badarith, Reason}) is never caught by a try ... catch block and bubbles up into an 'EXIT'. At this point, it behaves exactly the same as exit(reason) did, but with a stack trace giving more details about what happened.
Exception source: spawn_link(fun() -> erlang:error(reason) end)
Untrapped ResultError in process <0.47.0> with exit value: {reason, [{erlang, apply, 2}]}
Trapped Result{'EXIT', <0.74.0>, {reason, [{erlang, apply, 2}]}}
Pretty much the same as with 1/0. That's normal, erlang:error/1 is meant to allow you to do just that.
Exception source: spawn_link(fun() -> throw(rocks) end)
Untrapped ResultError in process <0.51.0> with exit value: {{nocatch, rocks}, [{erlang, apply, 2}]}
Trapped Result{'EXIT', <0.79.0>, {{nocatch, rocks}, [{erlang, apply, 2}]}}
Because the throw is never caught by a try ... catch, it bubbles up into an error, which in turn bubbles up into an EXIT. Without trapping exit, the process fails. Otherwise it deals with it fine.


Y eso es todo en lo que respecta a las excepciones habituales. Las cosas son normales: todo va bien. Suceden cosas excepcionales: los procesos mueren, se envían diferentes señales.

Luego está exit/2. Este es el equivalente a un arma en el proceso Erlang. Permite que un proceso mate a otro a distancia, de forma segura. Estas son algunas de las posibles llamadas:


Exception source: exit(self(), normal)
Untrapped Result** exception exit: normal
Trapped Result{'EXIT', <0.31.0>, normal}
When not trapping exits, exit(self(), normal) acts the same as exit(normal). Otherwise, you receive a message with the same format you would have had by listening to links from foreign processes dying.
Exception source: exit(spawn_link(fun() -> timer:sleep(50000) end), normal)
Untrapped Result: - nothing -
Trapped Result: - nothing -
This basically is a call to exit(Pid, normal). This command doesn't do anything useful, because a process can not be remotely killed with the reason normal as an argument.
Exception source: exit(spawn_link(fun() -> timer:sleep(50000) end), reason)
Untrapped Result** exception exit: reason
Trapped Result{'EXIT', <0.52.0>, reason}
This is the foreign process terminating for reason itself. Looks the same as if the foreign process called exit(reason) on itself.
Exception source: exit(spawn_link(fun() -> timer:sleep(50000) end), kill)
Untrapped Result** exception exit: killed
Trapped Result{'EXIT', <0.58.0>, killed}
Surprisingly, the message gets changed from the dying process to the spawner. The spawner now receives killed instead of kill. That's because kill is a special exit signal. More details on this later.
Exception source: exit(self(), kill)
Untrapped Result** exception exit: killed
Trapped Result** exception exit: killed
Oops, look at that. It seems like this one is actually impossible to trap. Let's check something.
Exception source: spawn_link(fun() -> exit(kill) end)
Untrapped Result** exception exit: killed
Trapped Result{'EXIT', <0.67.0>, kill}
Now that's getting confusing. When another process kills itself with exit(kill) and we don't trap exits, our own process dies with the reason killed. However, when we trap exits, things don't happen that way.


Aunque se pueden atrapar la mayoría de las razones de salida, hay situaciones en las que se puede querer asesinar brutalmente un proceso: tal vez uno de ellos esté atrapando salidas pero también está atascado en un bucle infinito, sin leer ningún mensaje. La razón de eliminación actúa como una señal especial que no se puede atrapar. Esto garantiza que cualquier proceso que se termine con ella realmente estará muerto. Por lo general, matar es un último recurso, cuando todo lo demás ha fallado.

Como la razón de eliminación nunca se puede atrapar, se debe cambiar a matar cuando otros procesos reciben el mensaje. Si no se cambiara de esa manera, todos los demás procesos vinculados a ella morirían a su vez por la misma razón de eliminación y, a su vez, matarían a sus vecinos, y así sucesivamente. Se produciría una cascada de muertes.

Esto también explica por qué exit(kill) parece muerto cuando se recibe de otro proceso vinculado (la señal se modifica para que no se produzca una cascada), pero sigue pareciendo muerto cuando se atrapa localmente.

Si todo esto le parece confuso, no se preocupe. Muchos programadores sienten lo mismo. Las señales de salida son un poco curiosas. Afortunadamente, no hay muchos más casos especiales que los descritos anteriormente. Una vez que los comprenda, podrá comprender la mayor parte de la gestión de errores concurrentes de Erlang sin problemas.


miércoles, 29 de enero de 2025

Expresiones Switch en Java


Con Java 12, Oracle introdujo una nueva funcionalidad en el lenguaje: las expresiones switch, como parte de una característica en fase de vista previa. Esta adición busca mejorar la legibilidad y reducir la verbosidad del código al trabajar con estructuras switch.

En versiones anteriores de Java, la estructura switch era exclusivamente una sentencia, lo que significa que no devolvía un valor. Con la introducción de las expresiones switch, ahora puedes usar switch como una expresión que devuelve un valor, simplificando significativamente códigos comunes y eliminando la necesidad de manejar variables auxiliares.


Vea,mos un ejemplo básico de expresión switch


En versiones anteriores de Java:


int day = 3;

String dayName;

switch (day) {

    case 1:

        dayName = "Lunes";

        break;

    case 2:

        dayName = "Martes";

        break;

    case 3:

        dayName = "Miércoles";

        break;

    default:

        dayName = "Día inválido";

        break;

}

System.out.println(dayName);


Con expresiones switch en Java 12:


int day = 3;

String dayName = switch (day) {

    case 1 -> "Lunes";

    case 2 -> "Martes";

    case 3 -> "Miércoles";

    default -> "Día inválido";

};

System.out.println(dayName);


La nueva sintaxis también admite bloques de código más complejos, usando llaves, veamos un ejemplo:


int number = 5;

String parity = switch (number % 2) {

    case 0 -> {

        System.out.println("Es un número par.");

        yield "Par";

    }

    case 1 -> {

        System.out.println("Es un número impar.");

        yield "Impar";

    }

    default -> throw new IllegalStateException("Valor inesperado: " + number % 2);

};

System.out.println("El número es " + parity);


Las expresiones switch introducidas en Java 12 y estabilizada en Java 14, representan un paso importante hacia la modernización del lenguaje, ofreciendo una sintaxis más concisa y fácil de usar. Si bien inicialmente estaban en fase de vista previa, su adopción completa en versiones posteriores las convierte en una herramienta esencial para los desarrolladores Java modernos.


Concurrencia en Erlang parte 9


Un enlace es un tipo específico de relación que se puede crear entre dos procesos. Cuando se establece esa relación y uno de los procesos muere debido a un error o salida inesperados, el otro proceso vinculado también muere.

Este es un concepto útil desde la perspectiva de no detener los errores lo antes posible: si el proceso que tiene un error se bloquea pero los que dependen de él no, entonces todos estos procesos dependientes tienen que lidiar con la desaparición de una dependencia. Dejar que mueran y luego reiniciar todo el grupo suele ser una alternativa aceptable. Los enlaces nos permiten hacer exactamente esto.

Para establecer un enlace entre dos procesos, Erlang tiene la función primitiva link/1, que toma un Pid como argumento. Cuando se llama, la función creará un enlace entre el proceso actual y el identificado por Pid. Para deshacerse de un enlace, usamos unlink/1. Cuando uno de los procesos vinculados se bloquea, se envía un tipo especial de mensaje, con información relativa a lo que sucedió. No se envía dicho mensaje si el proceso muere por causas naturales (léase: termina de ejecutar sus funciones). Primero veamos esta nueva función como parte de linkmon.erl:


myproc() ->

    timer:sleep(5000),

    exit(reason).


Si ejecutamos las siguiente función (y espera 5 segundos entre cada comando de generación), debería ver que el shell se bloquea por "razón" solo cuando se haya establecido un vínculo entre los dos procesos.


1> c(linkmon).

{ok,linkmon}

2> spawn(fun linkmon:myproc/0).

<0.52.0>

3> link(spawn(fun linkmon:myproc/0)).

true

** exception error: reason


No se puede capturar con un try... catch el mensaje de error como de costumbre. Se deben utilizar otros mecanismos para hacer esto. 

Es importante señalar que los enlaces se utilizan para establecer grupos más grandes de procesos que deberían morir todos juntos:


chain(0) ->

    receive

        _ -> ok

    after 2000 ->

        exit("chain dies here")

    end;

chain(N) ->

    Pid = spawn(fun() -> chain(N-1) end),

    link(Pid),

    receive

        _ -> ok

    end.


Esta función tomará un entero N, iniciará N procesos vinculados entre sí. Para poder pasar el argumento N-1 al siguiente proceso de "cadena" (que llama a spawn/1), envuelvo la llamada dentro de una función anónima para que ya no necesite argumentos. Llamar a spawn(?MODULE, chain, [N-1]) habría hecho un trabajo similar.

Aquí, tendremos muchos procesos vinculados entre sí, que morirán cuando cada uno de sus sucesores salga:


4> c(linkmon).               

{ok,linkmon}

5> link(spawn(linkmon, chain, [3])).

true

** exception error: "chain dies here"


Y como puedes ver, el shell recibe la señal de muerte de algún otro proceso. Aquí hay una representación dibujada de los procesos generados y los enlaces que se caen:


[shell] == [3] == [2] == [1] == [0]

[shell] == [3] == [2] == [1] == *dead*

[shell] == [3] == [2] == *dead*

[shell] == [3] == *dead*

[shell] == *dead*

*dead, error message shown*

[shell] <-- restarted


Después de que el proceso que ejecuta linkmon:chain(0) muere, el error se propaga a lo largo de la cadena de enlaces hasta que el proceso de shell muere por ello. El fallo podría haber ocurrido en cualquiera de los procesos enlazados; como los enlaces son bidireccionales, solo es necesario que uno de ellos muera para que los demás sigan su ejemplo.

Si deseamos matar otro proceso desde el shell, podemos utilizar la función exit/2, que se llama de esta manera: exit(Pid, Reason). 

Los enlaces no se pueden apilar. Si llama a link/1 15 veces para los mismos dos procesos, solo seguirá existiendo un enlace entre ellos y una sola llamada a unlink/1 será suficiente para borrarlo.

Es importante tener en cuenta que link(spawn(Function)) o link(spawn(M,F,A)) ocurren en más de un paso. En algunos casos, es posible que un proceso muera antes de que se haya establecido el enlace y luego provoque un comportamiento inesperado. Por este motivo, se ha añadido al lenguaje la función spawn_link/1-3. Esta función toma los mismos argumentos que spawn/1-3, crea un proceso y lo vincula como si link/1 hubiera estado allí, excepto que todo se realiza como una operación atómica (las operaciones se combinan como una sola, que puede fallar o tener éxito, pero nada más). Esto generalmente se considera más seguro y también ahorra un conjunto de paréntesis.

lunes, 27 de enero de 2025

Las características introducidas en las versiones de Java desde la 12 hasta la más reciente


Hace mucho que no pispeo las nuevas características de java, desde la 11 más o menos. Por lo tanto he resuelto hacer un post por cada nueva característica.

Java 12 (marzo 2019):

  •  Expresiones `switch` (vista previa): Permiten utilizar `switch` como una expresión, simplificando la sintaxis y reduciendo errores.
  •  API de Compilación de Resúmenes de Archivos: Facilita la generación de resúmenes hash para archivos y directorios.
  •  Colectores de Teeing: Introducción de un nuevo colector en la API de Streams que permite combinar dos colecciones en una.


Java 13 (septiembre 2019):

  •  Bloques de texto (vista previa): Permiten manejar cadenas de texto multilínea de manera más sencilla y legible.
  •  Mejoras en ZGC: El Garbage Collector Z se ha mejorado para devolver memoria al sistema operativo más eficientemente.


Java 14 (marzo 2020):

  •  Clases de registros (vista previa): Introducción de `record` para simplificar la creación de clases que son principalmente contenedores de datos.
  •  Coincidencia de patrones para `instanceof` (vista previa): Simplifica el uso de `instanceof` al permitir la asignación directa de la variable si la comprobación es exitosa.


Java 15 (septiembre 2020):

  •  Clases selladas (vista previa): Permiten restringir qué clases pueden heredar de una clase o implementar una interfaz, mejorando el control sobre la jerarquía de clases.
  •  Eliminación de Nashorn: El motor JavaScript Nashorn fue eliminado del JDK.


Java 16 (marzo 2021):

  •  Clases de registros: La funcionalidad de `record` se estabilizó, facilitando la creación de clases inmutables.
  •  API de Acceso a Memoria Externa (incubadora): Proporciona una forma segura y eficiente de acceder a memoria fuera del montón de Java.


Java 17 (septiembre 2021):

  •  Coincidencia de patrones para `switch` (vista previa): Extiende la coincidencia de patrones al `switch`, permitiendo casos basados en el tipo del argumento.
  •  Funciones de clase sellada: Las clases selladas se estabilizaron, ofreciendo un control más preciso sobre la herencia.


Java 18 (marzo 2022):

  •  API de Servidor Web Simple: Introduce una API para crear servidores web mínimos, útiles para pruebas y propósitos educativos.
  •  Mejoras en la API de Caracteres Unicode: Actualizaciones para soportar las últimas versiones del estándar Unicode.


Java 19 (septiembre 2022):

  •  API de Vectores (incubadora): Proporciona una API para operaciones vectoriales que pueden ser optimizadas en hardware compatible.
  •  Patrones de registros (vista previa): Extiende la coincidencia de patrones para trabajar con componentes de registros.


Java 20 (marzo 2023):

  •  Extensiones de la API de Memoria y Función Externa (vista previa): Mejoras en la API para interactuar con memoria y funciones externas de manera más segura y eficiente.
  •  Mejoras en la API de Vectores: Continúan las mejoras en la API de Vectores para un mejor rendimiento y soporte de hardware.


Java 21 (septiembre 2023):

  •  Clases sin nombre y métodos principales sin nombre (vista previa): Simplifica la creación de programas pequeños al eliminar la necesidad de clases y métodos principales explícitos.
  •  Mejoras en la coincidencia de patrones: Ampliaciones adicionales para la coincidencia de patrones en diversas estructuras del lenguaje.


Este es el resumen. Vamos a ver como me va. Si quieren que haga un post para otra característica, escriban en los comentarios. 



domingo, 19 de enero de 2025

Diferencias entre Hilos y Procesos en Elixir


Elixir, gracias a la BEAM VM, utiliza procesos livianos que se diferencian fundamentalmente de los hilos tradicionales:  

1. Aislamiento Completo  

  • Los procesos en Elixir no comparten memoria. Esto evita condiciones de carrera, simplificando el manejo de la concurrencia.  
  • En contraste, los hilos suelen compartir memoria, requiriendo mecanismos como locks y semáforos para sincronización.  


2. Ligereza y Escalabilidad  

  • Cada proceso en Elixir consume muy pocos recursos, permitiendo que millones coexistan.  
  • Los hilos son más pesados y su número está limitado por el sistema operativo.  


3. Comunicación por Mensajes  

  • Los procesos en Elixir se comunican mediante mensajes asíncronos, usando send y receive.  
  • Los hilos comparten datos directamente, lo que puede complicar la concurrencia.  

4. Tolerancia a Fallos  

  • Elixir sigue la filosofía "Let it crash", donde los supervisores reinician procesos fallidos.  
  • Los hilos carecen de un sistema equivalente nativo de supervisión.  


Veamos un ejemplo rápido: 


spawn(fn -> receive do

  msg -> IO.puts("Mensaje recibido: #{msg}")

end end)

|> send("¡Hola, proceso!")


Elixir muestra cómo un enfoque basado en procesos simplifica y fortalece la concurrencia, ideal para sistemas distribuidos y resilientes.  


sábado, 18 de enero de 2025

El Operador |> de Elixir y sus equivalentes en otros lenguajes


En Elixir, el operador |> pasa el resultado de una expresión como el primer argumento de la siguiente función. Ya lo explicamos en el post anterior. 


" hello "

|> String.trim()

|> String.upcase()

Resultado: "HELLO"


Este diseño promueve una lectura fluida del código, eliminando la necesidad de paréntesis anidados.


F#, un lenguaje funcional inspirado en ML, también tiene un operador pipe |> con un propósito similar al de Elixir.


" hello "

|> String.trim

|> String.uppercase


El operador en F# permite que el flujo de datos sea explícito, facilitando la composición de funciones.


Python no tiene un operador pipe nativo, pero existen bibliotecas que lo emulan, como `pipe` o `toolz`. Sin embargo, sin bibliotecas adicionales, puedes lograr algo similar con reduce:


from functools import reduce


data = " hello "

result = reduce(lambda acc, fn: fn(acc), [str.strip, str.upper], data)

print(result)  # HELLO


Con una biblioteca como pipe:


from pipe import Pipe


result = " hello " | Pipe(str.strip) | Pipe(str.upper)

print(result)  # HELLO


JavaScript aún no tiene un operador pipe oficial, pero hay una propuesta en desarrollo en el comité TC39 (etapa 2 al momento de escribir). Con esta propuesta, el pipe se usa de la siguiente manera:


" hello "

  |> (x => x.trim())

  |> (x => x.toUpperCase());


Por ahora, puedes emularlo con funciones:


const pipeline = (...fns) => x => fns.reduce((v, f) => f(v), x);


const result = pipeline(

  x => x.trim(),

  x => x.toUpperCase()

)(" hello ");

console.log(result); // HELLO


Scala no tiene un operador pipe nativo, pero es posible definir uno:


implicit class PipeOps[T](val value: T) extends AnyVal {

  def |>[R](f: T => R): R = f(value)

}


val result = " hello "

  |> (_.trim)

  |> (_.toUpperCase)

println(result) // HELLO


En C#, aunque no existe un operador pipe, los métodos de extensión de LINQ se comportan de manera similar:


string result = " hello "

    .Trim()

    .ToUpper();

Console.WriteLine(result); // HELLO


El concepto detrás del operador pipe (`|>`) es universal: facilita la composición de funciones y mejora la legibilidad. Aunque su implementación varía entre lenguajes, su propósito sigue siendo el mismo: transformar datos paso a paso de manera clara y concisa.


viernes, 17 de enero de 2025

Concurrencia en Erlang parte 8


El concepto de 'flushing' permite implementar una recepción selectiva que puede dar prioridad a los mensajes que recibe mediante la anidación de llamadas:


important() ->

    receive

        {Priority, Message} when Priority > 10 ->

            [Message | important()]

    after 0 ->

        normal()

    end.


normal() ->

    receive

        {_, Message} ->

            [Message | normal()]

    after 0 ->

        []

    end.


Esta función creará una lista de todos los mensajes, comenzando primero con aquellos con una prioridad superior a 10:


1> c(multiproc).

{ok,multiproc}

2> self() ! {15, high}, self() ! {7, low}, self() ! {1, low}, self() ! {17, high}.       

{17,high}

3> multiproc:important().

[high,high,low,low]


Como utilicé el bit posterior a 0, se obtendrán todos los mensajes hasta que no quede ninguno, pero el proceso intentará capturar todos aquellos con una prioridad superior a 10 antes incluso de considerar los demás mensajes, que se acumulan en la llamada normal/0.

Si esta práctica le parece interesante, tenga en cuenta que a veces no es segura debido a la forma en que funcionan las recepciones selectivas en Erlang.

Cuando se envían mensajes a un proceso, se almacenan en el buzón hasta que el proceso los lee y coinciden con un patrón allí. Los mensajes se almacenan en el orden en que se recibieron. Esto significa que cada vez que coincide con un mensaje, comienza por el más antiguo.

Luego, ese mensaje más antiguo se prueba con cada patrón de la recepción hasta que uno de ellos coincide. Cuando lo hace, el mensaje se elimina del buzón y el código del proceso se ejecuta normalmente hasta la próxima recepción. Cuando se evalúa esta próxima recepción, la máquina virtual buscará el mensaje más antiguo que se encuentre actualmente en el buzón (el siguiente al que eliminamos), y así sucesivamente.

Cuando no hay forma de encontrar una coincidencia con un mensaje determinado, se lo coloca en una cola de guardado y se intenta con el siguiente mensaje. Si el segundo mensaje coincide, el primero se vuelve a colocar en la parte superior del buzón para volver a intentarlo más tarde.

Esto le permite preocuparse únicamente por los mensajes que son útiles. Ignorar algunos mensajes para manejarlos más tarde de la manera descrita anteriormente es la esencia de las recepciones selectivas. Si bien son útiles, el problema con ellas es que si su proceso tiene muchos mensajes que nunca le interesan, leer los mensajes útiles en realidad llevará cada vez más tiempo (y los procesos también crecerán en tamaño).

Imagine que queremos el mensaje n.° 367, pero los primeros 366 son basura ignorada por nuestro código. Para obtener el mensaje n.° 367, el proceso debe intentar hacer coincidir los 366 primeros. Una vez que haya terminado y todos se hayan colocado en la cola, se saca el mensaje n.° 367 y los primeros 366 se vuelven a colocar en la parte superior del buzón. El siguiente mensaje útil podría estar mucho más escondido y tardar aún más en encontrarlo.

Este tipo de recepción es una causa frecuente de problemas de rendimiento en Erlang. Si su aplicación se ejecuta con lentitud y sabe que hay muchos mensajes circulando, esta podría ser la causa.

Si estas recepciones selectivas están causando una ralentización masiva de su código, lo primero que debe hacer es preguntarse por qué recibe mensajes que no desea. ¿Se envían los mensajes a los procesos correctos? ¿Son correctos los patrones? ¿Los mensajes tienen un formato incorrecto? ¿Está utilizando un proceso cuando debería haber muchos? Responder a una o varias de estas preguntas podría resolver su problema.

Debido a los riesgos de que los mensajes inútiles contaminen el buzón de un proceso, los programadores de Erlang a veces toman una medida defensiva contra tales eventos. Una forma estándar de hacerlo podría ser la siguiente:


receive

    Pattern1 -> Expression1;

    Pattern2 -> Expression2;

    Pattern3 -> Expression3;

    ...

    PatternN -> ExpressionN;

    Unexpected ->

        io:format("unexpected message ~p~n", [Unexpected])

end.


Lo que esto hace es asegurarse de que cualquier mensaje coincida con al menos una cláusula. La variable Unexpected coincidirá con cualquier cosa, sacará el mensaje inesperado del buzón y mostrará una advertencia. Dependiendo de su aplicación, es posible que desee almacenar el mensaje en algún tipo de servicio de registro donde podrá encontrar información sobre él más adelante: si los mensajes van al proceso equivocado, sería una pena perderlos para siempre y tener dificultades para encontrar por qué ese otro proceso no recibe lo que debería.

En el caso de que necesite trabajar con una prioridad en sus mensajes y no pueda usar una cláusula de captura general, una forma más inteligente de hacerlo sería implementar un min-heap o usar el módulo gb_trees y volcar todos los mensajes recibidos en él (asegúrese de poner el número de prioridad primero en la clave para que se use para ordenar los mensajes). Luego, puede simplemente buscar el elemento más pequeño o más grande en la estructura de datos según sus necesidades.

En la mayoría de los casos, esta técnica debería permitirle recibir mensajes con una prioridad de manera más eficiente que las recepciones selectivas. Sin embargo, podría ralentizarlo si la mayoría de los mensajes que recibe tienen la máxima prioridad posible. Como siempre, el truco es perfilar y medir antes de optimizar.

Desde R14A, se agregó una nueva optimización al compilador de Erlang. Simplifica las recepciones selectivas en casos muy específicos de comunicaciones de ida y vuelta entre procesos. Un ejemplo de una función de este tipo es optimized/1 en multiproc.erl.

-module(multiproc).

-compile([export_all]).


sleep(T) ->

    receive

    after T -> ok

    end.


flush() ->

    receive

        _ -> flush()

    after 0 ->

        ok

    end.


important() ->

    receive

        {Priority, Message} when Priority > 10 ->

            [Message | important()]

    after 0 ->

        normal()

    end.


normal() ->

    receive

        {_, Message} ->

            [Message | normal()]

    after 0 ->

        []

    end.


%% optimized in R14A

optimized(Pid) ->

    Ref = make_ref(),

    Pid ! {self(), Ref, hello},

    receive

        {Pid, Ref, Msg} ->

            io:format("~p~n", [Msg])

    end.

Para que funcione, se debe crear una referencia (make_ref()) en una función y luego enviarla en un mensaje. En la misma función, se realiza una recepción selectiva. Si ningún mensaje puede coincidir a menos que contenga la misma referencia, el compilador se asegura automáticamente de que la máquina virtual omitirá los mensajes recibidos antes de la creación de esa referencia.

Tenga en cuenta que no debe intentar forzar su código para que se ajuste a dichas optimizaciones. Los desarrolladores de Erlang solo buscan patrones que se usan con frecuencia y luego los hacen más rápidos. Si escribe código idiomático, las optimizaciones deberían venir a usted. No al revés.

Con estos conceptos entendidos, el siguiente paso será realizar el manejo de errores con múltiples procesos.


martes, 14 de enero de 2025

El Poder del Operador |> en Elixir: Elegancia y Legibilidad


La programación funcional se centra en la composición de funciones para resolver problemas de manera clara y concisa. Uno de los operadores más representativos de este paradigma es el operador pipe |>, que permite encadenar llamadas a funciones de forma fluida y natural.

El operador |> (pipe) se utiliza para pasar el resultado de una expresión como el primer argumento de la siguiente función en la cadena.


value |> function1() |> function2()


Esto es equivalente a:


function2(function1(value))


Como ventajas podemos nombrar: 

  1. Legibilidad Mejorada: El flujo de datos se representa de forma secuencial, como si leyeras un proceso paso a paso.
  2. Eliminación de Paréntesis Anidados: Reduce la complejidad visual de funciones anidadas.
  3. Facilita el Refactoring: Reordenar o agregar pasos en el flujo es más sencillo.


Esto :

String.upcase(String.trim(" hola "))


Se puede escribir así:

" hola "

|> String.trim()

|> String.upcase()


Ambos códigos producen el mismo resultado: `"HOLA"`, pero la versión con |> es más legible.

El operador |> no se limita a funciones de la librería estándar; también puedes usarlo con tus propias funciones.


defmodule Math do

  def square(x), do: x * x

  def double(x), do: x * 2

end


5

|> Math.square()

|> Math.double()

Resultado: 50


Veamos ejemplos de uso : 


[1, 2, 3, 4]

|> Enum.map(&(&1 * 2))

|> Enum.filter(&(&1 > 4))

Resultado: [6, 8]


Otro ejemplo con estructuras más complejas: 


%{name: "John", age: 30}

|> Map.put(:country, "USA")

|> Map.update!(:age, &(&1 + 1))

Resultado: %{name: "John", age: 31, country: "USA"}


El operador `|>` es una herramienta fundamental en Elixir que no solo mejora la legibilidad del código, sino que también alienta un diseño funcional y modular. Al adoptarlo, puedes construir pipelines claros y efectivos que hagan que tu código sea más expresivo y fácil de mantener.