Quote and unquote
Quote and unquote
이 가이드는 Elixir에서 사용할 수 있는 메타프로그래밍(meta-programming) 기법을 소개하기 위한 문서예요. Elixir 프로그램을 자기 자신의 데이터 구조로 표현하는 능력이 메타프로그래밍의 핵심입니다. 이 장에서는 먼저 그 구조와 관련된 quote/2, unquote/1 구성 요소를 탐구한 뒤, 다음 가이드에서 매크로를 살펴보고 마지막에는 우리만의 도메인 특화 언어(DSL)를 만들어 볼 거예요.
본문
Quoting
Elixir 프로그램의 구성 단위는 세 개의 요소로 이루어진 튜플이에요. 예를 들어 함수 호출 sum(1, 2, 3)은 내부적으로 다음과 같이 표현됩니다.
{:sum, [], [1, 2, 3]}
quote/2 매크로를 쓰면 어떤 표현식의 표현이라도 얻을 수 있어요.
iex> quote do: sum(1, 2, 3)
{:sum, [], [1, 2, 3]}
첫 번째 요소는 함수 이름, 두 번째는 메타데이터를 담은 키워드 리스트, 세 번째는 인자 리스트입니다.
연산자도 그런 튜플로 표현됩니다.
iex> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}
심지어 맵도 %{}에 대한 호출로 표현돼요.
iex> quote do: %{1 => 2}
{:%{}, [], [{1, 2}]}
변수는 같은 삼중 튜플로 표현되는데, 마지막 요소가 리스트가 아니라 아톰(atom)이라는 차이가 있어요.
iex> quote do: x
{:x, [], Elixir}
더 복잡한 표현식을 quote하면 코드가 그런 튜플들로 표현되고, 그것들이 서로 중첩되어 나무(tree)와 비슷한 구조를 이루는 걸 볼 수 있어요. 많은 언어가 이런 표현을 **추상 구문 트리(Abstract Syntax Tree, AST)**라고 부릅니다. Elixir는 이걸 quoted expressions(인용된 표현식)라고 해요.
iex> quote do: sum(1, 2 + 3, 4)
{:sum, [], [1, {:+, [context: Elixir, import: Kernel], [2, 3]}, 4]}
가끔은 quoted expression으로 작업할 때 텍스트 코드 표현을 다시 얻는 게 유용할 수 있어요. 이는 Macro.to_string/1로 할 수 있습니다.
iex> Macro.to_string(quote do: sum(1, 2 + 3, 4))
"sum(1, 2 + 3, 4)"
일반적으로 위 튜플들은 다음 형식에 따라 구조화됩니다.
{atom | tuple, list, list | atom}
- 첫 번째 요소는 아톰 또는 같은 표현 형식의 다른 튜플
- 두 번째 요소는 숫자, 컨텍스트 같은 메타데이터를 담은 키워드 리스트
- 세 번째 요소는 함수 호출의 인자 리스트 또는 아톰. 이 요소가 아톰이면 그 튜플이 변수를 나타낸다는 뜻입니다.
위에서 정의한 튜플 외에도, quote했을 때 자기 자신을(튜플이 아니라) 그대로 반환하는 Elixir 리터럴이 다섯 가지 있어요.
:sum #=> Atoms
1.0 #=> Numbers
[1, 2] #=> Lists
"strings" #=> Strings
{key, value} #=> Tuples with two elements
대부분의 Elixir 코드는 기저의 quoted expression으로 직관적으로 번역됩니다. 다양한 코드 샘플을 직접 시도해 보고 결과를 확인해 보길 권해요. 예를 들어 String.upcase("foo")는 무엇으로 확장될까요? 또 우리는 if(true, do: :this, else: :that)가 if true do :this else :that end와 같다는 것도 배웠죠. 이 주장이 quoted expression에서는 어떻게 성립할까요?
Unquoting
Quoting은 어떤 코드 조각의 내부 표현을 가져오는 것이에요. 하지만 때로는 가져오려는 표현 안에 다른 특정 코드 조각을 주입(inject)해야 할 수도 있습니다.
예를 들어, quoted expression 안에 주입하고 싶은 숫자를 담은 number 변수가 있다고 상상해 보세요.
iex> number = 13
iex> Macro.to_string(quote do: 11 + number)
"11 + number"
이건 우리가 원한 게 아니에요. number 변수의 값이 주입되지 않고, number가 표현식 안에서 그대로 quote되었기 때문이죠. number 변수의 값을 주입하려면 quoted representation 안에서 unquote/1을 사용해야 합니다.
iex> number = 13
iex> Macro.to_string(quote do: 11 + unquote(number))
"11 + 13"
unquote/1은 함수 이름을 주입하는 데까지 쓸 수 있어요.
iex> fun = :hello
iex> Macro.to_string(quote do: unquote(fun)(:world))
"hello(:world)"
어떤 경우에는 리스트 안에 여러 값을 주입해야 할 수도 있어요. 예를 들어 [1, 2, 6]이 담긴 리스트가 있고 여기에 [3, 4, 5]를 주입하고 싶다고 해 보죠. unquote/1을 쓰면 원하는 결과가 나오지 않아요.
iex> inner = [3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote(inner), 6])
"[1, 2, [3, 4, 5], 6]"
바로 이때 unquote_splicing/1이 유용해집니다.
iex> inner = [3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote_splicing(inner), 6])
"[1, 2, 3, 4, 5, 6]"
Unquoting은 매크로를 쓸 때 매우 유용해요. 매크로를 작성할 때 개발자는 코드 조각을 받아 다른 코드 조각 안에 주입할 수 있는데, 이는 코드를 변환하거나 컴파일 중에 코드를 생성하는 코드를 쓰는 데 사용될 수 있습니다.
Escaping
이 장의 시작에서 봤듯이, Elixir에서 유효한 quoted expression은 일부 값뿐이에요. 예를 들어 맵은 유효한 quoted expression이 아닙니다. 네 개의 요소를 가진 튜플도 마찬가지고요. 하지만 그런 값도 quoted expression으로 표현될 수는 있어요.
iex> quote do: %{1 => 2}
{:%{}, [], [{1, 2}]}
어떤 경우에는 그런 값을 quoted expression 안에 주입해야 할 수도 있어요. 그러려면 먼저 Macro.escape/1의 도움으로 그 값들을 quoted expression으로 이스케이프해야 합니다.
iex> map = %{hello: :world}
iex> Macro.escape(map)
{:%{}, [], [hello: :world]}
매크로는 quoted expression을 받고 quoted expression을 반환해야 해요. 하지만 매크로 실행 중에 값으로 작업해야 할 때가 있고, 그럴 때는 값과 quoted expression을 구분하는 게 필요합니다.
다시 말해, 일반 Elixir 값(리스트, 맵, 프로세스, 레퍼런스 등)과 quoted expression을 구분하는 게 중요해요. 정수, 아톰, 문자열 같은 일부 값은 값 자체와 같은 quoted expression을 가집니다. 맵 같은 다른 값은 명시적으로 변환해야 해요. 마지막으로 함수와 레퍼런스 같은 값은 아예 quoted expression으로 변환할 수 없습니다.
매크로와 코드를 생성하는 코드를 다룰 때는 Elixir의 AST로 작업하는 많은 함수를 담고 있는 Macro 모듈의 문서를 확인해 보세요.
이번 소개에서 우리는 마침내 첫 매크로를 작성하기 위한 기반을 다졌어요. 다음 가이드에서 그걸 확인할 수 있습니다.
더 알아보기
Macro— AST 작업 함수 모듈quote/2— 표현식의 내부 표현을 얻는 매크로unquote/1— quoted expression에 값을 주입하는 매크로unquote_splicing/1— 리스트를 펼쳐 주입하는 매크로Macro.escape/1— 값을 quoted expression으로 변환