Patterns and guards

Patterns and guards

Elixir는 **패턴 매칭(pattern matching)**을 제공해서 데이터 구조의 모양을 확인하거나 값을 추출할 수 있게 해 줍니다. 패턴은 종종 **가드(guard)**로 보강되는데, 가드를 쓰면 조금 더 복잡한(하지만 제한된) 검사를 수행할 수 있어요. 이 문서는 패턴과 가드의 완전한 참고 자료로, 그 의미(semantics), 어디에서 허용되는지, 그리고 어떻게 확장하는지를 다룹니다.

출처: Patterns and guards

본문

Patterns (패턴)

Elixir의 패턴은 변수, 리터럴, 데이터 구조 특유의 문법으로 만들어져요. 패턴 매칭을 수행하는 데 가장 자주 쓰이는 구성 요소 중 하나가 매치 연산자(=)입니다.

iex> x = 1
1
iex> 1 = x
1

위 예시에서 x는 값을 갖지 않고 시작해 1이 할당됩니다. 그다음 x의 값을 리터럴 1과 비교하면 둘 다 1이므로 성공해요.

x를 2와 매칭하면 예외가 발생합니다.

iex> 2 = x
** (MatchError) no match of right hand side value: 1

패턴은 양방향이 아니에요. 한 번도 할당되지 않은 변수(흔히 unbound variable이라 함) y가 있을 때 1 = y라고 쓰면 오류가 발생합니다.

iex> 1 = y
** (CompileError) iex:2: undefined variable "y"

다시 말해 패턴은 =의 왼쪽에서만 허용됩니다. =의 오른쪽은 언어의 일반적인 평가 의미를 따릅니다.

이제 각 구성 요소에 대한 패턴 매칭 규칙을, 그다음 관련 데이터 타입 각각에 대해 살펴볼게요.

Variables (변수)

패턴 안의 변수는 항상 할당됩니다.

iex> x = 1
1
iex> x = 2
2
iex> x
2

즉 Elixir는 재바인딩(rebinding)을 지원합니다. 변수 값을 바꾸고 싶지 않다면 핀 연산자(pin operator, ^)를 쓸 수 있어요.

iex> x = 1
1
iex> ^x = 2
** (MatchError) no match of right hand side value: 2

같은 변수가 같은 패턴 안에 여러 번 나타나면 모두 같은 값에 바인딩되어야 합니다.

iex> {x, x} = {1, 1}
{1, 1}
iex> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}

하지만 변수는 같은 패턴 안에서 자기 자신을 통해서 정의될 수 없어요. 여기에는 변수가 중첩된 매치 연산자의 양쪽에 나타나는 경우도 포함됩니다.

{:ok, x} = {x, :ok}
%{a: value} = %{b: value}

이런 패턴은 순환 변수 정의(cyclic variable definitions)로 거부됩니다. 한 번 매칭한 뒤 나머지 제약 조건은 가드나 별도의 비교로 다시 써야 해요.

밑줄 변수(_)는 어떤 값에도 결코 바인딩될 수 없다는 특별한 의미를 가져요. 패턴에서 특정 값을 신경 쓰지 않을 때 특히 유용합니다.

iex> {_, integer} = {:not_important, 1}
{:not_important, 1}
iex> integer
1
iex> _
** (CompileError) iex:3: invalid use of _

핀된 값(pinned value)은 값 그 자체를 나타내며, 문법적으로 같더라도 그 패턴을 나타내지 않아요. 오른쪽은 핀된 값과 같도록 비교됩니다.

iex> x = %{}
%{}
iex> {:ok, %{}} = {:ok, %{a: 13}}
{:ok, %{a: 13}}
iex> {:ok, ^x} = {:ok, %{a: 13}}
** (MatchError) no match of right hand side value: {:ok, %{a: 13}}

Tuples (튜플)

튜플은 패턴에서 중괄호 문법({})으로 나타날 수 있어요. 패턴 안의 튜플은 크기가 같은 튜플하고만 매치되며, 각 튜플 요소도 각각 매치되어야 합니다.

iex> {:ok, integer} = {:ok, 13}
{:ok, 13}

# 크기가 달라서 매치되지 않음
iex> {:ok, integer} = {:ok, 11, 13}
** (MatchError) no match of right hand side value: {:ok, 11, 13}

# 첫 번째 요소가 달라서 매치되지 않음
iex> {:ok, binary} = {:error, :enoent}
** (MatchError) no match of right hand side value: {:error, :enoent}

Lists (리스트)

리스트는 패턴에서 대괄호 문법([])으로 나타날 수 있어요. 패턴 안의 리스트는 크기가 같은 리스트하고만 매치되며, 각 리스트 요소도 각각 매치되어야 합니다.

iex> [:ok, integer] = [:ok, 13]
[:ok, 13]

# 크기가 달라서 매치되지 않음
iex> [:ok, integer] = [:ok, 11, 13]
** (MatchError) no match of right hand side value: [:ok, 11, 13]

# 첫 번째 요소가 달라서 매치되지 않음
iex> [:ok, binary] = [:error, :enoent]
** (MatchError) no match of right hand side value: [:error, :enoent]

튜플과 달리, 리스트는 [head | tail] 표기법으로 비어 있지 않은 리스트에 매칭할 수도 있어요. 이 표기법은 리스트의 headtail에 매칭됩니다.

iex> [head | tail] = [1, 2, 3]
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]

여러 요소가 | tail 구성 앞에 올 수 있어요.

iex> [first, second | tail] = [1, 2, 3]
[1, 2, 3]
iex> tail
[3]

[head | tail]은 빈 리스트에는 매치되지 않음을 주의하세요.

iex> [head | tail] = []
** (MatchError) no match of right hand side value: []

캐릭리스트(charlist)는 정수들의 리스트로 표현되므로, 리스트 결합 연산자(++)로 캐릭리스트에도 접두사 매칭을 수행할 수 있어요.

iex> ~c"hello " ++ world = ~c"hello world"
~c"hello world"
iex> world
~c"world"

이는 [?h, ?e, ?l, ?l, ?o, ?\s | world]에 매칭하는 것과 동일합니다. 접미사 매칭(hello ++ ~c" world")은 유효한 패턴이 아니에요.

Maps (맵)

맵은 패턴에서 퍼센트 기호 뒤 중괄호 문법(%{})으로 나타날 수 있어요. 리스트·튜플과 달리 맵은 부분 집합 매칭(subset match)을 수행합니다. 즉 맵 패턴은 그 안에 있는 키를 적어도 모두 가진 다른 맵과 매치된다는 뜻입니다.

모든 키가 매치되는 예시:

iex> %{name: name} = %{name: "meg"}
%{name: "meg"}
iex> name
"meg"

키의 부분 집합이 매치되는 경우:

iex> %{name: name} = %{name: "meg", age: 23}
%{age: 23, name: "meg"}
iex> name
"meg"

패턴의 키가 맵에 없으면 매치되지 않아요.

iex> %{name: name, age: age} = %{name: "meg"}
** (MatchError) no match of right hand side value: %{name: "meg"}

빈 맵은 모든 맵에 매치된다는 점에 유의하세요. 이는 빈 튜플이나 빈 리스트가 각각 빈 튜플·빈 리스트에만 매치되는 튜플·리스트와 대조됩니다.

iex> %{} = %{name: "meg"}
%{name: "meg"}

마지막으로, 패턴 안의 맵 키는 항상 리터럴이거나 핀 연산자로 매칭된 이전에 바인딩된 변수여야 합니다.

Structs (구조체)

구조체는 패턴에서 퍼센트 기호 뒤에 구조체 모듈 이름 또는 변수, 그리고 중괄호 문법(%{})으로 나타날 수 있어요.

다음 구조체가 있다고 가정해 보세요.

defmodule User do
  defstruct [:name]
end

모든 키가 매치되는 예시:

iex> %User{name: name} = %User{name: "meg"}
%User{name: "meg"}
iex> name
"meg"

알 수 없는 키가 주어지면 컴파일러가 오류를 발생시킵니다.

iex> %User{type: type} = %User{name: "meg"}
** (CompileError) iex: unknown key :type for struct User

모듈 이름 대신 변수를 넣으면 구조체 이름을 추출할 수 있어요.

iex> %struct_name{} = %User{name: "meg"}
%User{name: "meg"}
iex> struct_name
User

Binaries (바이너리)

바이너리는 패턴에서 이중 꺾쇠 문법(<<>>)으로 나타날 수 있어요. 패턴 안의 바이너리는 서로 다른 타입·크기·단위를 가진 여러 세그먼트를 동시에 매치할 수 있습니다.

iex> <<val::unit(8)-size(2)-integer>> = <<123, 56>>
"{8"
iex> val
31544

바이너리의 패턴 매칭에 대한 완전한 정의는 <<>> 문서를 참고하세요.

마지막으로, Elixir의 문자열은 UTF-8로 인코딩된 바이너리라는 점을 기억하세요. 즉 캐릭리스트와 비슷하게, 바이너리 결합 연산자(<>)로 문자열에도 접두사 매칭이 가능합니다.

iex> "hello " <> world = "hello world"
"hello world"
iex> world
"world"

접미사 매칭(hello <> " world")은 유효한 패턴이 아니에요.

Guards (가드)

가드는 패턴 매칭을 더 복잡한 검사로 보강하는 방법이에요. 함수 정의, case 절 등 패턴 매칭이 허용되는 미리 정의된 구성 요소 집합에서 허용됩니다.

가드 절에는 모든 표현식이 아니라 일부만 허용돼요. 이는 의도적인 선택입니다. 이렇게 함으로써 Elixir는(Erlang을 통해) 모든 가드가 예측 가능하고(변이나 다른 부작용 없음) 효율적으로 최적화·수행될 수 있게 보장합니다.

허용되는 함수·연산자 목록

내장 가드 목록은 Kernel 모듈에서 찾을 수 있어요. 개요는 다음과 같습니다.

  • 비교 연산자(==, !=, ===, !==, <, <=, >, >=), max, min
  • 엄격한 불리언 연산자(and, or, not). &&, ||, ! 형제 연산자는 허용되지 않아요 — 그들은 엄격히 불리언이 아니어서 인자가 불리언일 것을 요구하지 않기 때문입니다
  • 산술 단항 연산자(+, -)
  • 산술 이항 연산자(+, -, *, /)
  • in, not in 연산자 (오른쪽이 리스트나 범위인 한)
  • "타입 확인" 함수(is_list/1, is_number/1 등)
  • 내장 데이터 타입에 동작하는 함수(abs/1, hd/1, map_size/1 등)
  • map.field 문법

Bitwise 모듈도 Erlang 비트 연산 몇 가지를 가드로 포함합니다.

위 가드들의 어떤 조합으로 만들어진 매크로도 유효한 가드예요 — 예를 들어 Integer.is_even/1. 자세한 내용은 아래 "커스텀 패턴·가드 표현식" 섹션을 참고하세요.

왜 가드인가

함수 절에 쓰인 가드 예시를 봅시다.

def empty_map?(map) when map_size(map) == 0, do: true
def empty_map?(map) when is_map(map), do: false

가드는 when 연산자로 시작하고 그 뒤에 가드 표현식이 와요. 가드 표현식이 true를 반환할 때만 그 절이 실행됩니다. 여러 불리언 조건은 and, or 연산자로 결합할 수 있어요.

empty_map?/1 함수를 패턴 매칭만으로 쓰는 것은 불가능합니다(%{}에 매칭하면 빈 맵뿐 아니라 어떤 맵에도 매치되기 때문).

통과하지 못하는 가드(Non-passing guards)

가드 표현식이 true로 평가될 때만 함수 절이 실행돼요. 다른 값이 반환되면 그 함수 절은 건너뜁니다. 특히 가드에는 "truthy"나 "falsy"라는 개념이 없습니다.

예를 들어 리스트의 head가 nil이 아닌지 확인하는 함수를 생각해 보세요.

def not_nil_head?([head | _]) when head, do: true
def not_nil_head?(_), do: false

not_nil_head?(["some_value", "another_value"])
#=> false

리스트의 head가 nil이 아님에도 not_nil_head?/1의 첫 번째 절은 실패해요. 그 표현식이 true가 아니라 "some_value"로 평가되기 때문이죠. 그래서 false를 반환하는 두 번째 절이 발동됩니다. 가드가 제대로 동작하게 하려면 가드가 true로 평가되도록 해야 해요.

def not_nil_head?([head | _]) when head != nil, do: true
def not_nil_head?(_), do: false

not_nil_head?(["some_value", "another_value"])
#=> true

가드에서의 오류(Errors in guards)

가드에서 함수가 보통 예외를 발생시키면, 가드는 대신 실패하게 됩니다.

예를 들어 tuple_size/1은 튜플에서만 동작해요. 다른 것에 쓰면 인자 오류가 발생합니다.

iex> tuple_size("hello")
** (ArgumentError) argument error

하지만 가드에 쓰이면, 오류를 발생시키는 대신 해당 절의 매칭이 실패합니다.

iex> case "hello" do
...>   something when tuple_size(something) == 2 ->
...>     :worked
...>   _anything_else ->
...>     :failed
...> end
:failed

많은 경우 이 특성을 활용할 수 있어요. 위 코드에서 tuple_size/1로 주어진 값이 튜플인지 확인하는 것과 그 크기를 확인하는 것을 동시에 했죠(is_tuple(something) and tuple_size(something) == 2를 쓰는 대신).

하지만 가드에 튜플이나 맵을 확인하는 것 같은 여러 조건이 있다면, tuple_size/1 앞에 is_tuple/1 같은 타입 확인 함수를 호출하는 게 가장 좋아요. 그렇지 않으면 튜플이 주어지지 않을 때 가드 전체가 실패하거든요. 또는 다음 섹션에서 보여주듯 함수 절에 여러 가드를 쓸 수도 있습니다.

같은 절의 여러 가드(Multiple guards in the same clause)

가드에서 or 표현식 체인을 단순화하는 추가 방법이 있어요: Elixir는 같은 절에 "여러 가드"를 쓰는 것을 지원합니다. 다음 코드는:

def categorize_number(term) when is_integer(term) or is_float(term) or is_nil(term),
  do: :maybe_number
def categorize_number(_other),
  do: :something_else

다음처럼 쓸 수도 있습니다.

def categorize_number(term)
    when is_integer(term)
    when is_float(term)
    when is_nil(term) do
  :maybe_number
end

def categorize_number(_other) do
  :something_else
end

각 가드 표현식이 항상 불리언을 반환한다면 두 형태는 동일해요. 하지만 가드 안의 어떤 함수 호출이 예외를 발생시키면 가드 전체가 실패한다는 점을 기억하세요. 이를 보여주기 위해 다음 함수는 빈 튜플을 감지하지 못합니다.

defmodule Check do
  # 튜플이 주어지면 map_size/1이 예외를 발생시키고 tuple_size/1은 평가되지 않음
  def empty?(val) when map_size(val) == 0 or tuple_size(val) == 0, do: true
  def empty?(_val), do: false
end

Check.empty?(%{})
#=> true

Check.empty?({})
#=> false # true가 기대됐다!

이는 is_map(val) and map_size(val) == 0 같은 타입 확인으로 예외가 발생하지 않게 하거나, 여러 가드를 써서 예외가 한 가드를 실패시키면 다음 가드가 평가되게 함으로써 고칠 수 있어요.

defmodule Check do
  # 튜플이 주어지면 map_size/1이 예외를 발생시키고 두 번째 가드가 평가됨
  def empty?(val)
      when map_size(val) == 0
      when tuple_size(val) == 0,
      do: true

  def empty?(_val), do: false
end

Check.empty?(%{})
#=> true

Check.empty?({})
#=> true

패턴과 가드를 사용할 수 있는 곳

위 예시에서는 매치 연산자(=)와 함수 절로 각각 패턴과 가드를 보여줬어요. 여기 패턴과 가드를 지원하는 Elixir의 내장 구성 요소 목록이 있습니다.

  • match?/2:
match?({:ok, value} when value > 0, {:ok, 13})
  • 함수 절:
def type(term) when is_integer(term), do: :integer
def type(term) when is_float(term), do: :float
  • case 표현식:
case x do
  1 -> :one
  2 -> :two
  n when is_integer(n) and n > 2 -> :larger_than_two
end
  • 익명 함수(fn/1):
larger_than_two? = fn
  n when is_integer(n) and n > 2 -> true
  n when is_integer(n) -> false
end
  • forwith<-의 왼쪽에서 패턴과 가드를 지원합니다:
for x when x >= 0 <- [1, -2, 3, -4], do: x

with는 패턴 매칭과 가드를 지원하는 else 키워드도 지원해요.

  • trycatchelse에서 패턴과 가드를 지원합니다
  • receive는 받은 메시지에 매칭하기 위해 패턴과 가드를 지원합니다
  • 커스텀 가드는 defguard/1defguardp/1로도 정의할 수 있어요. 커스텀 가드는 기존 가드를 기반으로만 정의할 수 있습니다.

매치 연산자(=)는 가드를 지원하지 않는다는 점에 유의하세요.

{:ok, binary} = File.read("some/file")

커스텀 패턴·가드 표현식

이 페이지에 나열된 구성 요소만 패턴과 가드에서 허용돼요. 하지만 매크로를 활용해 프로그램을 단순화하거나 더 도메인 특화되게 만드는 커스텀 패턴 가드를 작성할 수 있습니다. 결국 중요한 것은 매크로의 출력이 위 구성 요소들의 조합으로 귀결된다는 점이에요.

예를 들어 Elixir의 Record 모듈은 패턴과 가드에 쓰는 일련의 매크로를 제공해서, 컴파일 중에 튜플이 이름 있는 필드를 가지게 합니다.

자신만의 가드를 정의하려면 Elixir는 defguarddefguardp에서 편의 기능을 제공하기도 해요. 간단한 사례 연구를 봅시다: 인자가 짝수 정수인지 홀수 정수인지 확인하고 싶다고 해 보죠. 패턴 매칭으로는 불가능해요. 정수가 무한히 많아서 모든 정수에 패턴 매칭할 수 없기 때문이에요. 그래서 가드를 써야 합니다. 홀수 확인은 거의 동일하므로 짝수 확인에만 집중할게요.

그런 가드는 이렇게 생겼어요.

def my_function(number) when is_integer(number) and rem(number, 2) == 0 do
  # do stuff
end

이 확인이 필요할 때마다 쓰면 반복적일 거예요. 대신 defguard/1defguardp/1을 써서 가드 매크로를 만들 수 있습니다. 예시는 다음과 같아요.

defmodule MyInteger do
  defguard is_even(term) when is_integer(term) and rem(term, 2) == 0
end

그리고:

import MyInteger, only: [is_even: 1]

def my_function(number) when is_even(number) do
  # do stuff
end

매크로로 커스텀 가드를 만드는 것이 가능하지만, 추가적인 컴파일 타임 검사를 수행하는 defguard/1defguardp/1을 써서 정의하는 걸 권장해요.

더 알아보기

  • Kernel 가드 — 내장 가드 함수·연산자 목록
  • defguard/1 — 커스텀 가드 정의 매크로
  • match?/2 — 패턴 매칭을 불리언으로 평가
  • case — 패턴 매칭 제어 구조
  • <<>>/1 — 바이너리 패턴 매칭 정의