What is Elixir?
Elixir is a powerful functional programming language built for scalability and maintainability. In this post, we’ll create a simple chat application to explore how Elixir handles distributed systems, concurrency, and fault-tolerance.
Why Elixir?
Elixir runs on the Erlang Virtual Machine (BEAM), renowned for its ability to handle millions of lightweight processes with ease. This makes Elixir ideal for building fault-tolerant systems like chat apps. Here’s why developers love it:
- Concurrency: Simplifies running multiple tasks simultaneously.
- Fault-tolerance: Built-in supervision trees restart failed processes.
- Scalability: Horizontal scaling across nodes is seamless.
- Developer Productivity: Features like readable syntax and great tooling.
Setting Up the Project
First, install Elixir if you haven’t already. Once that’s done, create a new project with the mix
tool:
mix new chat --sup
What’s Happening Here?
mix
: Elixir’s build tool^[Mix].new
: Command to create a new project.chat
: The name of our project.--sup
: Adds a supervisor to manage processes.
Navigate into your project directory and test it:
cd chat
mix test
You should see output like this:
Compiling 2 files (.ex)
Generated chat app
..
Finished in 0.01 seconds (0.00s async, 0.01s sync)
1 doctest, 1 test, 0 failures
Adding a Supervisor
Supervisors^[Supervisor] are essential in Elixir for building fault-tolerant systems. They manage the lifecycle of processes, restarting them if something goes wrong.
Let’s update lib/chat/application.ex
to include a Task.Supervisor
:
# lib/chat/application.ex
defmodule Chat.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
{Task.Supervisor, name: Chat.TaskSupervisor}
]
opts = [strategy: :one_for_one, name: Chat.Supervisor]
Supervisor.start_link(children, opts)
end
end
Here, we’re setting up a Task.Supervisor
under the Chat.Supervisor
tree. This will allow us to dynamically spawn tasks later.
Creating the Chat Module
Now, let’s define the Chat
module and write a function to handle incoming messages:
# lib/chat.ex
defmodule Chat do
@moduledoc false
def receive_message(message) do
IO.puts message
end
end
Easy! Isn’t it?
What does it do? Simple: it takes the message we send and prints it directly to the terminal.
But receiving messages is only half the story. We also need to send them! Let’s add a function for that.
Sending Messages
To send messages, we’ll use a supervised task. Here’s how we define the send_message/2
function:
defmodule Chat do
...
def send_message(recipient, message) do
spawn_task(__MODULE__, :receive_message, recipient, [message])
end
def spawn_task(module, fun, recipient, args) do
recipient
|> remote_supervisor()
|> Task.Supervisor.async(module, fun, args)
|> Task.await()
end
defp remote_supervisor(recipient) do
{Chat.TaskSupervisor, recipient}
end
end
How It Works:
send_message/2
: Sends a message to a recipient by spawning a task.spawn_task/4
: Spawns a supervised task on the recipient’s node and waits for it to complete.remote_supervisor/1
: Locates theTask.Supervisor
on the recipient’s node.
Running the App
Elixir makes it easy to run distributed applications. First, start an interactive shell (IEx) with a node name:
iex --sname alex@localhost -S mix
In a second terminal, start another node:
iex --sname kate@localhost -S mix
Now, send a message from one node to the other:
Chat.send_message(:kate@localhost, "Hello from Alex!")
You’ll see the message printed in the terminal where the kate
node is running.
What Did We Build?
We’ve created a simple yet powerful chat app demonstrating how Elixir handles:
- Process Supervision: Ensuring fault-tolerance with supervisors.
- Concurrency: Using
Task.Supervisor
to manage tasks. - Distribution: Sending messages between nodes.
Conclusion
Elixir’s robust tooling and the Erlang VM’s strengths make it a perfect choice for distributed systems. By building this small application, you’ve seen how to set up a project, use supervisors, and communicate between nodes.
Happy coding with Elixir! 🚀