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.
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
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
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
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
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
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}]
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
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}]
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
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:
- Gustavo Oliveira - https://www.linkedin.com/in/gustavo-oliveira-642b23aa/
- Elxpro Linkedin - https://www.linkedin.com/company/36977430
- Twitter - https://twitter.com/elx_pro
- ElxproBr - https://www.youtube.com/channel/UCl_BBK2sXZzQy_3ziNU7-XA
- Elxpro - https://www.youtube.com/channel/UCLzHBFuE6oxPdP6t9iqpGpQ
Top comments (0)