Kernel.SpecialForms 모듈
Kernel.SpecialForms 모듈
특수 형식(special forms)은 Elixir의 기본 구성요소라서 개발자가 재정의할 수 없어요.
Kernel.SpecialForms 모듈은 오직 매크로로만 이루어져 있고, Kernel.SpecialForms. 접두사 없이 Elixir 코드 어디서든 호출할 수 있어요. 이는 Kernel 모듈의 함수·매크로처럼 모두 자동 import되기 때문이에요.
일부 특수 형식은 렉시컬(lexical)이에요. 예를 들어 alias/2와 case/2가 그렇죠. {}/1와 <<>>/1 매크로는 각각 튜플과 바이너리 데이터 구조를 정의하는 특수 형식이에요.
이 모듈은 또한 Elixir 컴파일 환경에 대한 정보를 돌려주는 매크로(__ENV__/0, __MODULE__/0, __DIR__/0, __STACKTRACE__/0, __CALLER__/0)도 문서화해요. 그리고 개발자가 직접 부르기 위한 게 아니라 quote된 내용에 등장하는 __block__/1과 __aliases__/1 두 특수 형식도 다뤄요.
본문
%/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/2나 cond/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/2—require후 해당 모듈의__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/1—Kernel.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: 매크로 사용 시 피해야 할 함정