GenLoop
GenLoop provides a safe, OTP-compliant way to write processes using the "plain receive loop" pattern. It is built directly on top of Ulf Wiger's :plain_fsm but adapted for Elixir with some additional conveniences.
It allows you to write processes that use selective receive (unlike GenServer) while still handling system messages, parent exits, and other OTP requirements automatically.
Installation
Add gen_loop to your list of dependencies in mix.exs:
def deps do
[
{:gen_loop, "~> 2.0"},
]
endFeatures
- OTP Compliant: Handles system messages (
sysmodule), parent supervision, and debugging. - Selective Receive: Use
receiveblocks to pick messages when you want them, enabling complex state machines or protocols to be implemented more naturally. - Convenient Macros:
rcall/2andrcast/1macros to easily match onGenLoop.call/2andGenLoop.cast/2messages. - Familiar API: Uses
start_link/3,call/3,cast/2similar toGenServer.
Usage
To use GenLoop, use GenLoop in your module. You need to define an entry point function (default is enter_loop/1 or specified via enter: :function_name).
Basic Example
Here is a simple Stack implementation:
defmodule Stack do
use GenLoop
# Client API
def start_link(initial_stack) do
GenLoop.start_link(__MODULE__, initial_stack, name: __MODULE__)
end
def push(item) do
GenLoop.cast(__MODULE__, {:push, item})
end
def pop do
GenLoop.call(__MODULE__, :pop)
end
# Server Callbacks
# Optional: init/1 can be used to validate args or set up initial state.
# It should return {:ok, state}, {:stop, reason}, or :ignore.
def init(initial_stack) do
{:ok, initial_stack}
end
# The main loop.
# The `receive/2` macro (provided by GenLoop) is used instead of `receive/1`.
# It takes the current state as the first argument to handle system messages automatically.
def enter_loop(stack) do
receive stack do
# Match a synchronous call
rcall(from, :pop) ->
case stack do
[head | tail] ->
reply(from, head) # Helper to send reply
enter_loop(tail) # Loop with new state
[] ->
reply(from, nil)
enter_loop([])
end
# Match an asynchronous cast
rcast({:push, item}) ->
enter_loop([item | stack])
# Match standard messages
other ->
IO.inspect(other, label: "Unexpected message")
enter_loop(stack)
end
end
endUsing the Process
{:ok, _pid} = Stack.start_link([1, 2])
Stack.pop()
#=> 1
Stack.push(3)
#=> :ok
Stack.pop()
#=> 3Why GenLoop?
vs GenServer
GenServer is the standard abstraction for client-server relations. However, it forces you to handle every message in a callback (handle_call, handle_info, etc.). This is great for most cases, but can be cumbersome for complex state machines where the set of expected messages changes depending on the state.
GenLoop allows you to write a "plain" recursive loop with receive, so you can wait for specific messages at specific times (selective receive).
vs :gen_statem
:gen_statem is the OTP standard for state machines. It is very powerful but can be verbose. GenLoop offers a middle ground: it's simpler and feels more like writing a raw Elixir process, but with the safety net of OTP compliance.
Advanced Usage
Custom Entry Point
You can specify a different entry function name:
use GenLoop, enter: :my_loop
def my_loop(state) do
# ...
endHandling System Messages
The receive state do ... end macro is the magic that makes your loop OTP-compliant. It expands to a receive block that also includes clauses for handling system messages (like sys.get_state, sys.suspend, etc.) and parent exit signals.
If you want to handle everything manually (not recommended unless you know what you are doing), you can use the standard receive do ... end, but your process will not respond to standard OTP system calls.
Acknowledgements
This library is built on top of :plain_fsm by Ulf Wiger.
It also draws inspiration from plain_fsm_ex by ashneyderman.