Kernel.SpecialForms 모듈

Kernel.SpecialForms 모듈

특수 형식(special forms)은 Elixir의 기본 구성요소라서 개발자가 재정의할 수 없어요.

Kernel.SpecialForms 모듈은 오직 매크로로만 이루어져 있고, Kernel.SpecialForms. 접두사 없이 Elixir 코드 어디서든 호출할 수 있어요. 이는 Kernel 모듈의 함수·매크로처럼 모두 자동 import되기 때문이에요.

일부 특수 형식은 렉시컬(lexical)이에요. 예를 들어 alias/2case/2가 그렇죠. {}/1<<>>/1 매크로는 각각 튜플과 바이너리 데이터 구조를 정의하는 특수 형식이에요.

이 모듈은 또한 Elixir 컴파일 환경에 대한 정보를 돌려주는 매크로(__ENV__/0, __MODULE__/0, __DIR__/0, __STACKTRACE__/0, __CALLER__/0)도 문서화해요. 그리고 개발자가 직접 부르기 위한 게 아니라 quote된 내용에 등장하는 __block__/1__aliases__/1 두 특수 형식도 다뤄요.

출처: Kernel.SpecialForms

본문

%/2 — 구조체 매칭·생성

구조체는 키에 기본값, 다형성 디스패치용 태그, 컴파일 타임 단언을 제공하는 tagged map이에요. 구조체는 보통 Kernel.defstruct/1로 정의해요.

defmodule User do
  defstruct name: "john", age: 27
end

이제 구조체를 이렇게 만들 수 있어요.

%User{}

내부적으로 구조체는 :__struct__ 키가 User 모듈을 가리키는 맵이며, 키는 컴파일 타임에 검증돼요.

%User{} == %{__struct__: User, name: "john", age: 27}

구조체를 만들 때 필드를 줄 수도 있고, 패턴 매칭으로 값을 꺼낼 수도 있어요.

%User{age: 31}
%User{age: age} = user

구조체의 장점은 주어진 키가 정의된 구조체의 일부인지 검증한다는 거예요. User 구조체에 :full_name 키가 없다면 아래는 실패해요.

%User{full_name: "john doe"}

맵 갱신 문법으로 구조체를 갱신할 수도 있어요.

%{user | age: 28}

구조체 이름에 패턴 매칭을 쓸 수도 있어요.

%struct_name{} = user
struct_name #=> User

& — 캡처 연산자

모듈에서 주어진 이름과 arity의 함수를 캡처하거나 익명 함수를 만드는 데 써요.

iex> fun = &Kernel.is_atom/1
iex> fun.(:atom)
true
iex> fun.("string")
false

모듈 이름을 생략하면 로컬(비공개 포함) 또는 import된 함수를 캡처할 수 있어요. &local_function/1은 로컬 캡처를 만들지만 &__MODULE__.local_function/1은 원격(remote) 캡처를 만들어요.

= — 매치 연산자

오른쪽 값을 왼쪽 패턴에 매칭해요.

^ — 핀 연산자

매치 절에서 이미 바인딩된 변수에 접근해요. Elixir는 정적 단일 배정(static single assignment)으로 변수를 다시 바인딩할 수 있는데, 어떤 상황에서는 다시 바인딩 대신 기존 값에 매칭하는 게 유용해요. 이때 ^를 쓰죠.

iex> x = 1
iex> ^x = List.first([1])
iex> ^x = List.first([2])
** (MatchError) no match of right hand side value:

^x는 항상 매칭 이전의 x 값을 가리켜요.

fn — 익명 함수

익명 함수를 정의해요. 자세한 내용은 Function 참고.

iex> add = fn a, b -> a + b end
iex> add.(1, 2)
3

익명 함수는 여러 절을 가질 수도 있고, 모든 절이 같은 수의 인자를 기대해요.

iex> negate = fn
...>   true -> false
...>   false -> true
...> end
iex> negate.(false)
true

quote — 표현식의 표현 얻기

어떤 표현식의 표현(AST)을 얻어요.

iex> quote do
...>   sum(1, 2, 3)
...> end
{:sum, [], [1, 2, 3]}

Elixir 코드는 모두 Elixir 데이터 구조로 표현될 수 있어요. 매크로의 구성요소는 세 요소로 된 튜플이에요. 위 튜플은 sum에 1, 2, 3을 인자로 넘기는 함수 호출을 나타내요. 튜플의 첫 요소는 항상 아톰 또는 같은 표현의 다른 튜플, 두 번째는 메타데이터, 세 번째는 호출 인자 목록이에요.

unquote — quote 안에서 표현식 재평가

quoted 표현식 안에서 주어진 표현식을 언쿼트해요. 유효한 Elixir AST를 인자로 기대하며, 맵이나 4요소 튜플 같은 임의 값을 언쿼트하려면 먼저 Macro.escape/1을 호출해야 해요.

value = quote do
  13
end

quote do
  sum(1, unquote(value), 3)
end

alias — 별칭 설정

모듈 이름의 별칭을 설정하는 데 주로 쓰여요.

defmodule Math do
  alias MyKeyword, as: Keyword
end

이제 Keyword 참조는 자동으로 MyKeyword로 대체돼요. 원래 Keyword에 접근하려면 Elixir.Keyword.values처럼 Elixir로 접근하면 돼요. :as 옵션 없이 호출하면 모듈의 마지막 부분을 기준으로 별칭이 자동 설정돼요.

case — 절 매칭

주어진 표현식을 주어진 절(clause)들에 매칭해요. 패턴 매칭과 가드를 사용해 어떤 절을 실행할지 골라요. 패턴·가드로 표현할 수 없는 논리라면 if/2cond/1을 고려하세요.

iex> string_date = "2015-01-23"
iex> case Date.from_iso8601(string_date) do
...>   {:ok, date} -> date
...>   {:error, _reason} -> Date.utc_today()
...> end
~D[2015-01-23]

cond — 조건 분기

모든 조건이 false일 때까지 조건 목록을 평가하다가 참인 첫 조건의 본문을 실행해요. case가 패턴 매칭 기반이라면 cond는 일반 불리언 조건 기반이에요.

with — 매칭 절 결합

여러 매칭 절을 결합해요. with가 개선하는 코드 패턴을 보면 가장 잘 이해돼요. 폭과 높이가 선택적인 맵에서 넓이를 {:ok, area}로 계산하거나 :error를 돌려준다고 해요. 이를 case로만 구현하면 이중으로 중첩된 case가 필요해요.

def area(opts) do
  with {:ok, width} <- Map.fetch(opts, :width),
       {:ok, height} <- Map.fetch(opts, :height) do
    {:ok, width * height}
  end
end

area(%{width: 10, height: 15}){:ok, 150}을 돌려줍니다.

for — 컴프리헨션

열거 가능한 구조나 비트열에서 빠르게 데이터 구조를 만드는 데 써요.

iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]

컴프리헨션은 여러 생성자(generator)와 필터를 받아요. <- 연산자로 오른쪽 열거형에서 값을 꺼내 왼쪽 패턴에 매칭하는데 이를 생성자라고 불러요. 필터도 줄 수 있어요.

iex> for x <- [1, 2], y <- [2, 3], do: x * y
[2, 3, 4, 6]
iex> for n <- [1, 2, 3, 4], rem(n, 2) == 0, do: n
[2, 4]

into: 옵션으로 결과를 다른 구조로 수집할 수도 있어요.

import, require, use

  • import/2 — 다른 모듈의 함수·매크로를 현재 스코프로 가져와요. only/except 옵션 지원.
  • require/2 — 매크로를 호출하기 전에 모듈이 로드·컴파일되게 해요.
  • use/2require 후 해당 모듈의 __using__/1을 호출해 코드를 주입해요.

receive — 메시지 소비

현재 프로세스 메일박스에서 주어진 절 중 하나에 매칭되는 첫 메시지를 소비해요. 매칭되는 메시지가 없으면 매칭 메시지가 도착하거나 타임아웃까지 기다려요. 매칭되지 않는 새·기존 메시지는 메일박스에 남아요.

iex> send(self(), {:selector, 5, :quantity})
iex> receive do
...>   {:selector, number, name} when is_integer(number) -> name
...>   name when is_atom(name) -> name
...> end
:quantity

매칭되는 메시지가 없을 경우를 대비해 선택적 after 절을 줄 수 있어요.

try — 예외 처리

주어진 표현식을 평가하고 발생했을 수 있는 error·exit·throw를 처리해요.

try do
  do_something_that_may_fail(some_arg)
rescue
  ArgumentError ->
    IO.puts("Invalid argument given")
catch
  value ->
    IO.puts("Caught #{inspect(value)}")
else
  value ->
    IO.puts("Success! The result was #{inspect(value)}")
after
  IO.puts("This is printed regardless if it failed or succeeded")
end

rescue 절은 예외를, catch 절은 throw된 값과 exit를 잡는 데 써요. else 절은 표현식 결과를 기반으로 흐름을 제어하고, after 절은 성공 여부와 무관하게 항상 실행돼요.

그 외 특수 형식

  • super/1Kernel.defoverridable/1로 함수를 오버라이드할 때 오버라이드된 함수를 호출해요.
  • ::/2 — 비트스트링 패턴에서 세그먼트 타입·크기를 지정해요.
  • . — 원격 호출 문법의 기반.
  • __ENV__/0, __MODULE__/0, __DIR__/0, __CALLER__/0, __STACKTRACE__/0 — 컴파일 환경 정보.
  • __block__/1, __aliases__/1 — quote된 내용에 등장하는 내부 특수 형식.
  • __cursor__/0 — IEx에서 쓰는 특수 형식.
  • {}/1, %{}/1, <<>>/1 — 각각 튜플, 맵, 비트스트링 리터럴.

더 알아보기

  • Kernel 모듈: 기본 함수·매크로
  • Macro 모듈: AST 조작 함수
  • quote·unquote 문서: 메타 프로그래밍의 핵심 개념
  • Meta-programming anti-patterns: 매크로 사용 시 피해야 할 함정