Posts Elixir and the Pin Operator
Post
Cancel

Elixir and the Pin Operator

The Problem

I am working on my first side project using Elixir and Phoenix. I was writing some tests and encountered a flaky unit test (randomly fails or passes). In this test, I wanted to make a case differentiation, but today I learned (TIL) in Elixir, the = behaves a bit differently. It is not an assignment operator; it is the match operator.

Pattern matching

When you use =, Elixir tries to match the value on the right side of the = with the pattern on the left side. If the pattern on the left does not match the value on the right, an error is raised.

1
2
3
x = 42      # Matches and binds the value 42 to the variable x
{a, b} = {1, 2}  # Matches and binds 1 to a, and 2 to b
[head | tail] = [1, 2, 3]  # Matches and binds head to 1, tail to [2, 3]

The pin operator ^ is used to assert that a variable on the left side of the pattern should match the value on the right side. It prevents the introduction of a new variable with the same name.

1
2
x = 42
^x = 42  # Matches because the value on the right matches the existing variable x

Without the pin operator ^, the second line would introduce a new variable named x rather than asserting that it should match the existing x.

The Code

In the given code snippet, we have a example test case that involves generating a new game response. The response includes information about the winner and loser of the game, identified by their respective user IDs.

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
defmodule ExampleTest do
  use ExUnit.Case

  def get_new_game_response() do
    random_number = :rand.uniform(10)

    if random_number <= 5 do
      %{
        "winner" => %{"user_id" => "foo"},
        "looser" => %{"user_id" => "bar"}
      }
    else
      %{
        "winner" => %{"user_id" => "bar"},
        "looser" => %{"user_id" => "foo"}
      }
    end
  end

  test "new game response flacky" do
    player_1 = "foo"
    player_2 = "bar"

    response = get_new_game_response()

    case response["winner"]["user_id"] do
      player_1 ->
        assert response["looser"]["user_id"] =~ player_2

      player_2 ->
        assert response["looser"]["user_id"] =~ player_1

      _ ->
        raise "Unexpected user_id"
    end
  end

  test "new game response with pin operator" do
    player_1 = "foo"
    player_2 = "bar"

    response = get_new_game_response()

    case response["winner"]["user_id"] do
      ^player_1 ->
        assert response["looser"]["user_id"] =~ player_2

      ^player_2 ->
        assert response["looser"]["user_id"] =~ player_1

      _ ->
        raise "Unexpected user_id"
    end
  end
end

Pattern Matching Without Pin Operator

Let’s look at the original version of the test case without using the pin operator:

1
2
3
4
5
6
7
8
9
10
case response["winner"]["user_id"] do
  player_1 ->
    assert response["looser"]["user_id"] =~ player_2

  player_2 ->
    assert response["looser"]["user_id"] =~ player_1

  _ ->
    raise "Unexpected user_id"
end

In this version, we attempt to match the user IDs of the winners (player_1 and player_2) and take corresponding actions. However, this code has a subtle issue that might not be immediately apparent.

A new variable binding occurs as this variable is used more than once in the same pattern. In our original code, both player_1 and player_2 are used as variables in the case pattern. So always the first case is executed, which randomly fails if player_2 is the winner but passes if not.

Using the Pin Operator

The pin operator (^) in Elixir is used to enforce a match against an existing variable’s value, preventing variable rebinding. Let’s revisit the modified test case that includes the pin operator:

1
2
3
4
5
6
7
8
9
10
case response["winner"]["user_id"] do
  ^player_1 ->
    assert response["looser"]["user_id"] =~ player_2

  ^player_2 ->
    assert response["looser"]["user_id"] =~ player_1

  _ ->
    raise "Unexpected user_id"
end

By using the pin operator, we explicitly state that we want to match the value of player_1 and player_2 against the corresponding user IDs without introducing new bindings. This prevents any unintentional variable rebinding and ensures that our pattern matching behaves as expected.

Resources

This post is licensed under CC BY 4.0 by the author.