Metaprogramming Elixir (Chris McCord, 2015) 독후감

3 minute read

강력한 매크로(macro)를 배우는 재미에 푹 빠졌다. 예제 코드가 다 훌륭하다.

José Valim, the creator of Elixir, chose to do something very different. He exposed the AST in a form that can be represented by Elixir’s own data structures and gave us a natural syntax to interact with it. [.] Having the AST accessible by normal Elixir code lets you do very powerful things because you can operate at the level typically reserved only for compilers and language designers. [.] Macros are code that writes code. Their purpose in life is to interact with the AST using Elixir’s high-level syntax.

AST(Abstract Syntax Tree)를 조작할 수 있다. 컴파일러가 조작할 수 있게 기능을 조금 열어주는 수준이 아니다. elixir 언어가 사용하는 자료구조로 된 AST를 elixir 함수로 조작할 수 있다. 그래서 강력한 metaprogramming이 가능하다. elixir의 많은 핵심 함수도 매크로로 구현했다. if 문(statement)도 매크로일 줄이야.

This also highlights an effective approach to macros, where the goal is to generate as little code as possible within the caller’s context. By proxying to an outside function, we keep the code generation as straightforward as possible.

매크로 작성 가이드라인이 좋다. 매크로 구현 코드가 길어지는 걸 경계해야 한다. 매크로 컨텍스트(context)와 호출자 컨텍스트(caller’s context)가 섞이게 돼서 코드 읽기가 힘들어진다. 함수 호출이 아니라 코드 생성 기능이 들어가니 디버깅도 힘들어진다. 그래서 매크로 내부 구현을 최대한 가볍게 만들어야 한다. 핵심 구현은 함수로 빼고 매크로에서는 함수 호출에 필요한 인자(argument)를 추출하고 가공하는 방식으로 구현하는 걸 추천한다. assert 매크로가 좋은 예이다. assert(operator, lhs, rhs) 같은 함수를 만들어 놓고 매크로에서는 함수 호출에 필요한 인자를 만들어 함수를 호출한다.

They are often used in places where constants would be applied in other languages, but Elixir provides other tricks for us to exploit during compilation. By taking advantage of the accumulate: true option when registering an attribute, we can keep an appended list of registrations during the compile phase.

따라하면서 가장 놀랐던 기능이다. Module.register_attribute/3 함수를 accumulate: true 옵션을 추가해 호출하면 모듈 속성(module attributes) 리스트를 만들 수 있다. 테스트케이스를 test 매크로를 사용해 정의하는데, 어떻게 테스트 케이스들이 자동으로 실행될까? 여기에 비밀이 숨겨져 있다. test 매크로에서 함수와 모듈 속성을 정의한다. 같은 모듈 속성 이름에 값을 테스트 케이스 이름을 사용한다. 모듈 속성을 리스트로 만든다. before_compile 이벤트 핸들러를 정의해 모듈 속성 리스트에 있는 테스트케이스를 모두 실행하는 함수를 추가한다.

파일로부터 함수를 생성하는 코드도 재미있다. 책에서 소개한 elixir 소스 코드를 찾아보니 31619줄의 UnicodeData.txt가 있다. 이 파일로 downcase, upcase 등의 함수를 만들어낸다. 패턴 매칭 덕에 함수 생성이 더 손쉽다. c++ 같은 언어로 코드를 만들어낸다면 switch 문을 작성해야 할 것 같다. 다른 언어라고 코드 생성을 못 할 건 없지만 패턴 매칭을 사용할 수 있는 elixir 코드 가독성이 더 좋다. elixir 모듈에서 파일을 읽고 순회하며 함수를 정의하면 된다.

HTML 태그 DSL(Domain-Specific Language)을 만드는 걸 보여준다. 트리랑 잘 어울리는 HTML은 프로덕션 코드에서는 찾아보기 힘든 이상적인 예제라 반칙이긴 하다. 영리하게 정의했다. div, h1, h2 같은 태그를 만드는 게 아니라 tag :div, tag :h1 처럼 사용할 수 있는 tag 매크로부터 만든다. 이렇게 tag 함수를 구현한 후 사용할 수 있는 html 태그 전체 목록이 있는 텍스트 파일을 구해서 이 파일로부터 태그를 만든다. html 중간 결과물은 Agent에 저장한다. 레벨이 다른 태그의 html 중간 결과물을 저장할 방법이 마땅찮아서 내린 결정 같다. 패턴 매칭으로 구현한 다음 Macro.postwalk/3 함수를 사용해 더 간단하게 구현한다. 풍부하고 재미있는 예제였다.

매크로 디버깅 팁도 배울 수 있었다. Macro.to_string/2 함수를 사용하거나 매크로를 quote로 감싼 다음 Macro.expand_once/2 함수를 사용해서 매크로 확장이 어떻게 되는지 보는 방법으로 매크로 코드를 디버깅할 수 있다. 테스트 챕터는 별것 없었다. 만들어내는 AST가 맞는지 극세사 테스트를 하지 말고 만들어내는 코드가 의도대로 돌아가는지 테스트하란 얘기를 한다.

밑줄

  • José Valim, the creator of Elixir, chose to do something very different. He exposed the AST in a form that can be represented by Elixir’s own data structures and gave us a natural syntax to interact with it. [.] Having the AST accessible by normal Elixir code lets you do very powerful things because you can operate at the level typically reserved only for compilers and language designers. [.] Macros are code that writes code. Their purpose in life is to interact with the AST using Elixir’s high-level syntax.
  • Elixir has the concept of macro hygiene. Hygiene means that variables, imports, and aliases that you define in a macro do not leak into the caller’s own definitions.
  • This also highlights an effective approach to macros, where the goal is to generate as little code as possible within the caller’s context. By proxying to an outside function, we keep the code generation as straightforward as possible.