DEV Community

Elxpro
Elxpro

Posted on • Edited on

Read this Article if you want to learn Elixir with GenServer.

Greeting

Hello #devElixir!!! Welcome to #FullStackElxpro

Here we discuss strategies and tips for your Elixir learning journey, from scratch to a hero Elixir developer.

I am Gustavo, and today's theme is GenServer.

ps: You can follow the Article with a VIDEO

Do you want to learn Elixir in three months? https://elxpro.com/sell

Want to learn more about Elixir on a Telegram channel?

https://elxpro.com/subscribe-elxcrew

What is a GenServer?

GenServer is a process like any other Elixir process (Tasks, Spawn, Agents). It can be used to manage state and execute code asynchronously and includes some handy functionality around tracing and error reporting.

And for me, as a Liveview evangelist is one of the best ways to learn what you have behind the LiveView Apps, providing a better way to organize, think and isolate pieces of code.

Before we move forward, I think it is essential to read When Not Use GenServer. Because one of the biggest mistakes you can make is using GenServer for Everything, which is unnecessary.

What is the difference between GenServer, Tasks, and Agents?

Before you keep reading this article, I highly recommend watching the video about Elixir's processes.

https://youtu.be/em4QECkQx4s

GenServer : The GenServer behaviour abstracts the common client-server interaction. Developers are only required to implement the callbacks and functionality they are interested in.

Tasks: Tasks are processes meant to execute one particular action throughout their lifetime, often with little or no communication with other processes. The most common use case for tasks is to convert sequential code into concurrent code by computing a value asynchronously.

Agents: Agents are a simple abstraction around state.

Why is GenServer Important?

I have seen developers who start their career as Elixir developers but don't understand the basic concepts of how Elixir works.

Elixir is not a normal programming language. I mean that because Frontend Developers usually come to Elixir from React, Angular, and Vue, all of which use JavaScript and oriented programming languages. And daily, they face difficulties understanding what is behind LiveView, how to split components, and thinking in single responsibilities principles.

They don't understand it because of two simple things. Behind Elixir, there are processes, and LiveView is very similar to a GenServer.

What is the best moment to use GenServer?

Handle Sync and Async Calls: As we discussed, the main purpose of a GenServer is not to Code Organization but as a way to keep the state and manipulate it easily. Example Shopping Cart

System Messages: You can see some examples when you create ways to use spawn, send and receive_ . As _ I mentioned, I recorded a video explaining how the process works, and one of the examples is a simulation of how to access the database and execute a query.

Monitoring Events: Imagine that you have a company monitoring company stocks every second. The stocks' assets rise and fall every second.

Where to start?

Callbacks

  • init/1 (required) - invoked when the server is started and sets the initial state
  • handle_cast/2 - invoked when GenServer.cast/2 is called to handle messages asynchronously
  • handle_call/3 - invoked when GenServer.call/3 is called to handle messages synchronously
    • call/3 will block until a reply is received (unless the call times out or nodes are disconnected)
  • handle_info/2 - invoked to handle all other messages (i.e., outside of those triggered by GenServer.call/3 and GenServer.cast/2, and "system" messages)
    • g., messages/events handled internally within the GenServer
  • terminate/2 - invoked when the GenServer is about to exit

Init

Before we talk about Init, we must talk about start_link together.

start_link: when it is called, the new process created will call init, and init will start the state, which is an empty map, and the parent of ShoppingCart is the IEX terminal.

Take a look at the code.

defmodule ShoppingCart do
  use GenServer

  def start_link([name, state]) do
    IO.inspect(state, label: "start_link")
    GenServer.start_link(__MODULE__, state, name: name)
  end

  def init(state) do
    IO.inspect state, label: "init"
    {:ok, state}
  end
end
Enter fullscreen mode Exit fullscreen mode
iex(1)> ShoppingCart.start_link [:elxpro_cart, []]
start_link: []
init: []
{:ok, #PID<0.161.0>}
iex(2)> Process.whereis :elxpro_cart
#PID<0.161.0>
iex(3)> self   
#PID<0.159.0>
iex(4)> :observer.start

Enter fullscreen mode Exit fullscreen mode

Image description

HandleCast


Invoked when GenServer.cast/2 is called to handle messages asynchronously

In our example, imagine that you need to add a new product to your cart but don't care how many products you have there. You only want to add it is a perfect example of how to call handle_cast.

  def handle_cast({:add, product}, state) do
    state = state ++ [product]
    {:noreply, state}
  end
Enter fullscreen mode Exit fullscreen mode
iex(1)> ShoppingCart.start_link [:cart_1, []]
start_link: []
init: []
{:ok, #PID<0.151.0>}

iex(2)> GenServer.cast :cart_1, {:add, %{name: "Pumpkin", price: 10}}

iex(3)> "0.151.0" |> pid |> :sys.get_state
Enter fullscreen mode Exit fullscreen mode

HandleCall


The handle_call/3 callback is invoked when GenServer.call/3 is called to handle synchronous messages. The main params are function_pattern_mathing_from, state.

function_pattern_mathing: a Way to identify how to call the function

_from: most of the time is the process_id that calls the function, and most of the time is useful 

state: actual component state.

And the most common return is* {:reply, response, state}*

Reply: indicates that those who called the process will receive a message result.

Response: message to return for who called the process.

State: update process state.

Check the example, Bellow:

  def handle_call(:apply_discount, _from, state) do
    state = Enum.map(state, &%{&1 | price: &1.price - 1})
    products = Enum.map(state, &(&1.name <> " "))
    {:reply, "Discount applied to: #{products}", state}
  end
Enter fullscreen mode Exit fullscreen mode
iex(1)> ShoppingCart.start_link [:cart_1, []]
start_link: []
init: []
{:ok, #PID<0.161.0>}


iex(2)>  GenServer.cast :cart_1, {:add, %{name: "Pumpkin", price: 10}}

iex(3)>   GenServer.cast :cart_1, {:add, %{name: "Gatorade", price: 20}}


iex(6)>  GenServer.call :cart_1, :apply_discount            
Result: "Discount applied to: Pumpkin Gatorade "

State: 
iex(7)>   pid = "0.161.0" |> pid |> :sys.get_state
[%{name: "Pumpkin", price: 8}, %{name: "Gatorade", price: 18}]
Enter fullscreen mode Exit fullscreen mode

HandleInfo

The handle_info/2 callback is invoked to handle all other messages (i.e., outside of those triggered by GenServer.call/3 and GenServer.cast/2)

  def handle_info(:increase_price, state) do
    state = Enum.map(state, &%{&1 | price: &1.price + 1})
    {:noreply, state}
  end
Enter fullscreen mode Exit fullscreen mode
iex(1)>  ShoppingCart.start_link [:cart_1, []]
iex(2)> GenServer.cast :cart_1, {:add, %{name: "Pumpkin", price: 10}}
iex(3)> pid = pid("0.161.0")                

iex(4)> :sys.get_state(pid)
[%{name: "Pumpkin", price: 10}]

iex(5)> send pid, :increase_price
iex(6)> :sys.get_state(pid)      
[%{name: "Pumpkin", price: 11}]
Enter fullscreen mode Exit fullscreen mode

Code:

defmodule ShoppingCart do
  use GenServer

  def start_link([name, state]) do
    IO.inspect(state, label: "start_link")
    GenServer.start_link(__MODULE__, state, name: name)
  end

  def init(state) do
    IO.inspect(state, label: "init")
    {:ok, state}
  end

  def handle_cast({:add, product}, state) do
    state = state ++ [product]
    {:noreply, state}
  end

  def handle_call(:apply_discount, _from, state) do
    state = Enum.map(state, &%{&1 | price: &1.price - 1})
    products = Enum.map(state, &(&1.name <> " "))
    {:reply, "Discount applied to: #{products}", state}
  end

  def handle_info(:increase_price, state) do
    state = Enum.map(state, &%{&1 | price: &1.price + 1})
    {:noreply, state}
  end
end
Enter fullscreen mode Exit fullscreen mode

THE END.

The main goal of this Article is to help you understand how easy it is GenServer and after couple examples, you can possibly create simpler LiveView Pages and Elixir Codes

Social networks:

Top comments (0)