29
Concurrent search in Elixir
Elixir on the BEAM — same actor model as Erlang, but with pipelines and Task.async_stream giving you a parallel-search idiom in a single line.
- Represent the graph as a map from vertex to a MapSet of neighbors. MapSet membership tests are O(log n) — fast enough that the visited-set check isn't your bottleneck.
defmodule Hamilton do def graph do %{ :a => MapSet.new([:b, :c, :d]), :b => MapSet.new([:a, :c]), :c => MapSet.new([:a, :b, :d]), :d => MapSet.new([:a, :c]) } end end - Backtracking helper. Pattern matching on the path length lets us split "still extending" from "trying to close the loop" into two function clauses with no branching.
def search(_g, start, [v | _] = path, n) when length(path) == n do if start in elem(get_neighbors(_g, v), 0), do: {:ok, [start | path]}, else: :none end def search(g, start, [v | _] = path, n) do g[v] |> Enum.reject(&(&1 in path)) |> Enum.find_value(:none, fn nxt -> case search(g, start, [nxt | path], n) do {:ok, full} -> {:ok, full} :none -> nil end end) end - Parallelize the first step with Task.async_stream. Each starting neighbor of the root gets its own lightweight process; the first one to find a circuit wins.
def parallel_hamilton(g, start) do n = map_size(g) g[start] |> Task.async_stream(fn nxt -> search(g, start, [nxt, start], n) end, max_concurrency: System.schedulers_online()) |> Enum.find_value(:none, fn {:ok, {:ok, p}} -> {:ok, p} _ -> nil end) end - Run it from iex. Elixir's piped expressions read top-to-bottom — find a starting move, kick off the parallel search, return the first hit.
iex> Hamilton.parallel_hamilton(Hamilton.graph(), :a) {:ok, [:a, :b, :c, :d, :a]}