Translate

Mostrando las entradas para la consulta erlang ordenadas por fecha. Ordenar por relevancia Mostrar todas las entradas
Mostrando las entradas para la consulta erlang ordenadas por fecha. Ordenar por relevancia Mostrar todas las entradas

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/

viernes, 1 de noviembre de 2024

Quicksort en F#


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

Ahora le toca a F#. 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:  


let rec quicksort list =

    match list with

    | [] -> []

    | pivot :: tail ->

        let lower = tail |> List.filter (fun x -> x <= pivot)

        let higher = tail |> List.filter (fun x -> x > pivot)

        quicksort lower @ [pivot] @ quicksort higher


lunes, 28 de octubre de 2024

Quicksort en Elixir


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 scalaerlangrusthaskell y lisp.

Ahora le toca a Elixir. 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 vacia o tiene un elemento, ya esta ordenada. 

Vamos al código: 


defmodule QuickSort do

  def sort([]), do: []

  def sort([pivot | tail]) do

    lower = Enum.filter(tail, &(&1 <= pivot))

    higher = Enum.filter(tail, &(&1 > pivot))


    sort(lower) ++ [pivot] ++ sort(higher)

  end

end


De Heathrow a Londres con Erlang


Estás en un avión que aterrizará en el aeropuerto de Heathrow en las próximas horas. Tienes que llegar a Londres lo más rápido posible; tu tío rico se está muriendo y quieres ser el primero en llegar para reclamar su herencia.

Hay dos carreteras que van de Heathrow a Londres y un montón de calles más pequeñas que las unen. Debido a los límites de velocidad y al tráfico habitual, algunas partes de las carreteras y calles más pequeñas requieren más tiempo que otras. Antes de aterrizar, decides maximizar tus posibilidades encontrando el camino óptimo hacia su casa. Aquí tienes el mapa que has encontrado en tu portátil:


Un pequeño mapa con una carretera principal 'A' con 4 segmentos de longitud 50, 5, 40 y 10, B con 4 segmentos de longitud 10, 90, 2 y 8, donde cada uno de estos segmentos está unido por caminos 'X' de longitud 30, 20, 25 y 0.

Para que te resulte más fácil trabajar con el mapa, ingresas los datos de la siguiente manera en un archivo llamado road.txt:


50

10

30

5

90

20

40

2

25

10

8

0


La carretera está trazada según el siguiente patrón: A1, B1, X1, A2, B2, X2, ..., An, Bn, Xn, donde X es una de las carreteras que une el lado A con el lado B del mapa. Insertamos un 0 como último segmento X, porque no importa lo que hagamos, ya estamos en nuestro destino. Los datos probablemente se puedan organizar en tuplas de 3 elementos (triples) de la forma {A,B,X}.

Al escribir una función recursiva, lo primero que hay que hacer es encontrar nuestro caso base. Para nuestro problema en cuestión, esto sería si tuviéramos solo una tupla para analizar, es decir, si solo tuviéramos que elegir entre A, B (y cruzar X, que en este caso es inútil porque estamos en el destino)

Entonces la elección es solo entre elegir cuál de los caminos A o B es el más corto. Si has aprendido bien la recursión, sabes que debemos intentar converger hacia el caso base. Esto significa que en cada paso que daremos, querremos reducir el problema a elegir entre A y B para el siguiente paso.

Extendamos nuestro mapa y comencemos de nuevo:


Camino A: 5, 10. Camino B: 1, 15. Camino de cruce X: 3.

¡Ah! ¡Se pone interesante! ¿Cómo podemos reducir la triple {5,1,3} a una elección estricta entre A y B? Veamos cuántas opciones son posibles para A. Para llegar a la intersección de A1 y A2 (lo llamaré el punto A1), puedo tomar la carretera A1 directamente (5), o venir desde B1 (1) y luego cruzar X1 (3). En este caso, la primera opción (5) es más larga que la segunda (4). Para la opción A, el camino más corto es [B, X]. Entonces, ¿cuáles son las opciones para B? Puedes proceder desde A1 (5) y luego cruzar X1 (3), o tomar estrictamente el camino B1 (1).

Lo que tenemos es una longitud 4 con el camino [B, X] hacia la primera intersección A y una longitud 1 con el camino [B] hacia la intersección de B1 y B2. Entonces tenemos que decidir qué elegir entre ir al segundo punto A (la intersección de A2 y el punto final o X2) y al segundo punto B (intersección de B2 y X2). Para tomar una decisión, sugiero que hagamos lo mismo que antes. 

Todos los caminos posibles a tomar en este caso se pueden encontrar de la misma manera que en el caso anterior. Podemos llegar al siguiente punto A tomando el camino A2 desde [B, X], que nos da una longitud de 14 (14 = 4 + 10), o tomando B2 y luego X2 desde [B], que nos da una longitud de 16 (16 = 1 + 15 + 0). En este caso, el camino [B, X, A] es mejor que [B, B, X].

El mismo dibujo que el anterior, pero con los caminos dibujados encima.

También podemos llegar al siguiente punto B tomando el camino A2 desde [B, X] y luego cruzando X2 por una longitud de 14 (14 = 4 + 10 + 0), o tomando la carretera B2 desde [B] por una longitud de 16 (16 = 1 + 15). Aquí, el mejor camino es elegir la primera opción, [B, X, A, X].

Entonces, cuando todo este proceso está hecho, nos quedan dos caminos, A o B, ambos de longitud 14. Cualquiera de ellos es el correcto. La última selección siempre tendrá dos caminos de la misma longitud, dado que el último segmento X tiene una longitud 0. Al resolver nuestro problema de forma recursiva, nos hemos asegurado de obtener siempre el camino más corto al final. No está mal, ¿verdad?

Teníamos listo el archivo que vamos a introducir como entrada. Para realizar manipulaciones de archivos, el módulo de archivos es nuestra mejor herramienta. Contiene muchas funciones comunes a muchos lenguajes de programación para trabajar con los archivos (establecer permisos, mover archivos, renombrarlos y eliminarlos, etc.)

También contiene las funciones habituales para leer y/o escribir desde archivos, como: file:open/2 y file:close/1 para hacer lo que dicen sus nombres (¡abrir y cerrar archivos!), file:read/2 para obtener el contenido de un archivo (ya sea como cadena o binario), file:read_line/1 para leer una sola línea, file:position/3 para mover el puntero de un archivo abierto a una posición determinada, etc.

También hay un montón de funciones de acceso directo, como file:read_file/1 (abre y lee el contenido como binario), file:consult/1 (abre y analiza un archivo como términos de Erlang) o file:pread/2 (cambia una posición y luego lee) y pwrite/2 (cambia la posición y escribe el contenido).

Con todas estas opciones disponibles, será fácil encontrar una función para leer nuestro archivo road.txt. Como sabemos que nuestra carretera es relativamente pequeña, llamaremos a file:read_file("road.txt").':


1> {ok, Binary} = file:read_file("road.txt").

{ok,<<"50\r\n10\r\n30\r\n5\r\n90\r\n20\r\n40\r\n2\r\n25\r\n10\r\n8\r\n0\r\n">>}

2> S = string:tokens(binary_to_list(Binary), "\r\n\t ").

["50","10","30","5","90","20","40","2","25","10","8","0"]


Tenga en cuenta que en este caso, agregué un espacio (" ") y una tabulación ("\t") a los tokens válidos, por lo que el archivo también podría haberse escrito en el formato "50 10 30 5 90 20 40 2 25 10 8 0". Dada esa lista, necesitaremos transformar las cadenas en números enteros. Usaremos un método similar al que usamos en nuestra calculadora RPN:

3> [list_to_integer(X) || X <- S].
[50,10,30,5,90,20,40,2,25,10,8,0]

Comencemos un nuevo módulo llamado road.erl y escribamos esta lógica:

-module(road).
-compile(export_all).

main() ->
    File = "road.txt",
    {ok, Bin} = file:read_file(File),
    parse_map(Bin).

parse_map(Bin) when is_binary(Bin) ->
    parse_map(binary_to_list(Bin));
parse_map(Str) when is_list(Str) ->
    [list_to_integer(X) || X <- string:tokens(Str,"\r\n\t ")].

La función main/0 es aquí la responsable de leer el contenido del archivo y pasarlo a parse_map/1. Debido a que utilizamos la función file:read_file/1 para obtener el contenido de road.txt, el resultado que obtenemos es un binario. Por este motivo, he hecho que la función parse_map/1 coincida tanto con listas como con binarios. En el caso de un binario, simplemente llamamos a la función nuevamente con la cadena convertida en una lista (nuestra función para dividir la cadena funciona solo con listas).

El siguiente paso para analizar el mapa sería reagrupar los datos en la forma {A,B,X} descrita anteriormente. Lamentablemente, no existe una forma genérica simple de extraer elementos de una lista de a 3 por vez, por lo que tendremos que hacer una coincidencia de patrones en una función recursiva para hacerlo:

group_vals([], Acc) ->
    lists:reverse(Acc);
group_vals([A,B,X|Rest], Acc) ->
    group_vals(Rest, [{A,B,X} | Acc]).

Esa función funciona de manera recursiva estándar; no hay nada demasiado complejo en juego aquí. Solo tendremos que llamarla modificando un poco parse_map/1:

parse_map(Bin) when is_binary(Bin) ->
    parse_map(binary_to_list(Bin));
parse_map(Str) when is_list(Str) ->
    Values = [list_to_integer(X) || X <- string:tokens(Str,"\r\n\t ")],
    group_vals(Values, []).

Si intentamos compilarlo todo, ahora deberíamos tener una carretera que tenga sentido:

1> c(road).
{ok,road}
2> road:main().
[{50,10,30},{5,90,20},{40,2,25},{10,8,0}]

Ah, sí, eso parece correcto. Obtenemos los bloques que necesitamos para escribir nuestra función que luego encajará en un pliegue. Para que esto funcione, es necesario encontrar un buen acumulador.

Para decidir qué usar como acumulador, el método que me parece más fácil de usar es imaginarme a mí mismo en medio del algoritmo mientras se ejecuta. Para este problema específico, imaginaré que actualmente estoy tratando de encontrar el camino más corto del segundo triple ({5,90,20}). Para decidir cuál es el mejor camino, necesito tener el resultado del triple anterior. Afortunadamente, sabemos cómo hacerlo, porque no necesitamos un acumulador y ya tenemos toda esa lógica. Entonces, para A:




Y tomemos el más corto de estos dos caminos. Para B, fue similar:





Ahora sabemos que el mejor camino actual que viene de A es [B, X]. También sabemos que tiene una longitud de 40. Para B, el camino es simplemente [B] y la longitud es 10. Podemos usar esta información para encontrar los siguientes mejores caminos para A y B volviendo a aplicar la misma lógica, pero contando los anteriores en la expresión. El otro dato que necesitamos es el camino recorrido para poder mostrárselo al usuario. Dado que necesitamos dos caminos (uno para A y otro para B) y dos longitudes acumuladas, nuestro acumulador puede tomar la forma {{DistanciaA, CaminoA}, {DistanciaB, CaminoB}}. De esa manera, cada iteración del pliegue tiene acceso a todo el estado y lo construimos para mostrárselo al usuario al final.

Esto nos da todos los parámetros que necesitará nuestra función: los triples {A,B,X} y un acumulador de la forma {{DistanciaA,RutaA}, {DistanciaB,RutaB}}.

Para obtener nuestro acumulador, podemos introducir esto en el código de la siguiente manera:

shortest_step({A,B,X}, {{DistA,PathA}, {DistB,PathB}}) ->
    OptA1 = {DistA + A, [{a,A}|PathA]},
    OptA2 = {DistB + B + X, [{x,X}, {b,B}|PathB]},
    OptB1 = {DistB + B, [{b,B}|PathB]},
    OptB2 = {DistA + A + X, [{x,X}, {a,A}|PathA]},
    {erlang:min(OptA1, OptA2), erlang:min(OptB1, OptB2)}.

Aquí, OptA1 obtiene la primera opción para A (pasando por A), OptA2 la segunda (pasando por B y luego por X). Las variables OptB1 y OptB2 reciben un tratamiento similar para el punto B. Finalmente, devolvemos el acumulador con las rutas obtenidas.

Sobre las rutas guardadas en el código anterior, tenga en cuenta que decidí usar la forma [{x, X}] en lugar de [x] por la sencilla razón de que podría ser bueno para el usuario saber la longitud de cada segmento. La otra cosa que estoy haciendo es que estoy acumulando las rutas hacia atrás ({x, X} viene antes de {b, B}). Esto se debe a que estamos en un pliegue, que es recursivo de cola: toda la lista se invierte, por lo que es necesario poner el último recorrido antes de los demás.

Finalmente, uso erlang:min/2 para encontrar la ruta más corta. Puede sonar extraño usar una función de comparación de este tipo en tuplas, pero recuerde que cada término de Erlang se puede comparar con cualquier otro. Como la longitud es el primer elemento de la tupla, podemos ordenarlos de esa manera.

Lo que queda por hacer es colocar esa función en un pliegue:

optimal_path(Map) ->
    {A,B} = lists:foldl(fun shortest_step/2, {{0,[]}, {0,[]}}, Map),
    {_Dist,Path} = if hd(element(2,A)) =/= {x,0} -> A;
                      hd(element(2,B)) =/= {x,0} -> B
                   end,
    lists:reverse(Path).

Al final del pliegue, ambas rutas deberían terminar teniendo la misma distancia, excepto que una pasa por el segmento final {x,0}. El if observa el último elemento visitado de ambas rutas y devuelve el que no pasa por {x,0}. Elegir la ruta con la menor cantidad de pasos (comparar con length/1) también funcionaría. Una vez que se ha seleccionado la más corta, se invierte (se construyó de manera recursiva de cola; debe invertirla). Luego puede mostrarla al mundo o mantenerla en secreto y obtener la herencia de su tío rico. Para hacer eso, debe modificar la función principal para llamar a optimal_path/1. Luego se puede compilar.

main() ->
    File = "road.txt",
    {ok, Bin} = file:read_file(File),
    optimal_path(parse_map(Bin)).

¡Mira! ¡Tenemos la respuesta correcta! ¡Buen trabajo!

1> c(road).
{ok,road}
2> road:main().
[{b,10},{x,30},{a,5},{x,20},{b,2},{b,8}]

O, para decirlo de forma visual:

El camino más corto, pasando por [b,x,a,x,b,b]
Pero, ¿sabes qué sería realmente útil? Poder ejecutar nuestro programa desde fuera del shell de Erlang. Tendremos que cambiar nuestra función principal nuevamente:

main([FileName]) ->
    {ok, Bin} = file:read_file(FileName),
    Map = parse_map(Bin),
    io:format("~p~n",[optimal_path(Map)]),
    erlang:halt().

La función principal ahora tiene una aridad de 1, necesaria para recibir parámetros desde la línea de comandos. También he añadido la función erlang:halt/0, que apagará la máquina virtual Erlang después de ser llamada. También he envuelto la llamada a optimal_path/1 en io:format/2 porque esa es la única forma de tener el texto visible fuera del shell de Erlang.

Con todo esto, tu archivo road.erl ahora debería verse así:

-module(road).
-compile(export_all).

main([FileName]) ->
    {ok, Bin} = file:read_file(FileName),
    Map = parse_map(Bin),
    io:format("~p~n",[optimal_path(Map)]),
    erlang:halt(0).

%% Transform a string into a readable map of triples
parse_map(Bin) when is_binary(Bin) ->
    parse_map(binary_to_list(Bin));
parse_map(Str) when is_list(Str) ->
    Values = [list_to_integer(X) || X <- string:tokens(Str,"\r\n\t ")],
    group_vals(Values, []).

group_vals([], Acc) ->
    lists:reverse(Acc);
group_vals([A,B,X|Rest], Acc) ->
    group_vals(Rest, [{A,B,X} | Acc]).

%% Picks the best of all paths, woo!
optimal_path(Map) ->
    {A,B} = lists:foldl(fun shortest_step/2, {{0,[]}, {0,[]}}, Map),
    {_Dist,Path} = if hd(element(2,A)) =/= {x,0} -> A;
                      hd(element(2,B)) =/= {x,0} -> B
                   end,
    lists:reverse(Path).

%% actual problem solving
%% change triples of the form {A,B,X}
%% where A,B,X are distances and a,b,x are possible paths
%% to the form {DistanceSum, PathList}.
shortest_step({A,B,X}, {{DistA,PathA}, {DistB,PathB}}) ->
    OptA1 = {DistA + A, [{a,A}|PathA]},
    OptA2 = {DistB + B + X, [{x,X}, {b,B}|PathB]},
    OptB1 = {DistB + B, [{b,B}|PathB]},
    OptB2 = {DistA + A + X, [{x,X}, {a,A}|PathA]},
    {erlang:min(OptA1, OptA2), erlang:min(OptB1, OptB2)}.

Y ejecutando el código:

$ erlc road.erl
$ erl -noshell -run road main road.txt
[{b,10},{x,30},{a,5},{x,20},{b,2},{b,8}]

¡Y sí, es correcto! Es prácticamente todo lo que necesitas hacer para que las cosas funcionen. Puedes crear un archivo bash/batch para encapsular la línea en un solo ejecutable, o puedes consultar el script para obtener resultados similares.

viernes, 18 de octubre de 2024

Respaldos externos de Gleam


import gleam/io


@external(erlang, "lists", "reverse")

pub fn reverse_list(items: List(e)) -> List(e) {

  tail_recursive_reverse(items, [])

}


fn tail_recursive_reverse(items: List(e), reversed: List(e)) -> List(e) {

  case items {

    [] -> reversed

    [first, ..rest] -> tail_recursive_reverse(rest, [first, ..reversed])

  }

}


pub fn main() {

  io.debug(reverse_list([1, 2, 3, 4, 5]))

  io.debug(reverse_list(["a", "b", "c", "d", "e"]))

}

Y el resultado es: 

[5, 4, 3, 2, 1]
["e", "d", "c", "b", "a"]

Es posible que una función tenga tanto una implementación de Gleam como una implementación externa. Si existe una implementación externa para el objetivo compilado actualmente, se utilizará; de lo contrario, se utilizará la implementación de Gleam.

Esto puede resultar útil si tiene una función que se puede implementar en Gleam, pero hay una implementación optimizada que se puede utilizar para un objetivo. Por ejemplo, la máquina virtual Erlang tiene una función de inversión de lista incorporada que se implementa en código nativo. El código aquí utiliza esta implementación cuando se ejecuta en Erlang, ya que está disponible en ese momento.

lunes, 14 de octubre de 2024

Implementaciones externas para varios destinos


import gleam/io


pub type DateTime


@external(erlang, "calendar", "local_time")

@external(javascript, "./my_package_ffi.mjs", "now")

pub fn now() -> DateTime


pub fn main() {

  io.debug(now())

}


Se pueden especificar varias implementaciones externas para la misma función, lo que permite que la función funcione tanto en Erlang como en JavaScript.

Si una función no tiene una implementación para el destino compilado actualmente, el compilador devolverá un error.

Se debe implementar funciones para todos los destinos, pero esto no siempre es posible debido a incompatibilidades en cómo funcionan la E/S y la concurrencia en Erlang y JavaScript. Con Erlang, la E/S simultánea se maneja de forma transparente por el entorno de ejecución, mientras que en JavaScript la E/S simultánea requiere el uso de promesas o devoluciones de llamadas. Si su código utiliza el estilo Erlang, normalmente no es posible implementarlo en JavaScript, mientras que si se utilizan devoluciones de llamadas, no será compatible con la mayoría del código Gleam y Erlang, ya que obliga a cualquier código que llame a la función a utilizar también devoluciones de llamadas.

Las bibliotecas que utilizan E/S simultáneas normalmente tendrán que decidir si admiten Erlang o JavaScript y documentar esto en su README.

miércoles, 9 de octubre de 2024

Externals en Gleam


import gleam/io


// A type with no Gleam constructors

pub type DateTime


// An external function that creates an instance of the type

@external(javascript, "./my_package_ffi.mjs", "now")

pub fn now() -> DateTime


// The `now` function in `./my_package_ffi.mjs` looks like this:

// export function now() {

//   return new Date();

// }


pub fn main() {

  io.debug(now())

}


El resultado es : 

//js(Date("2024-10-05T17:29:39.958Z"))


A veces, en nuestros proyectos, queremos usar código escrito en otros lenguajes, más comúnmente Erlang y JavaScript, dependiendo del entorno de ejecución que se esté usando. Las funciones externas y los tipos externos de Gleam nos permiten importar y usar este código que no es de Gleam.

Un tipo externo es aquel que no tiene constructores. Gleam no sabe qué forma tiene ni cómo crear uno, solo sabe que existe.

Una función externa es aquella que tiene el atributo @external, que indica al compilador que use la función del módulo especificado como implementación, en lugar del código de Gleam.

El compilador no puede determinar los tipos de funciones escritas en otros lenguajes, por lo que cuando se proporciona el atributo externo, se deben proporcionar anotaciones de tipo. Gleam confía en que el tipo proporcionado sea correcto, por lo que una anotación de tipo inexacta puede generar un comportamiento inesperado y fallas en el entorno de ejecución. ¡Ten cuidado!

Las funciones externas son útiles, pero se deben usar con moderación. Es preferible escribir código de Gleam cuando sea posible.

martes, 17 de septiembre de 2024

Calculadora de notación polaca inversa en Erlang


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

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

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

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

10 4 3 + 2 * -

10 (4 3 +) 2 * -

10 ((4 3 +) 2 *) -

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

(10 (7 2 *) -)

(10 14 -)

-4

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

Una pila que muestra los valores [3 4 10]

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

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

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

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

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

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

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

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

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

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

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

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

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


-module(calc).

-export([rpn/1]).

 

rpn(L) when is_list(L) -> 

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

    Res.


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

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


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


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


read(N) ->

case string:to_float(N) of

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

{F,_} -> F

end.


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

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


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

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


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


1> c(calc).

{ok,calc}

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

8

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

15

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

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

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

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

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


90 34 12 33 55 66 + * - +

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


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

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

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

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

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

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

sábado, 14 de septiembre de 2024

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


Con el titulo explique todo. Entonces programemos: 

 

-module(palindrome).

-export([is_palindrome/1]).


is_palindrome(Word) ->

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

    NormalizedWord == lists:reverse(NormalizedWord).


Y si lo probamos: 

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


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

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

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

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

Y si lo probamos: 

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

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


miércoles, 14 de agosto de 2024

Try ... catch en Erlang parte 2


Erlang tiene otra estructura de manejo de errores. Esa estructura se define como la palabra clave catch y básicamente captura todos los tipos de excepciones además de los buenos resultados. Es un poco extraña porque muestra una representación diferente de las excepciones:


1> catch throw(whoa).

whoa

2> catch exit(die).

{'EXIT',die}

3> catch 1/0.

{'EXIT',{badarith,[{erlang,'/',[1,0]},

                   {erl_eval,do_apply,5},

                   {erl_eval,expr,5},

                   {shell,exprs,6},

                   {shell,eval_exprs,6},

                   {shell,eval_loop,3}]}}

4> catch 2+2.

4


Lo que podemos ver de esto es que los lanzamientos siguen siendo los mismos, pero que las salidas y los errores se representan como {'EXIT', Reason}. Esto se debe a que los errores se incorporan al lenguaje después de las salidas (mantuvieron una representación similar para compatibilidad con versiones anteriores).

La forma de leer este seguimiento de pila es la siguiente:


5> catch doesnt:exist(a,4).              

{'EXIT',{undef,[{doesnt,exist,[a,4]},

                {erl_eval,do_apply,5},

                {erl_eval,expr,5},

                {shell,exprs,6},

                {shell,eval_exprs,6},

                {shell,eval_loop,3}]}}


El tipo de error es indefinido, lo que significa que la función que llamaste no está definida.

La lista que aparece justo después del tipo de error es un seguimiento de la pila

La tupla que está en la parte superior del seguimiento de la pila representa la última función que se llamó ({Módulo, Función, Argumentos}). Esa es tu función indefinida.

Las tuplas que siguen son las funciones llamadas antes del error. Esta vez tienen la forma {Módulo, Función, Aridad}.

Eso es todo lo que hay que hacer, en realidad.

También se puede obtener un seguimiento de la pila manualmente llamando a erlang:get_stacktrace/0 en el proceso que falló.

A menudo verás que catch está escrito de la siguiente manera:


catcher(X,Y) ->

    case catch X/Y of

        {'EXIT', {badarith,_}} -> "uh oh";

        N -> N

    end.


Y como era de esperar:


6> c(exceptions).

{ok,exceptions}

7> exceptions:catcher(3,3).

1.0

8> exceptions:catcher(6,3).

2.0

9> exceptions:catcher(6,0).

"uh oh"


Suena compacto y fácil de capturar excepciones, pero hay algunos problemas con catch. El primero de ellos es la precedencia de operadores:


10> X = catch 4+2.

* 1: syntax error before: 'catch'

10> X = (catch 4+2).

6


Esto no es exactamente intuitivo, dado que la mayoría de las expresiones no necesitan estar entre paréntesis de esta manera. Otro problema con catch es que no se puede ver la diferencia entre lo que parece ser la representación subyacente de una excepción y una excepción real:


11> catch erlang:boat().

{'EXIT',{undef,[{erlang,boat,[]},

                {erl_eval,do_apply,5},

                {erl_eval,expr,5},

                {shell,exprs,6},

                {shell,eval_exprs,6},

                {shell,eval_loop,3}]}}

12> catch exit({undef, [{erlang,boat,[]}, {erl_eval,do_apply,5}, {erl_eval,expr,5}, {shell,exprs,6}, {shell,eval_exprs,6}, {shell,eval_loop,3}]}). 

{'EXIT',{undef,[{erlang,boat,[]},

                {erl_eval,do_apply,5},

                {erl_eval,expr,5},

                {shell,exprs,6},

                {shell,eval_exprs,6},

                {shell,eval_loop,3}]}}


Y no puedes saber la diferencia entre un error y una salida real. También podrías haber usado throw/1 para generar la excepción anterior. De hecho, un throw/1 en un catch también podría ser problemático en otro escenario:


one_or_two(1) -> return;

one_or_two(2) -> throw(return).


Y ahora el problema mortal:


13> c(exceptions).

{ok,exceptions}

14> catch exceptions:one_or_two(1).

return

15> catch exceptions:one_or_two(2).

return


Como estamos detrás de un catch, nunca podemos saber si la función generó una excepción o si devolvió un valor real. Es posible que esto no suceda con mucha frecuencia en la práctica, pero sigue siendo un problema lo suficientemente grave como para justificar la incorporación de la construcción try...catch.


domingo, 11 de agosto de 2024

Try... catch en Erlang


Una expresión try... catch es una forma de evaluar una expresión y al mismo tiempo permitirle manejar tanto el caso exitoso como los errores encontrados. La sintaxis general para una expresión de este tipo es:

try Expression of

    SuccessfulPattern1 [Guards] ->

        Expression1;

    SuccessfulPattern2 [Guards] ->

        Expression2

catch

    TypeOfError:ExceptionPattern1 ->

        Expression3;

    TypeOfError:ExceptionPattern2 ->

        Expression4

end.


Se dice que la expresión entre try y of está protegida. Esto significa que cualquier tipo de excepción que ocurra dentro de esa llamada será capturada. Los patrones y expresiones entre try ... of y catch se comportan exactamente de la misma manera que un case ... of. Finalmente, la parte catch: aquí, puedes reemplazar TypeOfError por error, throw o exit. Si no se proporciona ningún tipo, se asume un throw. 

En primer lugar, comencemos un módulo llamado excepciones. Vamos a optar por algo simple:


-module(exceptions).

-compile(export_all).


throws(F) ->

    try F() of

        _ -> ok

    catch

        Throw -> {throw, caught, Throw}

    end.


Podemos compilarlo y probarlo con diferentes tipos de excepciones:


1> c(exceptions).

{ok,exceptions}

2> exceptions:throws(fun() -> throw(thrown) end).

{throw,caught,thrown}

3> exceptions:throws(fun() -> erlang:error(pang) end).

** exception error: pang


Este try... catch solo recibe Throw. Como se dijo anteriormente, esto se debe a que cuando no se menciona ningún tipo, se asume un Throw. Entonces tenemos funciones con cláusulas catch de cada tipo:


errors(F) ->

    try F() of

        _ -> ok

    catch

       error:Error -> {error, caught, Error}

    end.

 

exits(F) ->

    try F() of

        _ -> ok

    catch

         exit:Exit -> {exit, caught, Exit}

    end.


Y para probarlos:


4> c(exceptions).

{ok,exceptions}

5> exceptions:errors(fun() -> erlang:error("Die!") end).

{error,caught,"Die!"}

6> exceptions:exits(fun() -> exit(goodbye) end).

{exit,caught,goodbye}


El siguiente ejemplo del menú muestra cómo combinar todos los tipos de excepciones en un único try... catch. Primero declararemos una función para generar todas las excepciones que necesitamos:


sword(1) -> throw(slice);
sword(2) -> erlang:error(cut_arm);
sword(3) -> exit(cut_leg);
sword(4) -> throw(punch);
sword(5) -> exit(cross_bridge).

black_knight(Attack) when is_function(Attack, 0) ->
    try Attack() of
        _ -> "None shall pass."
    catch
        throw:slice -> "It is but a scratch.";
        error:cut_arm -> "I've had worse.";
        exit:cut_leg -> "Come on you pansy!";
        _:_ -> "Just a flesh wound."
    end.


Aquí is_function/2 es un BIF que garantiza que la variable Attack sea una función de aridad 0. Luego agregamos esto por si acaso:

talk() -> "blah blah".

Y ahora algo completamente diferente:


7> c(exceptions).
{ok,exceptions}
8> exceptions:talk().
"blah blah"
9> exceptions:black_knight(fun exceptions:talk/0).
"None shall pass."
10> exceptions:black_knight(fun() -> exceptions:sword(1) end).
"It is but a scratch."
11> exceptions:black_knight(fun() -> exceptions:sword(2) end).
"I've had worse."
12> exceptions:black_knight(fun() -> exceptions:sword(3) end).
"Come on you pansy!"
13> exceptions:black_knight(fun() -> exceptions:sword(4) end).
"Just a flesh wound."
14> exceptions:black_knight(fun() -> exceptions:sword(5) end).
"Just a flesh wound."


La expresión de la línea 9 demuestra el comportamiento normal, cuando la ejecución de la función se produce normalmente. Cada línea que sigue a esa demuestra la coincidencia de patrones en las excepciones según su clase (throw, error, exit) y la razón asociada a ellas (slice, cut_arm, cut_leg).

Una cosa que se muestra aquí en las expresiones 13 y 14 es una cláusula general para excepciones. El patrón _:_ es lo que necesita utilizar para asegurarse de capturar cualquier excepción de cualquier tipo. En la práctica, debe tener cuidado al utilizar los patrones generales: intente proteger su código de lo que puede controlar, pero no más que eso. Erlang tiene otras funciones para encargarse del resto.

También hay una cláusula adicional que se puede agregar después de un try ... catch que siempre se ejecutará. Esto es equivalente al bloque 'finally' en muchos otros lenguajes:


try Expr of
    Pattern -> Expr1
catch
    Type:Exception -> Expr2
after % this always gets executed
    Expr3
end


No importa si hay errores o no, se garantiza que las expresiones dentro de la parte after se ejecutarán. Sin embargo, no puede obtener ningún valor de retorno de la construcción after. Por lo tanto, after se usa principalmente para ejecutar código con efectos secundarios. El uso canónico de esto es cuando desea asegurarse de que un archivo que estaba leyendo se cierre independientemente de que se generen excepciones o no.

Ahora sabemos cómo manejar las 3 clases de excepciones en Erlang con bloques catch. Sin embargo, le he ocultado información: en realidad, es posible tener más de una expresión entre try y of!


whoa() ->
    try
        talk(),
        _Knight = "None shall Pass!",
        _Doubles = [N*2 || N <- lists:seq(1,100)],
        throw(up),
        _WillReturnThis = tequila
    of
        tequila -> "hey this worked!"
    catch
        Exception:Reason -> {caught, Exception, Reason}
    end.


Al llamar a exceptions:whoa(), obtendremos lo obvio {caught, throw, up}, debido a throw(up). Entonces, sí, es posible tener más de una expresión entre try y of...

Lo que acabo de resaltar en exceptions:whoa/0 y que quizás no hayas notado es que cuando usamos muchas expresiones de esa manera, es posible que no siempre nos importe cuál es el valor de retorno. La parte of se vuelve un poco inútil. Bueno, buenas noticias, puedes dejarla:

im_impressed() ->
    try
        talk(),
        _Knight = "None shall Pass!",
        _Doubles = [N*2 || N <- lists:seq(1,100)],
        throw(up),
        _WillReturnThis = tequila
    catch
        Exception:Reason -> {caught, Exception, Reason}
    end.

Es importante saber que la parte protegida de una excepción no puede ser recursiva de cola. La máquina virtual siempre debe mantener una referencia allí en caso de que aparezca una excepción.

Debido a que la construcción try ... catch sin la parte of no tiene nada más que una parte protegida, llamar a una función recursiva desde allí puede ser peligroso para programas que se supone que se ejecutarán durante mucho tiempo (que es el nicho de Erlang). Después de suficientes iteraciones, se quedará sin memoria o su programa se volverá más lento sin saber realmente por qué. Al colocar sus llamadas recursivas entre of y catch, no está en una parte protegida y se beneficiará de la optimización de última llamada.

Algunas personas usan try ... of ... catch en lugar de try ... catch de forma predeterminada para evitar errores inesperados de ese tipo, excepto para el código obviamente no recursivo con resultados que no serán utilizados por nada. ¡Lo más probable es que pueda tomar su propia decisión sobre qué hacer!

viernes, 9 de agosto de 2024

Generar excepciones en Erlang parte 3


Antes de seguir, por favor lee la parte 2. Y recorda que "Hay tres tipos de excepciones en Erlang: errors, throws y exits. Todas tienen diferentes usos :"

Y ahora vamos con throws. Un throws es una clase de excepción que se utiliza para los casos que se espera que el programador maneje. En comparación con exits y errors, en realidad no tienen ninguna intención de "bloquear ese proceso", sino que controlan el flujo. Como se utilizan excepciones mientras se espera que el programador las maneje, suele ser una buena idea documentar su uso dentro de un módulo que las utilice.

La sintaxis para generar una excepción es:

1> throw(permission_denied).

** exception throw: permission_denied

Donde puedes reemplazar permission_denied por cualquier cosa que quieras (incluso "todo está bien", pero eso no es útil y perderás amigos).

Los throws también se pueden usar para retornos no locales cuando se está en una recursión profunda. Un ejemplo de eso es el módulo ssl que usa throw/1 como una forma de enviar tuplas {error, Reason} de regreso a una función de nivel superior. Esta función simplemente devuelve esa tupla al usuario. Esto permite que el implementador solo escriba para los casos exitosos y tenga una función que se ocupe de las excepciones además de todo.

Otro ejemplo podría ser el módulo de matriz, donde hay una función de búsqueda que puede devolver un valor predeterminado proporcionado por el usuario si no puede encontrar el elemento necesario. Cuando no se puede encontrar el elemento, el valor predeterminado se lanza como una excepción y la función de nivel superior lo maneja y lo sustituye con el valor predeterminado proporcionado por el usuario. Esto evita que el programador del módulo tenga que pasar el valor predeterminado como parámetro de cada función del algoritmo de búsqueda, centrándose nuevamente solo en los casos exitosos.

Como regla general, intente limitar el uso de sus lanzamientos para retornos no locales a un solo módulo para facilitar la depuración de su código. También le permitirá cambiar las partes internas de su módulo sin requerir cambios en su interfaz.

lunes, 5 de agosto de 2024

Generar excepciones en Erlang parte 2


Antes de seguir, por favor lee la parte 1. Y recorda que "Hay tres tipos de excepciones en Erlang: errors, throws y exits. Todas tienen diferentes usos :"

Hay dos tipos de excepciones exits: salidas "internas" y salidas "externas". Las salidas internas se activan llamando a la función exit/1 y hacen que el proceso actual detenga su ejecución. Las salidas externas se llaman con exit/2 y tienen que ver con múltiples procesos en el aspecto concurrente de Erlang; como tal, nos centraremos principalmente en las salidas internas y visitaremos el tipo externo más adelante.

Las salidas internas son bastante similares a los errores. De hecho, históricamente hablando, eran lo mismo y solo existía exit/1. Tienen aproximadamente los mismos casos de uso. Entonces, ¿cómo elegir uno? Bueno, la elección no es obvia. Para entender cuándo usar uno u otro, no hay más remedio que comenzar a mirar los conceptos de actores y procesos desde lejos.

En la introducción, comparé los procesos con personas que se comunican por correo. Por ejemplo, un proceso 'A'  envia un mensaje a un proceso 'B' 

Aquí los procesos pueden enviarse mensajes entre sí. Un proceso también puede escuchar mensajes, esperarlos. También puede elegir qué mensajes escuchar, descartar algunos, ignorar otros, dejar de escuchar después de un tiempo determinado, etc.

Un proceso 'A' enviando 'hola' a un proceso 'B', que a su vez envía un mensaje a C con 'A dice hola!'

Estos conceptos básicos permiten a los implementadores de Erlang utilizar un tipo especial de mensaje para comunicar excepciones entre procesos. Actúan un poco como el último aliento de un proceso; se envían justo antes de que un proceso muera y el código que contiene deje de ejecutarse. Otros procesos que estaban escuchando ese tipo específico de mensaje pueden entonces saber acerca del evento y hacer lo que quieran con él. Esto incluye el registro, el reinicio del proceso que murió, etc.

Un proceso muerto que envía "Estoy muerto" a un proceso "B"

Una vez explicado este concepto, la diferencia entre usar erlang:error/1 y exit/1 es más fácil de entender. Si bien ambos se pueden usar de una manera extremadamente similar, la verdadera diferencia está en la intención. Luego, puede decidir si lo que tiene es "simplemente" un error o una condición que amerita matar el proceso actual. Este punto se fortalece por el hecho de que erlang:error/1 devuelve un seguimiento de la pila y exit/1 no. Si tuviera un seguimiento de la pila bastante grande o muchos argumentos para la función actual, copiar el mensaje de salida a cada proceso que escucha significaría copiar los datos. En algunos casos, esto podría volverse poco práctico.

viernes, 2 de agosto de 2024

Generar excepciones en Erlang

 


Al intentar supervisar la ejecución del código y protegerse contra errores lógicos, suele ser una buena idea provocar fallos en tiempo de ejecución para que los problemas se detecten de forma temprana.

Hay tres tipos de excepciones en Erlang: errors, throws y exits. Todas tienen diferentes usos :

Errores: Llamar a erlang:error(Reason) finalizará la ejecución en el proceso actual e incluirá un seguimiento de la pila de las últimas funciones llamadas con sus argumentos cuando lo detecte. Estos son los tipos de excepciones que provocan los errores en tiempo de ejecución.

Los errores son los medios que utiliza una función para detener su ejecución cuando no puede esperar que el código que la llama maneje lo que acaba de suceder. Si recibe un error if_clause, ¿qué puede hacer? Cambiar el código y volver a compilar, eso es lo que puede hacer (además de mostrar un bonito mensaje de error). 

También puedes definir tu propio tipo de errores:

1> erlang:error(badarith).

** exception error: bad argument in an arithmetic expression

2> erlang:error(custom_error).

** exception error: custom_error


Aquí, el shell Erlang no reconoce custom_error y no tiene una traducción personalizada como "argumento incorrecto en ...", pero se puede usar de la misma manera y el programador puede manejarlo de manera idéntica.

En post posteriores seguiremos con throws y exits.

lunes, 29 de julio de 2024

Run-time Errors en Erlang


Los errores de tiempo de ejecución son bastante destructivos en el sentido de que bloquean el código. Si bien Erlang tiene formas de lidiar con ellos, reconocerlos siempre es útil. Veamos unos ejemplos:


1> lists:sort([3,2,1]). 

[1,2,3]

2> lists:sort(fffffff). 

** exception error: no function clause matching lists:sort(fffffff)

        

Todas las cláusulas de protección de una función fallaron o ninguno de los patrones de las cláusulas de función coincidió.


3> case "Unexpected Value" of 

3>    expected_value -> ok;

3>    other_expected_value -> 'also ok'

3> end.

** exception error: no case clause matching "Unexpected Value"


¡Parece que alguien olvidó un patrón específico en su caso, envió el tipo de datos incorrecto o necesitaba una cláusula general!


4> if 2 > 4 -> ok;

4>    0 > 1 -> ok

4> end.

** exception error: no true branch found when evaluating an if expression


No puede encontrar una rama que evalúe como verdadera. Es posible que lo que necesite sea asegurarse de tener en cuenta todos los casos o agregar la cláusula true para todo.


5> [X,Y] = {4,5}.

** exception error: no match of right hand side value {4,5}


Los errores de coincidencia incorrecta ocurren siempre que falla la coincidencia de patrones. Esto probablemente significa que estás intentando hacer coincidencias de patrones imposibles (como las anteriores), intentando vincular una variable por segunda vez o simplemente cualquier cosa que no sea igual en ambos lados del operador = (que es básicamente lo que hace que la vinculación de una variable falle). Ten en cuenta que este error a veces ocurre porque el programador cree que una variable de la forma _MyVar es lo mismo que _. Las variables con un guión bajo son variables normales, excepto que el compilador no se quejará si no se usan. No es posible vincularlas más de una vez.


6> erlang:binary_to_list("heh, already a list").

** exception error: bad argument

     in function  binary_to_list/1

        called as binary_to_list("heh, already a list")

        

Trata sobre llamar a funciones con argumentos incorrectos. Este error generalmente lo genera el programador después de validar los argumentos desde dentro de la función, fuera de las cláusulas de protección. 


7> lists:random([1,2,3]).

** exception error: undefined function lists:random/1


Esto sucede cuando llamas a una función que no existe. Asegúrate de que la función se exporte desde el módulo con la aridad correcta (si la estás llamando desde fuera del módulo) y vuelve a verificar que hayas escrito correctamente el nombre de la función y el nombre del módulo. Otra razón para recibir el mensaje es cuando el módulo no está en la ruta de búsqueda de Erlang. De manera predeterminada, la ruta de búsqueda de Erlang está configurada para estar en el directorio actual. Puedes agregar rutas usando code:add_patha/1 o code:add_pathz/1. Si esto aún no funciona, ¡asegúrate de haber compilado el módulo para comenzar!


8> 5 + llama.

** exception error: bad argument in an arithmetic expression in operator  +/2 called as 5 + llama


Esto sucede cuando intentas realizar operaciones aritméticas que no existen, como divisiones por cero o entre átomos y números.


9> hhfuns:add(one,two).

** exception error: bad function one in function  hhfuns:add/2


La razón más frecuente por la que se produce este error es cuando se utilizan variables como funciones, pero el valor de la variable no es una función. En el ejemplo anterior, estoy utilizando la función hhfuns  y utilizando dos átomos como funciones. Esto no funciona y se genera badfun.


10> F = fun(_) -> ok end.

#Fun<erl_eval.6.13229925>

11> F(a,b).

** exception error: interpreted function with arity 1 called with two arguments



El error de badarity es un caso específico de badfun: ocurre cuando se utilizan funciones de orden superior, pero se les pasan más (o menos) argumentos de los que pueden manejar.
system_limit
Hay muchas razones por las que se puede generar un error system_limit: demasiados procesos (ya llegaremos a eso), átomos demasiado largos, demasiados argumentos en una función, cantidad de átomos demasiado grande, demasiados nodos conectados, etc. 


lunes, 22 de julio de 2024

Errores y excepciones en Erlang

Hay muchos tipos de errores: errores en tiempo de compilación, errores lógicos, errores en tiempo de ejecución y errores generados. Nos centraremos en los errores en tiempo de compilación en esta sección y revisaré los demás en las siguientes secciones.

Los errores en tiempo de compilación suelen ser errores sintácticos: debemos verificar los nombres de sus funciones, los tokens del lenguaje (corchetes, paréntesis, puntos, comas), la aridad de sus funciones, etc. Aquí hay una lista de algunos de los mensajes de error comunes en tiempo de compilación. y posibles soluciones en caso de que las encuentre:

module.beam: Module name 'madule' does not match file name 'module'

El nombre del módulo que ingresó en el atributo -module no coincide con el nombre del archivo.

./module.erl:2: Warning: function some_function/0 is unused

No has exportado una función, o el lugar donde se usa tiene el nombre o aridad incorrectos. También es posible que haya escrito una función que ya no es necesaria. ¡Comprueba tu código!

./module.erl:2: function some_function/1 undefined

La función no existe. Ha escrito el nombre o la aridad incorrectos en el atributo -export o al declarar la función. Este error también se genera cuando la función dada no se pudo compilar, generalmente debido a un error de sintaxis, como olvidar finalizar una función con un punto.

./module.erl:5: syntax error before: 'SomeCharacterOrWord'

Esto sucede por diversas razones, a saber, paréntesis no cerrados, tuplas o terminación de expresión incorrecta (como cerrar la última rama de un caso con una coma). Otras razones pueden incluir el uso de un átomo reservado en su código o caracteres Unicode que se convierten de manera extraña entre diferentes codificaciones (¡lo he visto suceder!)

./module.erl:5: syntax error before:

¡Muy bien, ese ciertamente no es tan descriptivo! Esto suele ocurrir cuando la terminación de su línea no es correcta. Este es un caso específico del error anterior, así que mantente atento.

./module.erl:5: Warning: this expression will fail with a 'badarith' exception

Erlang tiene que ver con la escritura dinámica, pero recuerde que los tipos son fuertes. En este caso, el compilador es lo suficientemente inteligente como para descubrir que una de sus expresiones aritméticas fallará (digamos, llama + 5). Sin embargo, no encontrará errores tipográficos mucho más complejos que eso.

./module.erl:5: Warning: variable 'Var' is unused

Declaraste una variable y nunca la usaste después. Esto podría ser un error en su código, así que verifique lo que ha escrito. De lo contrario, es posible que desee cambiar el nombre de la variable a _ o simplemente anteponerle un guión bajo (algo como _Var) si cree que el nombre ayuda a que el código sea legible.

./module.erl:5: Warning: a term is constructed, but never used

En una de tus funciones, estás haciendo algo como construir una lista, declarar una tupla o una función anónima sin vincularla a una variable ni devolverla. Esta advertencia te indica que estás haciendo algo inútil o que has cometido algún error.

./module.erl:5: head mismatch

Es posible que tu función tenga más de un encabezado y cada uno de ellos tenga una aridad diferente. No olvide que una aridad diferente significa funciones diferentes y no puede intercalar declaraciones de funciones de esa manera. Este error también aparece cuando inserta una definición de función entre las cláusulas principales de otra función.

./module.erl:5: Warning: this clause cannot match because a previous clause at line 4 always matches

Una función definida en el módulo tiene una cláusula específica definida después de una general. Como tal, el compilador puede advertirle que ni siquiera necesitará ir a la otra rama.

./module.erl:9: variable 'A' unsafe in 'case' (line 5)

Estás usando una variable declarada dentro de una de las ramas de un caso... o fuera de él. Esto se considera inseguro. Si desea utilizar dichas variables, sería mejor que hiciera MyVar = case... of...

Esto debería cubrir la mayoría de los errores que obtiene en tiempo de compilación en este momento. No hay demasiados y la mayoría de las veces la parte más difícil es encontrar qué error causó una enorme cascada de errores enumerados en otras funciones. Es mejor resolver los errores del compilador en el orden en que fueron reportados para evitar ser engañado por errores que en realidad pueden no ser errores.