Translate

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

viernes, 13 de diciembre de 2024

Grafos dirigidos en Erlang


Los grafos dirigidos en Erlang se implementan como dos módulos, digraph y digraph_utils. El módulo digraph básicamente permite la construcción y modificación de un grafo dirigido: manipular aristas y vértices, encontrar caminos y ciclos, etc. Por otro lado, digraph_utils permite navegar por un grafo (postorder, preorder), probar ciclos, arborescencias o árboles, encontrar vecinos, etc.

En Erlang, los grafos dirigidos se implementan utilizando los módulos digraph y digraph_utils, que forman parte de la librería estándar. Aquí tienes ejemplos de cómo usarlos:

El módulo digraph permite crear y manipular grafos. Veamos un ejemplo básico:


-module(digraph_example).

-export([create_graph/0, add_nodes_and_edges/1, print_graph/1]).


create_graph() ->

    digraph:new().


add_nodes_and_edges(Graph) ->

    % Agregar nodos

    Node1 = digraph:add_vertex(Graph, node1, "Nodo 1"),

    Node2 = digraph:add_vertex(Graph, node2, "Nodo 2"),

    Node3 = digraph:add_vertex(Graph, node3, "Nodo 3"),

    

    % Agregar aristas dirigidas

    digraph:add_edge(Graph, Node1, Node2),

    digraph:add_edge(Graph, Node2, Node3),

    digraph:add_edge(Graph, Node1, Node3),

    Graph.


print_graph(Graph) ->

    Vertices = digraph:vertices(Graph),

    Edges = digraph:edges(Graph),

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

    io:format("Aristas: ~p~n", [Edges]).


Veamos como podemos usarlo: 

1> c(digraph_example).

2> Graph = digraph_example:create_graph().

3> Graph = digraph_example:add_nodes_and_edges(Graph).

4> digraph_example:print_graph(Graph).

Nodos: [#Ref<0.0.0.247>,#Ref<0.0.0.248>,#Ref<0.0.0.249>]

Aristas: [#Ref<0.0.0.250>,#Ref<0.0.0.251>,#Ref<0.0.0.252>]


El módulo digraph_utils provee funciones para analizar y transformar grafos, como encontrar caminos, ciclos, o calcular componentes conexas.


find_path_example() ->

    Graph = digraph:new(),

    Node1 = digraph:add_vertex(Graph, node1),

    Node2 = digraph:add_vertex(Graph, node2),

    Node3 = digraph:add_vertex(Graph, node3),

    digraph:add_edge(Graph, Node1, Node2),

    digraph:add_edge(Graph, Node2, Node3),

    Path = digraph_utils:path(Graph, Node1, Node3),

    io:format("Camino de Node1 a Node3: ~p~n", [Path]),

    digraph:delete(Graph).



1> c(digraph_example).

2> digraph_example:find_path_example().

Camino de Node1 a Node3: [node1,node2,node3]


Para detectar ciclos en un grafo dirigido:


detect_cycle_example() ->

    Graph = digraph:new(),

    Node1 = digraph:add_vertex(Graph, node1),

    Node2 = digraph:add_vertex(Graph, node2),

    digraph:add_edge(Graph, Node1, Node2),

    digraph:add_edge(Graph, Node2, Node1),

    Cycles = digraph_utils:strong_components(Graph),

    io:format("Ciclos detectados: ~p~n", [Cycles]),

    digraph:delete(Graph).


1> digraph_example:detect_cycle_example().

Ciclos detectados: [[node1,node2]]


digraph es eficiente para representar grafos con un gran número de nodos y aristas.

digraph_utils complementa digraph con funciones analíticas, como encontrar componentes fuertes o caminos mínimos.

Debido a que los grafos dirigidos están estrechamente relacionados con la teoría de conjuntos, el módulo sofs contiene algunas funciones que permiten convertir familias en dígrafos y dígrafos en familias.


martes, 10 de diciembre de 2024

Set en Erlang


Hay 4 módulos principales para tratar con conjuntos en Erlang. Esto es un poco raro al principio, pero tiene más sentido una vez que te das cuenta de que se debe a que los implementadores acordaron que no había una "mejor" manera de construir un conjunto. Los cuatro módulos son ordsets, sets, gb_sets y sofs (conjuntos de conjuntos):

ordsets

Los ordsets se implementan como una lista ordenada. Son principalmente útiles para conjuntos pequeños, son el tipo de conjunto más lento, pero tienen la representación más simple y legible de todos los conjuntos. Hay funciones estándar para ellos como ordsets:new/0, ordsets:is_element/2, ordsets:add_element/2, ordsets:del_element/2, ordsets:union/1, ordsets:intersection/1, y muchas más.

sets

Sets (el módulo) se implementa sobre una estructura muy similar a la que se usa en dict. Implementan la misma interfaz que ordsets, pero se escalarán mucho mejor. Al igual que los diccionarios, son especialmente buenos para manipulaciones de lectura intensiva, como verificar si algún elemento es parte del conjunto o no.

gb_sets

Los gb_sets en sí se construyen sobre una estructura de árbol general equilibrado similar a la que se usa en el módulo gb_trees. gb_sets es a los conjuntos lo que gb_tree es a dict; una implementación que es más rápida al considerar operaciones diferentes a la lectura, lo que le permite tener más control. Si bien gb_sets implementa la misma interfaz que sets y ordsets, también agrega más funciones. Al igual que gb_trees, tiene funciones inteligentes contra ingenuas, iteradores, acceso rápido a los valores más pequeños y más grandes, etc.

sofs

Los conjuntos de conjuntos (sofs) se implementan con listas ordenadas, pegadas dentro de una tupla con algunos metadatos. Son el módulo que se debe usar si desea tener control total sobre las relaciones entre conjuntos, familias, imponer tipos de conjuntos, etc. Son realmente lo que desea si necesita un concepto matemático en lugar de "solo" grupos de elementos únicos.


Si bien tal variedad puede verse como algo grandioso, algunos detalles de implementación pueden ser francamente frustrantes. Como ejemplo, gb_sets, ordsets y sofs usan el operador == para comparar valores: si tiene los números 2 y 2.0, ambos terminarán siendo vistos como el mismo.

Sin embargo, sets (el módulo) utiliza el operador =:=, lo que significa que no necesariamente puedes cambiar de implementación como desees. Hay casos en los que necesitas un comportamiento preciso y, en ese punto, puedes perder el beneficio de tener múltiples implementaciones.

Es un poco confuso tener tantas opciones disponibles. Björn Gustavsson, del equipo Erlang/OTP y programador de Wings3D, sugiere principalmente usar gb_sets en la mayoría de las circunstancias, usar ordset cuando necesitas una representación clara que quieres procesar con tu propio código y 'sets' cuando necesitas el operador =:= (fuente).

En cualquier caso, al igual que para los almacenes de valores clave, la mejor solución suele ser realizar una evaluación comparativa y ver qué se adapta mejor a tu aplicación.

jueves, 5 de diciembre de 2024

Matrices en erlang


Pero, ¿qué sucede con el código que requiere estructuras de datos con nada más que claves numéricas? Bueno, para eso, existen las matrices. Permiten acceder a elementos con índices numéricos y plegar toda la estructura, ignorando posiblemente las ranuras no definidas.

Las matrices de Erlang, al contrario de sus contrapartes imperativas, no pueden tener cosas como inserción o búsqueda en tiempo constante. Debido a que suelen ser más lentas que las de los lenguajes que admiten la asignación destructiva y que el estilo de programación realizado con Erlang no se presta necesariamente demasiado bien a las matrices y matrices, rara vez se utilizan en la práctica.

En general, los programadores de Erlang que necesitan realizar manipulaciones de matrices y otros usos que requieren matrices tienden a utilizar conceptos llamados Puertos para dejar que otros lenguajes hagan el trabajo pesado, o C-Nodos, Linked in drivers y NIF (Experimental, R13B03+).

Las matrices también son extrañas en el sentido de que son una de las pocas estructuras de datos que tienen un índice 0 (al contrario de las tuplas o listas), junto con el índice en el módulo de expresiones regulares.


Veamos un ejemplo: 


  %% Create a fixed-size array with entries 0-9 set to 'undefined'

  A0 = array:new(10).

  10 = array:size(A0).

 

  %% Create an extendible array and set entry 17 to 'true',

  %% causing the array to grow automatically

  A1 = array:set(17, true, array:new()).

  18 = array:size(A1).

 

  %% Read back a stored value

  true = array:get(17, A1).

 

  %% Accessing an unset entry returns the default value

  undefined = array:get(3, A1).

 

  %% Accessing an entry beyond the last set entry also returns the

  %% default value, if the array does not have fixed size

  undefined = array:get(18, A1).

 

  %% "sparse" functions ignore default-valued entries

  A2 = array:set(4, false, A1).

  [{4, false}, {17, true}] = array:sparse_to_orddict(A2).

 

  %% An extendible array can be made fixed-size later

  A3 = array:fix(A2).

 

  %% A fixed-size array does not grow automatically and does not

  %% allow accesses beyond the last set entry

  {'EXIT',{badarg,_}} = (catch array:set(18, true, A3)).

  {'EXIT',{badarg,_}} = (catch array:get(18, A3)).

miércoles, 27 de noviembre de 2024

Estructura Key-Value en Erlang


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

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

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

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

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

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

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

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

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

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

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

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

martes, 26 de noviembre de 2024

Registros en Erlang


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

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

-module(records).

-compile(export_all).


-record(robot, {name,

                type=industrial,

                hobbies,

                details=[]}).


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

first_robot() ->

    #robot{name="Mechatron",

           type=handmade, 

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


Y ejecutando el código:


1> c(records).

{ok,records}

2> records:first_robot().

{robot,"Mechatron",handmade,undefined,

       ["Moved by a small man inside"]}


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

3> rr(records).

[robot]

4> records:first_robot().         

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

       hobbies = undefined,

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


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

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

car_factory(CorpName) ->

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


Y ejecutalo:


5> c(records).

{ok,records}

6> records:car_factory("Jokeswagen").

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

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

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

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

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

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

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

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

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

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

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

       details = []}

6> Crusher#robot.hobbies.

["Crushing people","petting cats"]


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

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

#robot{name = undefined, type = industrial,

       hobbies = undefined,

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

                        hobbies = undefined,details = []}}

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

"erNest"


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


9> #robot.type.

3

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

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

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


%% use pattern matching to filter

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

    Name ++ " is allowed!";

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

    Name ++ " is not allowed".


%% can extend user without problem

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

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

    allowed;

adult_section(_) ->

    %% redirect to sesame street site

    forbidden.


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

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


repairman(Rob) ->

    Details = Rob#robot.details,

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

    {repaired, NewRob}.


Y luego:


16> c(records).

{ok,records}

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

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

                 hobbies = ["trying to have feelings"],

                 details = ["Repaired by repairman"]}}


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

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


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

-record(included, {some_field,

                   some_default = "yeah!",

                   unimaginative_name}).


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


-include("records.hrl").


Y luego la siguiente función para probarlo:


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


Ahora, pruébalo como siempre:


18> c(records).

{ok,records}

19> rr(records).

[included,robot,user]

20> records:included().

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

          unimaginative_name = undefined}


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

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

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.

lunes, 28 de octubre de 2024

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.

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.