Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/pragdave/otp_dsl
A simple Elixir DSL for creating GenServers
https://github.com/pragdave/otp_dsl
Last synced: 5 days ago
JSON representation
A simple Elixir DSL for creating GenServers
- Host: GitHub
- URL: https://github.com/pragdave/otp_dsl
- Owner: pragdave
- Created: 2013-08-15T23:17:26.000Z (about 11 years ago)
- Default Branch: master
- Last Pushed: 2014-03-15T21:42:45.000Z (over 10 years ago)
- Last Synced: 2024-08-01T19:40:51.143Z (3 months ago)
- Language: Elixir
- Size: 249 KB
- Stars: 57
- Watchers: 8
- Forks: 15
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# OtpDsl
A set of simple Elixir DSLs that wrap the common uses of OTP
servers. For example, here's the code for a `:gen_server` that exports
a function `factorial!`. When called, the actual factorial code is
executed in its own process.``` elixir
defmodule FactorialServer do
use OtpDsl.Genserverdefcall factorial!(n) do
reply(Enum.reduce(1..n, 1, &(&1*&2)))
end
end
```You could start this from a supervisor, or manually with
`FactorialServer.start_link`. You can then call the function
in the server process using``` elixir
FactorialServer.factorial!(20)
```Similarly, here's a different DSL that simplifies the creation of
`:gen_fsm` state machines.``` elixir
defmodule Listen.NewFSM douse OtpDsl.Genfsm, register: {:local, :listen_fsm},
initial_state: :start,
init_params: []@timeout 3*1000
defrecord CallInfo, from: "", to: "", suspicious_segments: 0
####################
#
# Events → our external APIevents do
call_initiated(from, to)
suspicious_phrase_heard
hang_up
end####################
#
# States → transitions we implementin_state(:start) do
{ :call_initiated, from, to } ->
IO.puts "Initiating a call from #{from} to #{to}"
next_state(:listening, CallInfo.new(from: from, to: to))
endin_state(:listening) do
{ :hang_up } ->
debug("Hangup", context)
next_state(:start, nil){ :suspicious_phrase_heard } ->
debug("Heard something suspicious", context)
next_state(:transcribing, context.update_suspicious_segments(&1+1), @timeout)
endin_state(:transcribing) do
{ :hang_up } ->
debug("Report on call", context)
next_state(:start, CallData.new){ :timeout } ->
next_state(:listening, context)
end# Helpers
defp debug(msg, CallInfo[from: from, to: to, suspicious_segments: suspicious_segments]) do
IO.puts("Call from #{from} to #{to}: #{msg}")
if suspicious_segments > 0 do
IO.puts(" (suspicious_segments: #{suspicious_segments})")
end
end
end
```The intent behind these DSLs it to remove the boilerplate in server
code. While Erlang has Emacs macros that generate it, Elixir has
built-in macros that make it redundant.Here's more in-depth documentation for each DSL
## OtpDsl.Genserver
A simple DSL wrapper for GenServer modules. It reduces duplication by
exploiting the fact that a GenServer module typically has an API function
that calls a corresponding handle_call function that runs on the server
process. We wrap those two functions into one macro. For example, the following
module is a GenServer with a single API, `factorial`.defmodule FactorialServer do
use OtpDsl.Genserverdefcall factorial!(n) do
reply(Enum.reduce(1..n, 1, &(&1*&2)))
end
endYou could start this from a supervisor, or manually with
`FactorialServer.start_link`. You can then call the function
in the server process usingFactorialServer.factorial!(10)
The `use` function takes the following options:
`register`
The specification of the name under which to register the server. For
example, if you wanted to register globally as `:fred`, douse OtpDsl.Genserver, register: { :global, :fred }
The option defaults to `{ :local, my_name }`, where `my_name` is
an atom derived from the module name.`initial_state`
A value representing an initial state for tbe server. Defaults to
`nil`.### Example
Here's a server that implements a simple key/value store:
defmodule KvServer do
use OtpDsl.Genserver, initial_state: HashDict.newdefcall put(key, value), kv_store do
reply(value, Dict.put(kv_store, key, value))
enddefcall get(key), kv_store do
reply(Dict.get(kv_store, key))
end
endNote how both functions specify that the state should be stored
in the variable `kv_store`. The first returns an updated state as the second
argument to reply, and the second leaves the state unchanged.## OtpDsl.Gemfsm
A DSL wrapper for Elixir's GenFSM.Behaviour. For example:
``` elixir
defmodule Listen.NewFSM douse OtpDsl.Genfsm, register: {:local, :listen_fsm},
initial_state: :start,
init_params: []@timeout 3*1000
defrecord CallInfo, from: "", to: "", suspicious_segments: 0
####################
#
# Events → our external APIevents do
call_initiated(from, to)
suspicious_phrase_heard
hang_up
end####################
#
# States → transitions we implementin_state(:start) do
{ :call_initiated, from, to } ->
IO.puts "Initiating a call from #{from} to #{to}"
next_state(:listening, CallInfo.new(from: from, to: to))
endin_state(:listening) do
{ :hang_up } ->
debug("Hangup", context)
next_state(:start, nil){ :suspicious_phrase_heard } ->
debug("Heard something suspicious", context)
next_state(:transcribing, context.update_suspicious_segments(&1+1), @timeout)
endin_state(:transcribing) do
{ :hang_up } ->
debug("Report on call", context)
next_state(:start, CallData.new){ :timeout } ->
next_state(:listening, context)
end# Helpers
defp debug(msg, CallInfo[from: from, to: to, suspicious_segments: suspicious_segments]) do
IO.puts("Call from #{from} to #{to}: #{msg}")
if suspicious_segments > 0 do
IO.puts(" (suspicious_segments: #{suspicious_segments})")
end
end
end
```There are three sections in this code
1. The `use` stanza.
2. The `event list`
3. The `state definitions`### `context` vs. `state`
In an Erlang server, state is passed between callbacks. However, it gets confusing talking
about this server state when we also have the concept of state machine states. So, in this
module, we use the convention that the server state is called the `context`.### The `use` stanza
As well as including the OptDel.Genfsm behaviour in your module, the `use`
call lets you set various options:| Option | Default | Meaning |
| ------------- | --------------- | --------------------|
| register | {:local, :fsm } | The first argument to `start_link`, used to register a name for this module |
| initial_state | :state | The name of the initial state of the FSM |
| context | [] | The initial context |### The Event List
In GenFSM, events are triggered by calls to public functions. Each function then calls `send_event`
to cause the appropriate callback to be invoked.This code is pretty boilerplate, so we abstract it into an `events` stanza:
``` elixir
events do
call_initiated(from, to)
suspicious_phrase_heard
hang_up
end
```This block simply creates the following functions:
``` elixir
def call_initiated(from, to) do
:gen_fsm.send_event(«servername», :call_initiated, from, to)
end
def suspicious_phrase_heard do
:gen_fsm.send_event(«servername», :suspicious_phrase_heard)
end
def hang_up do
:gen_fsm.send_event(«servername», hang_up)
end
```You're free to write these functions by hand, too.
### The States
We represent each state using `in_state(«name»)`. Within the block, separate clauses
match each incoming event (with optional parameters). The code corresponding with the match
will end with a call to `next_state`, which takes the new state name, the new context, and an optional timeout (in mS).``` elixir
in_state(:listening) do
{ :hang_up } ->
debug("Hangup", context)
next_state(:start, nil){ :suspicious_phrase_heard } ->
debug("Heard something suspicious", context)
next_state(:transcribing, context.update_suspicious_segments(&1+1), @timeout)
end
```So, if we're in state `listening` and we get a `hang_up` event, we
call `debug` to print a message, and then transition to the state
state, with no context. If instead we get a
`suspicious_phrase_heard` event, we transition to the `transcribing`
state. We pass an updated context where we increment the number of
suspicious segments, and we set a timeout.If a timeout fires, it generates a `timeout` event in the next state
(there's an example in the `transcribing` state of the sample app)## Add to your Project
Add the following to your list of dependencies in `mix.exs`:
``` elixir
{ :otp_dsl, github: "pragdave/otp_dsl" }
```## Copyright
Copyright © 2013 Dave Thomas, The Pragmatic Programmers
Licensed for use under the same terms as Elixir