thacoon's Blog

Elixir and the Pin Operator

· thacoon

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.

1x = 42      # Matches and binds the value 42 to the variable x
2{a, b} = {1, 2}  # Matches and binds 1 to a, and 2 to b
3[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.

1x = 42
2^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.

 1defmodule ExampleTest do
 2  use ExUnit.Case
 3
 4  def get_new_game_response() do
 5    random_number = :rand.uniform(10)
 6
 7    if random_number <= 5 do
 8      %{
 9        "winner" => %{"user_id" => "foo"},
10        "looser" => %{"user_id" => "bar"}
11      }
12    else
13      %{
14        "winner" => %{"user_id" => "bar"},
15        "looser" => %{"user_id" => "foo"}
16      }
17    end
18  end
19
20  test "new game response flacky" do
21    player_1 = "foo"
22    player_2 = "bar"
23
24    response = get_new_game_response()
25
26    case response["winner"]["user_id"] do
27      player_1 ->
28        assert response["looser"]["user_id"] =~ player_2
29
30      player_2 ->
31        assert response["looser"]["user_id"] =~ player_1
32
33      _ ->
34        raise "Unexpected user_id"
35    end
36  end
37
38  test "new game response with pin operator" do
39    player_1 = "foo"
40    player_2 = "bar"
41
42    response = get_new_game_response()
43
44    case response["winner"]["user_id"] do
45      ^player_1 ->
46        assert response["looser"]["user_id"] =~ player_2
47
48      ^player_2 ->
49        assert response["looser"]["user_id"] =~ player_1
50
51      _ ->
52        raise "Unexpected user_id"
53    end
54  end
55end

Pattern Matching Without Pin Operator

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

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

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:

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

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

#elixir

Reply to this post by email ↪