Processes Communication
Hey folks, I wonder how many of you have read all episodes so far, if you are one of them please shout out to me on twitter, I’d love to have some feedback :)
This is a series of short bar-like conversations around Elixir and its features, it aims to help you to wrap your head around it by using meaningful examples and putting it in the context of real world problems.
If you haven’t yet, you probably should check the last episodes, I’ve been writing this in a logical sequence that makes it way easier to understand by gracefully increasing the complexity of the topics and examples:
- Episode I - Elixir, Pipe Operator and Pattern Matching
- Episode II - Actor Model, Modules and functions
- Episode III - Maps, Functions + Pattern Matching = ❤️
- Episode IV - Elixir Types, Data Structures and Underscore
- Episode V - Concurrency, Processes and Recursion
- Episode VI - Head + Tail and List Comprehension
- Episode VII - Processes Communication
Processes Communication
We have talked a lot about processes on other episodes, not only how they work but also how they are a key component of Elixir, something it inherited from Erlang, and a core piece to extract the most out of the language capabilities.
On Episode II - Actor Model, Modules and functions we talked about Actor Model, a known architecture that was used on the implementation of Erlang VM, it enables processes to be totally isolated from each other, not sharing any context, and by doing so, it actually makes easier to share specific information between processes, but in a organized way, by exchanging messages.
Every process in Erlang (and therefore in Elixir as well) is considered an Actor, completely isolated from its pears, a single processes might be related to other processes, like Supervisors, or linked processes (we’ll talk about that in a near future), but it does not share the same context and variables with other processes.
Every process also has what you could imagine as an empty mailbox, just waiting for messages, and you can program your processes to respond to those messages depending on their content. You can also make your process send messages to others by using an identifier, a process ID, what we call PID (Process Identifier).
This alone can sound silly, but having this ability is a huge deal and enables you to do a lot of fun stuff you wouldn’t do in other non-functional languages like Ruby.
Let’s start by checking on how to send, receive and respond to messages on processes, doing this will involve some previous knowledge from past articles mostly episodes I, III and V.
# Let's start by figuring out our console Process ID (PID)
# by now you already now that all you need to strat the
# interactive console is to type `iex` on your terminal.
# The `self/0` function return the current Process PID.
current_proc = self()
# #PID<0.56.0>
# Now we can use the `process_info/2` function
# from Erlang to get all messages our current
# process (the interactive console) have
:erlang.process_info(self(), :messages)
# {:messages, []}
# As you can see, there is no messages waiting.
# We can send a message from this process to itself by
# using the `send/2` function, it has two arguments, the
# PID and the message.
send(current_proc, "new message")
# Now, if we check our current process messages we will
# find one message waiting to be dealt with.
:erlang.process_info(self(), :messages)
# {:messages, ["new message"]}
# To handle messages we need to use a receive block
# it's really straightforward and uses pattern matching
# agains the next message on the list.
receive do
message -> IO.inspect("Received #{message}")
end
# "Received new message"
#
# The message is displayed and now if we check the list
# of messages, it should be empty again
:erlang.process_info(self(), :messages)
# {:messages, []}
As you can tell, sending and receiving messages is something really simple in Elixir, and that’s one of its major benefits, it makes super easy for you to take advantage of its main capabilities.
In this first example we send only a string, but keep in mind you can send any type of data, the most commonly used are tuples
(you can check more about tuples on Episode IV - Elixir Types, Data Structures and Underscore).
Another thing that you might have noticed is that the receive
block only handles one message, the next one on the list of messages. If you have multiple messages waiting, the receive block will only handle the next one, the one on the bottom of the list:
current_proc = self()
# #PID<0.56.0>
:erlang.process_info(self(), :messages)
# {:messages, []}
# No messages waiting.
# This time we sen multiple messages to this same process
# we will send tuples, with a `:new_message` atom and
# an integer
send(current_proc, {:new_message, 1})
send(current_proc, {:new_message, 2})
send(current_proc, {:new_message, 3})
# On our messages list we'll find all three messages
:erlang.process_info(self(), :messages)
# {:messages, [new_message: 1, new_message: 2, new_message: 3]}
# Now we use the `receive` block to pattern match the
# tuple with `{:new_message, n}`, and then display a message
# indicating we got the message
receive do
{:new_message, n} -> IO.inspect("Received new message: #{n}")
end
# "Received new message: 1"
#
# Only the first message was handled, the other two
# are still in the list waiting for another
# receive block
:erlang.process_info(self(), :messages)
# {:messages, [new_message: 2, new_message: 3]}
Okay, but how can we have a process constantly waiting for messages and handling it as it arrives? That’s where recursion comes into play, we talked about it on Episode V - Concurrency, Processes and Recursion, and this is an awesome use case for it.
# Let's create a `Sum` module that will have a `sum_number/1`
# funciton.
defmodule Sum do
# The `sum_number/1` expects one argument, an integer
# that will be used to perform a sum operation
def sum_number(x) do
# Here we use `receive` to match messages into the
# tuple `{:sum, n}` where `n` is an integer sent
# within the message.
# At this point the process will stop and wait for a
# message (if there is none) before trying to match it
# and move on.
receive do
{:sum, n} ->
# We display the result of the sum between the number
# passed as argument to `sum_number/1` and the number
# on the received message
IO.inspect(x + n)
# Then call this function again passing the same argument.
# By using recursion (calling this function again)
# we setup the receive block again, waiting for the
# next message, so every time a message is received
# this function will handle it and setup a new receive
# block to handle the next one.
sum_number(x)
end
end
end
# Now we spawn a new process to execute the `sum_number/1`
# function from the `Sum` module we defined above, passing `2`
# as the argument, and this will return the PID for this new process
sum_proc = spawn(Sum, :sum_number, [2])
# #PID<0.121.0>
# Then we can send a message for the process we just spawn above
# passing the tuple `{:sum, 3}` to check what will be displayed
send(sum_proc, {:sum, 3})
# 5
#
# The number `5` is displayed as expected result of `2 + 3`.
# Because the process uses recursion we can still send messages
# to it and it stills alive, waiting for another message.
send(sum_proc, {:sum, 10})
# 12
send(sum_proc, {:sum, 6})
# 8
send(sum_proc, {:sum, 2})
# 4
# Here we can check the process is still alive and
# therefore waiting for messages
Process.alive?(sum_proc)
# true
Now we already know how to use recursion for creating a process always ready to receive messages, but what about multiple processes communicating with each other? Let’s check bellow.
Practical Example
For this example we’ll build a in-memory storage for gravatar images and its paths. Before diving into the code it’s important that you understand that functional languages do not have the usual structures for holding state, keep in mind there is no objects nor instances. Recursion is one of the ways you can support state, and that’s exactly what we need for this example.
We want to have an module with a function that returns the local path for a gravatar image for a given email, in order to achieve that we will need to perform the following actions:
- Check if we already have this image locally
- Check if the image on Gravatar
- Download the image and save it locally
- Return the local image path
- Append the new image path to a in-memory storage
# We start creating our Gravatar module
defmodule Gravatar do
# We'll get into `ensure_all_started/1` on future episodes
# but here we have to start `:inets` app because we'll
# need one of its modules down the road when downloading
# the gravatar image, `:inets` is part of erlang
# standard library
Application.ensure_all_started(:inets)
# This is our main function `gravatar_images/1`, it expects
# a `Map` as argument and this `Map` will store the paths
# for all images as we find and download them.
# As you can tell we are using pattern matching to ensure
# the `images_path` argument will be `Map`, this doesn't
# mean it needs to be empty, we could start with pre-existing
# images already.
def gravatar_images(images_path = %{}) do
# Here is our receive block, it tries to match messages
# into three different patterns (all expect to receive
# also the PID from the process that sent the message):
#
# - {:all_stored_images, pid}
# - {:get_image, email, pid}
# - {:store_image, email, path}
receive do
# `:all_stored_images` will basically send a message
# back for the process that sent this, with all the
# images, by returning the `images_path` map.
# Then it will call `gravatar_images/1` again, passing
# the same map as argument once again, so we sill have
# the receive block ready for the next message.
{:all_stored_images, pid} ->
send(pid, images_path)
gravatar_images(images_path)
# `:get_image` will send a message back to the PID that
# send this message with the local path for that image
{:get_image, email, pid} ->
# `get_image/2` is a private function defined down bellow,
# it tries to find the image into the Map we are using
# as storage, if it doesn't finds it, it'll download
# the image and return the path
new_path = get_image(images_path, email)
# Here we send the message back with the `new_path`
# of this image to the process that sent the
# initial message
send(pid, new_path)
# Then we send a message to this own process but
# with the tuple `{:store_image, email, new_path}`
send(self(), {:store_image, email, new_path})
# We call `gravatar_images/1` once again because we
# need to setup a new `receive` block to handle the
# message we just sent above with the `:store_image` tuple.
gravatar_images(images_path)
# `:store_image` will update the email key on the map
# we are using as storage to store the local path for
# the image.
# Using the pipe operator we call `gravatar_images/1`
# function again, but now passing the new Map as the
# argument, and that's what enable us to hold state
# and having a in-memory storage.
{:store_image, email, path} ->
Map.put(images_path, email, path)
|> gravatar_images
end
end
# The functions bellow are related to the gravatar
# integration, I'll avoid getting deep into explaning
# what is going on because it's not the main subject for
# this example
defp get_image(images_path, email) do
# Tries to get the `email` key from the map, if there
# is no key with that value, then call `download_image`.
# Here we are using case with pattern mathing, we talked
# about it in later episodes already.
case Map.get(images_path, email, :not_found) do
:not_found -> download_image(email)
path -> path
end
end
# Request, download and write the file to our local env
# return the path as the function response.
defp download_image(email) do
path = "gravatar/#{email}.jpg"
file = gravatar_file(email)
File.write!(path, file)
path
end
# Returns the binary of the image from gravatar API.
defp gravatar_file(email) do
http_opts = [body_format: :binary]
url_data = {gravatar_url(email), []}
{:ok, resp} = :httpc.request(:get, url_data, [], http_opts)
{ {_, 200, 'OK'}, _headers, body} = resp
body
end
# Mounts the gravatar API url accordinly to
# their docs.
defp gravatar_url(email) do
email_hash = :crypto.hash(:md5, email)
|> Base.encode16(case: :lower)
'http://www.gravatar.com/avatar/#{email_hash}.jpg'
end
end
# First we spaw a new process, that will execute
# the `gravatar_images/1` method from the `Gravatar`
# module we just created, seding and empty map (`%{}`)
# as argument (so no image yet).
pid = spawn(Gravatar, :gravatar_images, [%{}])
# Now we can send this process a message to get the
# avatar picture for a email
send(pid, {:get_image, "me@learnelixir.com", self()})
# If we check the messages our current interactive
# console has, we are supposed to have a new one with
# the local path for the image we just requested
:erlang.process_info(self(), :messages)
# {:messages, ["gravatar/me@learnelixir.com.jpg"]}
#
# As you can see, we do have a new message with the
# local path for that image
# If we ask for all images stored so far it will
# shoot a new message back, now with the whole map
send(pid, {:all_stored_images, self()})
:erlang.process_info(self(), :messages)
# {:messages,
# ["gravatar/me@joaomdmoura.com.jpg",
# %{"me@joaomdmoura.com" => "gravatar/me@joaomdmoura.com.jpg"}]}
#
# Now we have two messages, the last one and a new
# one with a map that has an email as a key and the
# path as value.
P.S.1 - If you already know Elixir and Erlang you probably noticed there is some room for improvement, I could have used the Task module or even ETS + GenServer to keep state. But for the sake of the simplicity of this examples it’s more than enough, we will talk about the Task module next, and about ETS a GenServers in the future.
What’s Next?
Finally on Episode VII of Elixir with a Rubyist! Again I failed to get into the Task module, but I do now we have gone through all the knowledge necessary to understand and use it, so the next episode will be dedicated to it!
I’m really glad from all the feedback I got so far, but I’d love to hear it from you, so please let me know on the comments bellow and over twitter.
If you don’t want to miss the next episodes and cool information around talks and even access to the code we use on the episodes you should subscribe to our mailing list by putting your email bellow.
On the next episode we will refactor our practical example using the Task
module and understand how to use it.