Elixir 1.6

Chapter 22 

Tasks and Agents

We have looked at two extremes for processes:

  • spawn primitive (tiny)
  • OTP (huge)

 

What if we want something more in the middle?

 

We are going to talk about:

  • Tasks
  • Agents

Tasks

An Elixir task is a function that runs in the background.

defmodule Fib do
  def of(0), do: 0
  def of(1), do: 1
  def of(n), do: Fib.of(n-1) + Fib.of(n-2)
end

IO.puts "Start the task"
worker = Task.async(fn -> Fib.of(20) end)
IO.puts "Do something else"
# ...
IO.puts "Wait for the task"
result = Task.await(worker)

IO.puts "The result is #{result}"

Task.async creates a separate process that runs the given function. The return value of async is a task descriptor. To get the function’s value, it calls Task.await

Tasks

$ ​​elixir​​ ​​tasks1.exs​
Start the task
Do something else
Wait for the task
The result is 6765
worker = Task.async(Fib, :of, [20])
result = Task.await(worker)
IO.puts "The result is #{result}"

We can also pass Task.async the name of a module and function, along with any arguments.

When we run the code:

Tasks and Supervision

Tasks are implemented as OTP servers, which means we can add them to our application’s
supervision tree in two ways.

  1. Call start_link instead of async
  2. Run them directly from a supervisor
# 2 directly in supervisor
children = [
  { Task, ​fn​ -> do_something_extraordinary() ​end​ }
]
Supervisor.start_link(children, ​strategy:​ ​:one_for_one​)

1st way: If the function running in the task crashes and we use start_link, our process will be terminated immediately. If we use async, our process will be terminated only when we subsequently call await.

Agents

An agent is a background process that maintains state. This state can be accessed at different places within a process or node, or across multiple nodes.
It's initial state is set by a function
we pass in when we start it.

Agent.get will get the state.
Agent.update will change the state.

iex> { :ok, count } = Agent.start(fn -> 0 end)
{:ok, #PID<0.69.0>}
iex> Agent.get(count, &(&1))
0
iex> Agent.update(count, &(&1+1))
:ok
iex> Agent.update(count, &(&1+1))
:ok
iex> Agent.get(count, &(&1))
2

Agent Naming

We can give an agen a name using name:.

iex> Agent.start(fn -> 1 end, name: Sum)
{:ok, #PID<0.78.0>}
iex> Agent.get(Sum, &(&1))
1
iex> Agent.update(Sum, &(&1+99))
:ok
iex> Agent.get(Sum, &(&1))
100

We exploit the fact that an uppercase bareword in Elixir is converted into an atom with the prefix Elixir., so when we say Sum it is actually the atom :Elixir.Sum.

Agents

Encapsulate an agent inside a module.

defmodule Frequency do
  def start_link do
    Agent.start_link(fn -> %{} end, name: __MODULE__)
  end

  def add_word(word) do
    Agent.update(__MODULE__, 
                 fn map ->
                      Map.update(map, word, 1, &(&1+1))
                 end)
  end

  def count_for(word) do
    Agent.get(__MODULE__, fn map -> map[word] end)
  end

  def words do
    Agent.get(__MODULE__, fn map -> Map.keys(map) end)
  end
end

Agents

Encapsulate an agent inside a module.

iex> c "agent_dict.exs"
[Frequency]
iex> Frequency.start_link
{:ok, #PID<0.101.0>}
iex> Frequency.add_word "dave"
:ok
iex> Frequency.words
["dave"]
iex(41)> Frequency.add_word "was"
:ok
iex> Frequency.add_word "here"
:ok
iex> Frequency.add_word "he"
:ok
iex> Frequency.add_word "was"
:ok
iex> Frequency.words
["he", "dave", "was", "here"]
iex> Frequency.count_for("dave")
1
iex> Frequency.count_for("was")
2
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
# four wordlist file
list1 list2 list3 list4
angor ester palet rogan
argon estre patel ronga
caret goran pelta steer
carte grano petal stere
cater groan pleat stree
crate leapt react terse
creat nagor recta tsere
creta orang reest tepal

$ 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"]

Agents and Tasks

defmodule Dictionary do
  @name {:global, __MODULE__}

Make It Distributed

Agents and tasks run as OTP servers, so they are easy to distribute—just give our agent a globally accessible name. 

# 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]
# Window 2 - load up list3 and list4
iex(two@FasterAir)> WordlistLoader.load_from_files(~w{words/list3 words/list4})
[:ok, :ok]

Make It Distributed

# Window 1 - load up list1 and list2
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 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"]

Agents and Tasks, or GenServer?

When do you use agents and tasks, and when do you use a GenServer?

Use the simplest approach that works.
Agents and tasks are great when you are dealing with very-specific background activities, whereas GenServers (as their name suggests) are more general.

You can eliminate the need to make a decision by wrapping your agents and tasks in modules, as we did in our anagram example. That way you can always switch from the agent or task implementation to the full-blown GenServer without affecting the rest of the code base.

Thank you!

Programming Elixir 1.2 Chapter 22

By Dustin McCraw

Programming Elixir 1.2 Chapter 22

  • 981