Actors in Elixir . (Im)mutability . Agents

The primary way of maintaining a mutable state in Erlang is to run a separate process.

We can start a process (not only) by running a spawn command:

   p = spawn( ... )

In between parentheses will be a reference to a function, or a body of an inline anonymous function, which will run concurrently. The return value is the id of the created process, often called pid.

We can use that value to send messages to that process:

   send p , message

Messages are arbitrary Elixir terms - whatever you can put in a variable (e.g. list, structures, ...), and sending a message means that its value is placed in the mailbox of the receiving process, after which the sender goes on executing its own code. The receiver can obtain the next message by calling the 'receive' statement. Messages are processed in the order they are placed in the mailbox (although this behavior can be altered in code).

defmodule Te
do

  p = spawn(fn() -> 
               receive do 
                   x when is_integer(x) ->
                     x |> Kernel.+(2) |> to_string |> IO.puts
                   x when is_nil(x) or is_boolean(x) -> 
                     x |> to_string |> IO.puts
                   _ -> 
                     IO.puts :stderr , "unknown value" 
               end
            end)

  send p ,  true 

end

    
defmodule Te
do

  def f1 
  do
    receive do 
        x when is_integer(x) ->
          x |> Kernel.+(2) |> to_string |> IO.puts
        x when is_atom(x) ->
          x |> to_string |> IO.puts
        _ ->
          IO.puts "bye"
        after 5000 ->
          IO.puts "timeout"
    end
  end

end

p = spawn(Te , :f1 , [])
send p , "Alice" 

When we want to maintain a continuous mutable state, we have to run an endless recursion in a separate process:

defmodule Te
do

  def f s , m
  do
    s + m
  end

  def loop state 
  do
    receive do
          {:sub , x} when is_integer(x) -> 
            new_state = state - x
            loop(new_state)
          {:add , x} when is_integer(x) -> 
            new_state = f(state , x)
            loop(new_state)
          {:set , x} when is_integer(x) -> 
            loop(x)
          {:get , pid} -> 
            send pid , state 
            loop(state)
          _ -> 
            IO.puts :stderr , "bye"
    end
  end

end

In Elixir, such recursion will not cause a stack overflow, since language has special handling of the so called "tail calls" which will, on a byte code level, be transformed to a jump/goto instructions. Consequently, this code simply runs an endless loop.

Once we have such process running, and hold its pid, we can interact with it via messages:

   # async send and pray
   send pid , {:set , 123}
   send pid , {:add , 234}

   # sync call and get response
   send pid , {:get , self()}
   r = receive ...                  # the receiver must send us the response

Erlang/OTP offers an abstraction called gen_server, which abstracts typical message passing patterns, but adds more boilerplate.

Elixir simplifies the use of 'gen_server', and there is an additional wrapper called Agent which removes most of the duplication

calculator

The first example is a simple calculator actor which supports increment/decrement operations.

defmodule Calc
do
  
    def init(state)
    do
      Agent.start(fn -> state end)
    end
  
    def get(x)
    do
      Agent.get(x, fn state -> state end)
    end
  
    def add(x , num)
    do
      Agent.update(x , fn state -> state + num end)
    end
  
    def sub(x , num)
    do
      Agent.update(x , fn state -> state - num end)
    end
  
    def set(x , num)
    do
      Agent.get_and_update(x , fn state -> {state , num} end)
    end
  
end

Let's see how we can use it:

{:ok , x} = Calc.init 0
Calc.add x , 10
Calc.sub x , 5
res = Calc.get x
IO.puts result

The actor is created with an initial value of 0. Then I add the value of 10, and subtract a value of 5. Finally, I retrieve the result and print it. Since calculator is an Actor it works concurrently. Specifically, sub/add operations are asynchronous, while get is obviously synchronous, since it has to return the result.

In the first line, an actor is created. Under the hood, the function start will spawn a process, sending it value 0 as an argument. The Actor will use that value as its initial state, which internally means, we will enter the infinite recursion with the value of 0.

Now we can use the variable to do something with an Actor , for example invoke increment/decrement operations. Behind the scene, these functions will send asynchronous messages {:add, 10} or {:sub, 5} to the calculator process without waiting for the response

Synchronous get operation internally sends a message to the Actor, and waits for it to respond.

Actors vs Objects

Actors and Objects have some properties in common: both encapsulate state while hiding its details from their clients. The clients can manipulate the state via messages (Actors) or methods (Objects).

Coming from OO, it helps me to think of Actors as Objects. However, be aware of the fact that actors are not garbage collectible. They are long running processes which will not be terminated if no "reference" to them exists.

Unlike Objects, Actors are inherently concurrent. They are also completely independent and have no data in common. One Actor cannot corrupt the state of another nor can a crash in one Actor impact the other ones, unless explicitly specified by the programmer.