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.
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.