# GenLoop
<!-- rdmx :badges
hexpm : "gen_loop?color=4e2a8e"
github_action : "lud/gen_loop/elixir.yaml?label=CI&branch=main"
license : gen_loop
-->
[](https://hex.pm/packages/gen_loop)
[](https://github.com/lud/gen_loop/actions/workflows/elixir.yaml?query=branch%3Amain)
[](https://hex.pm/packages/gen_loop)
<!-- rdmx /:badges -->
**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`:
<!-- rdmx :app_dep vsn:$app_vsn -->
```elixir
def deps do
[
{:gen_loop, "~> 2.0"},
]
end
```
<!-- rdmx /:app_dep -->
<!-- doc_start -->
## Features
- **OTP Compliant**: Handles system messages (`sys` module), parent supervision, and debugging.
- **Selective Receive**: Use `receive` blocks to pick messages when you want them, enabling complex state machines or protocols to be implemented more naturally.
- **Convenient Macros**: `rcall/2` and `rcast/1` macros to easily match on `GenLoop.call/2` and `GenLoop.cast/2` messages.
- **Familiar API**: Uses `start_link/3`, `call/3`, `cast/2` similar to `GenServer`.
## 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:
```elixir
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
end
```
### Using the Process
```elixir
{:ok, _pid} = Stack.start_link([1, 2])
Stack.pop()
#=> 1
Stack.push(3)
#=> :ok
Stack.pop()
#=> 3
```
## Why 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:
```elixir
use GenLoop, enter: :my_loop
def my_loop(state) do
# ...
end
```
### Handling 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`](https://github.com/uwiger/plain_fsm) by Ulf Wiger.
It also draws inspiration from [`plain_fsm_ex`](https://github.com/ashneyderman/plain_fsm_ex) by ashneyderman.