Applicatives in Elixir . (Im)mutability . Tasks

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
  
  def f1 num
  do
    p1 = spawn(fn() -> 
                 receive do 
                     {x , p2} when is_integer(x) -> send p2 , x + 2
                     _ -> IO.puts :stderr , "unknown value" 
                 end
              end)

    send p1 ,  {num , self()}

    :timer.sleep(7000)

    receive do
        x when is_integer(x) -> IO.puts x
        _ -> IO.puts :stderr , "smth strange"
    end
  end
 
end

Te.f1(10)

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

defmodule Te
do
  
  def f1(c , a , b)
  do
    m = c.(a , b)
    receive do 
      {:get , p2} -> send p2 , m
       _ -> IO.puts :stderr , "unknown value" 
    end
  end

  def f2
  do
    p1 = spawn(Te, :f1, [(fn(x , y) -> x + y end) , 4 , 5]) 
    :timer.sleep(7000)
    send p1 , {:get , self()}
    k = receive do
          x when is_integer(x) -> x
          _ -> IO.puts :stderr , "smth strange"
        end
    IO.puts (k + 56)
  end
 
end

Te.f2()

Elixir has an additional wrapper called Task which removes most of the boilerplate

consumer/producer

defmodule Consumer 
do

  def consume 
  do
    t1 = Task.async(Producer, :produce, [:random.uniform(1000)])
    t2 = Task.async(Producer, :produce, [:random.uniform(2000)])  
    :timer.sleep(5000)
    Enum.reduce([Task.await(t1), Task.await(t2)], 0 , &(&1 + &2)) |> IO.puts
  end

end
  

defmodule Producer
do

  def produce(s) 
  do
    :random.seed(s)
    :timer.sleep(100)
    :random.uniform(100)
  end

end

send/2
receive/1

      iex> send self() , {:hello, "world"}
      {:hello, "world"}
      iex> receive do {:hello , msg} -> msg ; {:world, msg} -> "won't match" end
      "world"

spawn/1 and spawn_link/1 are the basic primitives for creating processes in Elixir

but instead of spawn/1 and spawn_link/1, you can use Task.start/1 and Task.start_link/1

they return {:ok, pid} rather than just the pid. this enables Task to be used in supervision trees. furthermore, Task provides convenience functions, like Task.async/1 and Task.await/1