抽象語法樹(AST),也稱為_引用運算式_,是一種把程式碼表示成資料的方式。
抽象語法樹裡的每個節點,都是一個由三個元素組成的元組。
# AST representation of:
# 2 + 3
{:+, [], [2, 3]}
第一個元素是原子,代表運算。第二個元素是關鍵字陣列,也就是中介資料。第三個元素是引數的陣列,裡面包含其他節點。整數、原子和字串等字面值,在抽象語法樹中會直接以本身來表示,而不是用由三個元素組成的元組。
把 Elixir 程式碼轉換成抽象語法樹,以及把抽象語法樹轉回程式碼,都屬於標準函式庫的一部分。你可以在Code模組(例如把含有程式碼的字串轉換成抽象語法樹)和Macro模組(例如走訪抽象語法樹,或把它轉換成字串)裡,找到處理抽象語法樹的函式。
請注意,標準函式庫中所有的函式都用「quoted」這個名稱來表示抽象語法樹(是_引用運算式_的簡稱)。
把程式碼轉換成抽象語法樹的特殊形式叫做quote。它接受一個程式區塊,並回傳該區塊的抽象語法樹。
quote do
2 + 3 - 1
end
# => {:-, [], [
# {:+, [], [2, 3]},
# 1
# ]}
能把程式碼表示成抽象語法樹,是 Elixir 元程式設計的核心。_巨集_是一種撰寫能產生 Elixir 程式碼的 Elixir 程式碼的方式,它的運作方式就是把抽象語法樹當作輸出回傳。
抽象語法樹的另一個使用情境是靜態程式碼分析,例如 Exercism 自己的工具 Analyzer,你或許已經認識它了,它就是那個會在你的解答上留言的小機器人。
你隸屬於一個對抗企業間諜活動的專案小組。你在 Shady Company X 有一名秘密線人,你懷疑這家公司竊取競爭對手的機密。
你的線人特務 Ex 是一位 Elixir 開發者。她把秘密訊息編碼在自己的程式碼裡。
要解碼她的秘密訊息:
n個字元,其中n是該函式的參數個數。實作TopSecret.to_ast/1函式。它應該接受一個含有 Elixir 程式碼的字串,並回傳它的 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函式。它應該接受一個含有 Elixir 程式碼的字串,並回傳從程式碼中所有函式定義解碼出來的秘密訊息字串。務必重複使用前面步驟定義的函式。
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"