featured image

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!