Post

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() returns 1
  • MyModuleA.two() returns 2

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() returns 2 (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:

  1. Create a reusable game structure with __using__.
  2. Use a GenServer for managing game state and handling concurrent operations (within the reusable game).
  3. 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.