추상 구문 트리(AST)는 _인용 표현식_이라고도 하며, 코드를 데이터로 표현하는 방법이에요.
AST의 각 노드는 요소가 세 개인 튜플이에요.
# AST representation of:
# 2 + 3
{:+, [], [2, 3]}
첫 번째 요소인 아톰은 연산을 나타내요. 두 번째 요소인 키워드 리스트는 메타데이터예요. 세 번째 요소는 다른 노드들을 담고 있는 인자 목록이에요. 정수, 아톰, 문자열 같은 리터럴 값은 요소가 세 개인 튜플 대신 AST 안에서 자기 자신으로 표현돼요.
Elixir 코드를 AST로, AST를 다시 코드로 바꾸는 일은 표준 라이브러리에 포함돼 있어요. AST를 다루는 함수는 Code 모듈(예: 코드가 담긴 문자열을 AST로 바꾸기)과 Macro 모듈(예: AST를 순회하거나 문자열로 바꾸기)에서 찾을 수 있어요.
표준 라이브러리의 모든 함수는 AST를 가리킬 때 "quoted"라는 이름을 사용한다는 점을 기억해 두세요(_quoted expression_의 준말이에요).
코드를 AST로 바꾸는 특수 폼은 quote라고 해요. 코드 블록을 받아서 그 AST를 반환해요.
quote do
2 + 3 - 1
end
# => {:-, [], [
# {:+, [], [2, 3]},
# 1
# ]}
코드를 AST로 표현하는 능력은 Elixir 메타프로그래밍의 핵심이에요. Elixir 코드를 만들어 내는 Elixir 코드를 작성하는 방법인 _매크로_는 결과로 AST를 반환하는 방식으로 동작해요.
AST의 또 다른 활용 사례는 정적 코드 분석이에요. Exercism 자체 도구인 Analyzer가 그 예인데, 여러분의 풀이에 댓글을 남기는 작은 봇이라 이미 익숙할지도 몰라요.
여러분은 기업 스파이 활동에 맞서 싸우는 태스크 포스의 일원이에요. 경쟁사의 비밀을 훔치는 게 의심되는 Shady Company X에 비밀 정보원을 심어 두었어요.
여러분의 정보원인 Agent Ex는 엘릭서 개발자예요. 그녀는 자신의 코드에 비밀 메시지를 인코딩해요.
그녀의 비밀 메시지를 해독하려면:
n개 문자를 가져와요. 여기서 n은 그 함수의 인자 개수예요.TopSecret.to_ast/1 함수를 구현해요. 엘릭서 코드가 담긴 문자열을 받아 그 AST를 반환해야 해요.
TopSecret.to_ast("div(4, 3)")
# => {:div, [line: 1], [4, 3]}
TopSecret.decode_secret_message_part/2 함수를 구현해요. AST 노드와 비밀 메시지를 위한 누산기(리스트)를 받아요. 첫 번째 요소는 그대로 둔 AST 노드이고 두 번째 요소는 누산기인 튜플을 반환해야 해요.
AST 노드의 연산이 함수를 정의하는 것(def 또는 defp)이라면, 함수 이름을 문자열로 바꿔 누산기 앞에 붙여요. 연산이 다른 것이라면 누산기를 그대로 반환해요.
ast_node = TopSecret.to_ast("defp cat(a, b, c), do: nil")
TopSecret.decode_secret_message_part(ast_node, ["day"])
# => {ast_node, ["cat", "day"]}
ast_node = TopSecret.to_ast("10 + 3")
TopSecret.decode_secret_message_part(ast_node, ["day"])
# => {ast_node, ["day"]}
이 함수는 전체 AST를 확인하기 위해 재귀 호출을 할 필요가 없고, 주어진 노드만 확인하면 돼요. 마지막 단계에서 내장 도구로 전체 AST를 순회할 거예요.
TopSecret.decode_secret_message_part/2 함수를 확장해요. AST 노드의 연산이 함수를 정의하는 것이라면 함수 이름 전체를 반환하지 않아요. 대신 그 함수의 인자 개수를 확인해요. 그런 다음 이름에서 처음 n개 문자만 반환해요. 여기서 n은 인자 개수예요.
ast_node = TopSecret.to_ast("defp cat(a, b), do: nil")
TopSecret.decode_secret_message_part(ast_node, ["day"])
# => {ast_node, ["ca", "day"]}
ast_node = TopSecret.to_ast("defp cat(), do: nil")
TopSecret.decode_secret_message_part(ast_node, ["day"])
# => {ast_node, ["", "day"]}
TopSecret.decode_secret_message_part/2 함수를 확장해요. 가드를 사용하는 함수 정의에서도 함수 이름과 인자 개수가 올바르게 감지되도록 해요.
ast_node = TopSecret.to_ast("defp cat(a, b) when is_nil(a), do: nil")
TopSecret.decode_secret_message_part(ast_node, ["day"])
# => {ast_node, ["ca", "day"]}
TopSecret.decode_secret_message/1 함수를 구현해요. 엘릭서 코드가 담긴 문자열을 받아, 코드에서 찾은 모든 함수 정의에서 해독한 비밀 메시지를 문자열로 반환해야 해요. 이전 단계에서 정의한 함수들을 재사용해야 해요.
code = """
defmodule MyCalendar do
def busy?(date, time) do
Date.day_of_week(date) != 7 and
time.hour in 10..16
end
def yesterday?(date) do
Date.diff(Date.utc_today, date)
end
end
"""
TopSecret.decode_secret_message(code)
# => "buy"
Exercism에 가입하고 Elixir 트랙을 개념 58개연습 문제 168개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.