抽象構文木(AST)は、_クォート式_とも呼ばれ、コードをデータとして表現する方法です。
ASTの各ノードは3要素のタプルです。
# AST representation of:
# 2 + 3
{:+, [], [2, 3]}
最初の要素はアトムで、演算を表します。2番目の要素はキーワードリストで、メタデータです。3番目の要素は引数の配列で、他のノードを含みます。整数、アトム、文字列などのリテラル値は、3要素のタプルではなく、AST内ではそのままの値として表現されます。
ElixirコードをASTに変換したり、ASTをコードに戻したりすることは、標準ライブラリの一部です。ASTを扱う関数は、Codeモジュール(例:コードを含む文字列をASTに変換する)とMacroモジュール(例:ASTを走査したり、文字列に変換したりする)にあります。
標準ライブラリの関数はすべて、ASTを指す名前として「quoted」を使っていることに注意してください(_クォート式_の略です)。
コードをASTに変換する特殊形式はquoteと呼ばれます。これはコードブロックを受け取り、そのASTを返します。
quote do
2 + 3 - 1
end
# => {:-, [], [
# {:+, [], [2, 3]},
# 1
# ]}
コードをASTとして表現できることは、Elixirのメタプログラミングの核心です。_マクロ_は、Elixirコードを生成するElixirコードを書く方法であり、ASTを出力として返すことで機能します。
ASTのもう1つのユースケースは静的コード解析です。たとえば、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ノードを1番目の要素、アキュムレーターを2番目の要素とするタプルを返します。
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"