學習軌道
/
Elixir
Elixir
/
練習
/
最高機密
最高機密

最高機密

學習練習

簡介

抽象語法樹

抽象語法樹(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是該函式的參數個數。

1. 將程式碼轉換成資料

實作TopSecret.to_ast/1函式。它應該接受一個含有 Elixir 程式碼的字串,並回傳它的 AST。

TopSecret.to_ast("div(4, 3)")
# => {:div, [line: 1], [4, 3]}

2. 解析單一 AST 節點

實作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。

3. 從函式定義解碼秘密訊息片段

擴充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"]}

4. 修正帶有守衛的函式解碼

擴充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"]}

5. 解碼完整的秘密訊息

實作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"
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Elixir Exercism

準備好開始 最高機密 了嗎?

註冊 Exercism,透過 58 個概念168 個練習 和真人引導來學習並精通 Elixir,全部免費。