A Bigger Example

Let’s rewrite our anagram code to use both tasks and an agent.

We’ll load words in parallel from a number of separate dictionaries. A separate task handles each dictionary. We’ll use an agent to store the resulting list of words and signatures.

tasks/anagrams.exs
 defmodule​ Dictionary ​do
 
  @name __MODULE__
 
 ##
 # External API
 def​ start_link,
 do​: Agent.start_link(​fn​ -> %{} ​end​, ​name:​ @name)
 
 def​ add_words(words),
 do​: Agent.update(@name, &do_add_words(&1, words))
 
 def​ anagrams_of(word),
 do​: Agent.get(@name, &Map.get(&1, signature_of(word)))
 
 ##
 # Internal implementation
 
 defp​ do_add_words(map, words),
 do​: Enum.reduce(words, map, &add_one_word(&1, &2))
 
 defp​ add_one_word(word, map),
 do​: Map.update(map, signature_of(word), [word], &[word|&1])
 
 defp​ signature_of(word),
 do​: word |> to_charlist |> Enum.sort |> to_string
 
 end
 
 defmodule​ WordlistLoader ​do
 def​ load_from_files(file_names) ​do
  file_names
  |> Stream.map(​fn​ name -> Task.async(​fn​ -> load_task(name) ​end​) ​end​)
  |> Enum.map(&Task.await/1)
 end
 
 defp​ load_task(file_name) ​do
  File.stream!(file_name, [], ​:line​)
  |> Enum.map(&String.trim/1)
  |> Dictionary.add_words
 end
 end

Our four wordlist files contain the following:

list1list2list3list4
 angor
 argon
 caret
 carte
 cater
 crate
 creat
 creta
 ester
 estre
 goran
 grano
 groan
 leapt
 nagor
 orang
 palet
 patel
 pelta
 petal
 pleat
 react
 recta
 reest
 rogan
 ronga
 steer
 stere
 stree
 terse
 tsere
 tepal

Let’s run it:

 $ iex anagrams.exs
 iex>​ Dictionary.start_link
 {:ok, #PID<0.66.0>}
 iex>​ Enum.map(1..4, &​"​​words/list​​#{​&1​}​​"​) |> WordlistLoader.load_from_files
 [:ok, :ok, :ok, :ok]
 iex>​ Dictionary.anagrams_of ​"​​organ"
 ["ronga", "rogan", "orang", "nagor", "groan", "grano", "goran",
  "argon", "angor"]

Making It Distributed

Agents and tasks run as OTP servers, so they are easy to distribute—just give our agent a globally accessible name. That’s a one-line change:

 @name {​:global​, __MODULE__}

Now we’ll load our code into two separate nodes and connect them. (Remember that we have to specify names for the nodes so they can talk.)

Window #1

 $ iex --sname one anagrams_dist.exs
 iex(one@FasterAir)>

Window #2

 $ iex --sname two anagrams_dist.exs
 iex(two@FasterAir)> Node.connect :one@FasterAir
 true
 iex(two@FasterAir)> Node.list
 [:one@FasterAir]

We’ll start the dictionary agent in node one—this is where the actual dictionary will end up. We’ll then load the dictionary using both nodes one and two:

Window #1

 iex(one@FasterAir)> Dictionary.start_link
 {:ok, #PID<0.68.0>}
 iex(one@FasterAir)> WordlistLoader.load_from_files(~w{words/list1 words/list2})
 [:ok, :ok]

Window #2

 iex(two@FasterAir)> WordlistLoader.load_from_files(~w{words/list3 words/list4})
 [:ok, :ok]

Finally, we’ll query the agent from both nodes:

Window #1

 iex(one@FasterAir)> Dictionary.anagrams_of "argon"
 ["ronga", "rogan", "orang", "nagor", "groan", "grano", "goran", "argon",
  "angor"]

Window #2

 iex(two@FasterAir)> Dictionary.anagrams_of "crate"
 ["recta", "react", "creta", "creat", "crate", "cater", "carte",
 "caret"]