El mes pasado hice algo de programación funcional usando Erlang y me quedó pendiente mostrar cómo se vería desde otro lenguaje con una sintaxis un poco más simpática como Elixir. Vamos a verlo.
El planteamiento inicial era con una lista de 10 elementos ponerlos a competir. Elixir nos brinda una construcción de rango que facilita la tarea de definir el código hecho previamente en Erlang... donde teníamos:
Competitors = lists:seq(1, 10),
lists:foldl(fun(NewCandidate, OldWinner) ->
case rand:uniform(2) of
1 -> NewCandidate;
2 -> OldWinner
end
end, hd(Competitors), tl(Competitors)).
Ahora tenemos:
Enum.reduce(2..10, 1, &(Enum.random([&1, &2])))
Iteramos sobre el rango 2..10
tomando el valor 1
como inicial y vamos reduciendo usando la closure pasada como tercer parámetro. El uso de los ampersand (&) es una facilidad que nos da Elixir para no tener que definir la closure completa.
Ahora vamos a ver el código original en Erlang que nos permitía hacer una lucha más justa entre los elementos de una lista:
Competitors = lists:seq(1,8),
Select = fun(A, B) ->
io:format("fight ~p vs ~p~n", [A, B]),
case rand:uniform(2) of
1 -> A;
2 -> B
end
end,
Fight = fun Fight([A,B]) ->
Select(A, B);
Fight(List) ->
{List1, List2} = lists:split(length(List) div 2, List),
ZipList = lists:zip(List1, List2),
Winners = [ Select(A, B) || {A, B} <- ZipList ],
io:format("winners => ~p~n", [Winners]),
Fight(Winners)
end,
Fight(Competitors).
En Elixir esto queda así:
defmodule Fight do
def select([a, b]) do
IO.puts("fight #{a} vs #{b}")
Enum.random([a, b])
end
def fight([a, b]), do: select([a, b])
def fight(list) do
list
|> Enum.chunk(list
|> Enum.to_list()
|> length()
|> div(2))
|> Enum.zip()
|> Enum.map(&(select(Tuple.to_list(&1))))
|> fight()
end
end
La ejecución de este último código una vez escribimos todo lo anterior en iex
es la siguiente:
> Fight.fight(1..8)
fight 1 vs 5
fight 2 vs 6
fight 3 vs 7
fight 4 vs 8
fight 1 vs 3
fight 6 vs 4
fight 1 vs 4
1
Ahora vamos a por nota. Vamos a ver el código en paralelo. En esto Elixir facilita mucho la gestión ya que nos permite trabajar con flujos (Stream
), vamos a ver el código original en Erlang:
SendWinner = fun(Parent, A, B) ->
case rand:uniform(2) of
1 -> Parent ! {winner, A};
2 -> Parent ! {winner, B}
end
end,
SplitList = fun(List) ->
M = length(List) div 2,
lists:split(M, List)
end,
SplitAndSelect = fun Solve(Parent, [A, B]) ->
io:format("fight ~p vs ~p~n", [A, B]),
SendWinner(Parent, A, B);
Solve(Parent, Elements) ->
{List1, List2} = SplitList(Elements),
spawn_link(fun() -> Solve(Parent, List1) end),
Solve(Parent, List2)
end,
Loop = fun Loop(Data) ->
receive
{list, Elements} ->
Parent = self(),
spawn_link(fun() ->
SplitAndSelect(Parent, Elements)
end),
Loop([]);
{winner, D} ->
Loop([D|Data])
after 500 ->
case length(Data) =:= 1 of
true ->
io:format("winner is ~p~n", Data);
false ->
io:format("winners are ~p~n", [Data]),
self() ! {list, Data},
Loop([])
end
end
end,
Elements = lists:seq(1, 8),
Init = fun() -> self() ! {list, Elements}, Loop([]) end.
Init().
Vale, ahora en Elixir:
defmodule Fight do
def select([a, b]) do
IO.puts("fight #{a} vs #{b}")
Enum.random([a, b])
end
def fight([a, b]), do: select([a, b])
def fight(list) do
list
|> Enum.chunk(list
|> Enum.to_list()
|> length()
|> div(2))
|> Enum.map(&(Task.async(fn -> fight(&1) end)))
|> Enum.map(&Task.await/1)
|> List.flatten()
|> fight()
end
end
Este método está basado en el comportamiento Task
que lanza sucesivos fight
hasta conseguir un único resultado. Como puede verse es un enfoque más simple que con Erlang. Seguimos partiendo la lista y pasamos cada parte a un nuevo proceso, una nueva tarea que vuelve a ejecutar la misma función. Así hasta que todas las posibilidades se han cubierto.
Ejecutando todo de nuevo en iex
podemos hacer:
> Fight.fight(1..8)
fight 1 vs 2
fight 3 vs 4
fight 5 vs 6
fight 7 vs 8
fight 1 vs 4
fight 6 vs 8
fight 4 vs 6
6
Vemos que no hay mucha diferencia entre la primera versión y la segunda. No obstante esta versión ejecuta cada toma de decisiones de cada enfrentamiento en un proceso paralelo gracias a Task
.
¿Te ha resultado más fácil que con Erlang? ¿Demasiadas cosas diferentes? ¿Podríamos optimizarlo? ¡Déjanos un comentario!