Understanding use and __using__ in Elixir
Have you ever wondered about the magic of the word use
in use GenServer
to create a GenServer in Elixir?
In Elixir, use Whatever
allows us to inject code defined in the Whatever
module. __using__
defines the code to be injected in compile time.
This mechanism lets us define reusable code. This promotes code reuse and maintainability. Let’s explore how we can create our own:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
defmodule MyBehavior do
defmacro __using__(_opts) do
quote do
# We could also inject alias, import, etc.
def one do
1
end
def two do
2
end
defoverridable one: 0
end
end
end
defmodule MyModuleA do
use MyBehavior
end
defmodule MyModuleB do
use MyBehavior
def one do
"one"
end
# Compiler warning: two/0 is already defined
def two do
"two"
end
end
We can confirm that we injected the functions:
MyModuleA.one()
returns1
MyModuleA.two()
returns2
We can now see how MyModuleB
properly redefined one/0
because within __using__
we set defoverridable one: 0
in MyBehavior
:
MyModuleB.one()
returns"one"
Yet, as we didn’t include defoverridable two: 0
in MyBehavior
, at compile time we receive a warning in the MyModuleB
: this clause for two/0 cannot match because a previous clause at line ... always matches
.
Therefore, when we call it we get the original value:
MyModuleB.two()
returns2
(instead of"two"
)
Notice that __using__
is like any other macro that returns AST, and so, you use quote and unquote.
For example:
1
2
3
4
5
quote do
def two do
1 + 1
end
end
Returns the following AST:
1
2
3
4
5
{:def, [context: Elixir, imports: [{1, Kernel}, {2, Kernel}]],
[
{:two, [context: Elixir], Elixir},
[do: {:+, [context: Elixir, imports: [{1, Kernel}, {2, Kernel}]], [1, 1]}]
]}
And unquote allows us to run code within a quote
:
1
2
3
4
5
6
7
quote do
def two do
unquote do
1 + 1
end
end
end
Returns the following AST:
1
2
{:def, [context: Elixir, imports: [{1, Kernel}, {2, Kernel}]],
[{:two, [context: Elixir], Elixir}, [do: [do: 2]]]}
Game
We could also add use
within __using__
. For example, we’re going to create a framework for turn-based games in Elixir using GenServer.
Our approach will demonstrate how to:
- Create a reusable game structure with
__using__
. - Use a GenServer for managing game state and handling concurrent operations (within the reusable game).
- Implement a specific game by extending the generic framework.
We’ll start with a generic Game
module that will handle common game mechanics like player management, round timing, and basic state operations:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
defmodule Game do
defmacro __using__(opts) do
quote do
use GenServer
@round_time unquote(Keyword.get(opts, :round_time, 60_000))
def start_link(init_arg) do
GenServer.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def join(player_name) do
GenServer.call(__MODULE__, {:join, player_name})
end
def get_state do
GenServer.call(__MODULE__, :get_state)
end
def make_move(player_name, move) do
GenServer.call(__MODULE__, {:make_move, player_name, move})
end
@impl true
def init(_init_arg) do
{:ok, initialize_state()}
end
@impl true
def handle_call({:join, player_name}, _from, state) do
if Map.has_key?(state.players, player_name) do
{:reply, {:error, :player_exists}, state}
else
new_state = put_in(state, [:players, player_name], 0)
{:reply, :ok, new_state}
end
end
@impl true
def handle_call(:get_state, _from, state) do
{:reply, get_public_state(state), state}
end
@impl true
def handle_call({:make_move, player_name, move}, _from, state) do
handle_move(player_name, move, state)
end
@impl true
def handle_info(:round_end, state) do
{:noreply, start_round(state)}
end
defp start_round(state) do
if state.timer_ref, do: Process.cancel_timer(state.timer_ref)
timer_ref = Process.send_after(self(), :round_end, @round_time)
%{state | timer_ref: timer_ref}
|> reset_round()
end
defp initialize_state do
%{
players: %{},
timer_ref: nil,
guessed: false
}
|> start_round()
end
defp get_public_state(state) do
%{
players: state.players,
guessed: state.guessed
}
end
defp reset_round(state) do
%{state | guessed: false}
end
# Callbacks to be implemented by specific games
def handle_move(_player_name, _move, state), do: {:reply, {:error, :not_implemented}, state}
defoverridable init: 1,
handle_info: 2,
start_round: 1,
get_public_state: 1,
reset_round: 1,
handle_move: 3
end
end
end
Now we’ll implement a specific game using this framework: NumberGuessingGame
. This game will generate a random number between 1 and 100 for each round, and players will try to guess it. The game will provide feedback on each guess (too high, too low, or correct), and award points for correct guesses.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
defmodule NumberGuessingGame do
use Game, round_time: 60_000
@max_number 100
def guess(player, number), do: make_move(player, number)
def handle_move(player_name, guess, state) do
cond do
state.guessed ->
{:reply, {:game_over, :already_guessed}, state}
guess == state.current_number ->
new_state =
state
|> update_in([:players, player_name], &(&1 + 1))
|> Map.put(:guessed, true)
Process.cancel_timer(state.timer_ref)
{:reply, {:correct, :winner}, start_round(new_state)}
guess < state.current_number ->
{:reply, {:incorrect, "the number is higher"}, state}
guess > state.current_number ->
{:reply, {:incorrect, "the number is lower"}, state}
end
end
def start_round(state) do
super(state)
end
def reset_round(state) do
number = round(:rand.uniform(@max_number))
state
|> super()
|> Map.put(:current_number, number)
end
def get_public_state(state) do
base_state = super(state)
Map.put(base_state, :max_number, @max_number)
end
end
We start the game and add two players:
1
2
3
{:ok, _pid} = NumberGuessingGame.start_link([])
NumberGuessingGame.join("Alice")
NumberGuessingGame.join("Bob")
We can check the current state:
1
NumberGuessingGame.get_state()
1
%{players: %{"Alice" => 0, "Bob" => 0}, guessed: false, max_number: 100}
Alice now makes a guess:
1
NumberGuessingGame.guess("Alice", 50)
1
{:incorrect, "the number is higher"}
Now Bob:
1
NumberGuessingGame.guess("Bob", 70)
1
{:incorrect, "the number is lower"}
Bob again:
1
NumberGuessingGame.guess("Bob", 60)
1
{:incorrect, "the number is higher"}
Alice:
1
NumberGuessingGame.guess("Alice", 65)
1
{:incorrect, "the number is lower"}
Alice again (and wins!):
1
NumberGuessingGame.guess("Alice", 62)
1
{:correct, :winner}
In conclusion, although macros are harder to debug, introduce complexity and are often more difficult to read, they are powerful. They enable us to reuse code and create flexible frameworks. Our Game module is one. They can be customized for specific uses, like the NumberGuessingGame. By combining use
and __using__
we can build modular, scalable and maintainable systems.