Rubyテンプレートを深く掘り下げる:パーサー
今日、私たちはRubyテンプレートへの旅を続けています。レクサーを配置したら、次のステップであるパーサーに進みましょう。
前回は文字列の補間を検討し、その後、独自のテンプレート言語の作成に取り組みました。テンプレートを読み取り、それをトークンのストリームに変換するレクサーを実装することから始めました。今日は、付随するパーサーを実装します。また、少し言語理論に足を踏み入れます。
どうぞ!
抽象構文木
Welcome to {{name}}
の簡単なサンプルテンプレートを振り返ってみましょう。 。レクサーを使用して文字列をトークン化した後、次のようなトークンのリストを取得します。
Magicbars::Lexer.tokenize("Welcome to {{name}}")
# => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]]
最終的には、テンプレートを評価し、式を実際の値に置き換えます。物事をもう少し難しくするために、複雑なブロック式を評価して、繰り返しと条件を考慮に入れたいと思います。
これを行うには、テンプレートの論理構造を記述する抽象構文木(AST)を生成する必要があります。ツリーは、他のノードを参照したり、トークンからの追加データを保存したりできるノードで構成されています。
簡単な例では、目的の抽象構文木は次のようになります。
文法の定義
文法を定義するために、言語の理論的基礎から始めましょう。他のプログラミング言語と同様に、私たちのテンプレート言語は文脈自由言語であるため、文脈自由文法で記述できます。 (ウィキペディアの詳細な説明にある数学表記を怖がらせないでください。概念は非常に単純で、文法を表記するための開発者にとってより使いやすい方法があります。)
文脈自由文法は、言語のすべての可能な文字列がどのように構築されるかを説明する一連の規則です。 EBNF表記のテンプレート言語の文法を見てみましょう:
template = statements;
statements = { statement };
statement = CONTENT | expression | block_expression;
expression = OPEN_EXPRESSION, IDENTIFIER, arguments, CLOSE;
block_expression = OPEN_BLOCK, IDENTIFIER, arguments, CLOSE, statements, [ OPEN_INVERSE, CLOSE, statements ], OPEN_END_BLOCK, IDENTIFIER, CLOSE;
arguments = { IDENTIFIER };
各割り当てはルールを定義します。ルールの名前は左側にあり、レクサーからの他のルール(小文字)またはトークン(大文字)の束が右側にあります。ルールとトークンは、コンマ,
を使用して連結できます。 またはパイプを使用して交互に|
シンボル。中括弧内のルールとトークン{ ... }
数回繰り返される場合があります。角かっこで囲まれている場合[ ... ]
、オプションと見なされます。
上記の文法は、テンプレートがステートメントで構成されていることを簡潔に説明する方法です。ステートメントはCONTENT
のいずれかです トークン、式、またはブロック式。式はOPEN_EXPRESSION
です トークンの後にIDENTIFIER
が続きます トークン、引数、CLOSE
が続く トークン。また、ブロック式は、自然言語で説明するのではなく、上記のような表記法を使用する方がよい理由の完璧な例です。
上記のような文法定義からパーサーを自動的に生成するツールがあります。しかし、Ruby Magicの真の伝統では、楽しんでパーサーを自分で構築し、その過程で1つか2つのことを学んでいきましょう。
パーサーの構築
言語理論はさておき、実際にパーサーを構築することに取り掛かりましょう。さらに最小限の、しかしまだ有効なテンプレートから始めましょう:Welcome to Ruby Magic
。このテンプレートには式がなく、トークンのリストは1つの要素のみで構成されています。外観は次のとおりです:
[[:CONTENT, "Welcome to Ruby Magic"]]
まず、パーサークラスを設定します。次のようになります:
module Magicbars
class Parser
def self.parse(tokens)
new(tokens).parse
end
attr_reader :tokens
def initialize(tokens)
@tokens = tokens
end
def parse
# Parsing starts here
end
end
end
クラスはトークンの配列を受け取り、それを格納します。 parse
と呼ばれるパブリックメソッドが1つだけあります トークンをASTに変換します。
文法を振り返ると、一番上のルールはtemplate
です。 。これは、parse
を意味します 、解析プロセスの開始時に、Template
を返します ノード。
ノードは、独自の動作を持たない単純なクラスです。他のノードを接続するか、トークンからいくつかの値を格納するだけです。 Template
は次のとおりです ノードは次のようになります:
module Magicbars
module Nodes
class Template
attr_reader :statements
def initialize(statements)
@statements = statements
end
end
end
end
この例を機能させるには、Content
も必要です。 ノード。テキストコンテンツを保存するだけです("Welcome to Ruby Magic"
)トークンから。
module Magicbars
module Nodes
class Content
attr_reader :content
def initialize(content)
@content = content
end
end
end
end
次に、解析メソッドを実装して、Template
のインスタンスを作成しましょう。 およびContent
のインスタンス 正しく接続してください。
def parse
Magicbars::Nodes::Template.new(parse_content)
end
def parse_content
return unless tokens[0][0] == :CONTENT
Magicbars::Nodes::Content.new(tokens[0][1])
end
パーサーを実行すると、正しい結果が得られます:
Magicbars::Parser.parse(tokens)
# => #<Magicbars::Nodes::Template:0x00007fe90e939410 @statements=#<Magicbars::Nodes::Content:0x00007fe90e939578 @content="Welcome to Ruby Magic">>
確かに、これは、コンテンツノードが1つしかない単純な例でのみ機能します。実際に式を含むより複雑な例に切り替えましょう:Welcome to {{name}}
。
Magicbars::Lexer.tokenize("Welcome to {{name}}")
# => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]]
このためには、Expression
が必要です。 ノードとIdentifier
ノード。 Expression
ノードは、識別子と引数(文法によれば、0個以上のIdentifier
の配列)を格納します。 ノード)。他のノードと同様に、ここで確認できることはあまりありません。
module Magicbars
module Nodes
class Expression
attr_reader :identifier, :arguments
def initialize(identifier, arguments)
@identifier = identifier
@arguments = arguments
end
end
end
end
module Magicbars
module Nodes
class Identifier
attr_reader :value
def initialize(value)
@value = value.to_sym
end
end
end
end
新しいノードを配置したら、parse
を変更しましょう 通常のコンテンツと式の両方を処理するメソッド。これを行うには、parse_statements
を導入します。 parse_statement
を呼び出し続けるメソッド 値を返す限り。
def parse
Magicbars::Nodes::Template.new(parse_statements)
end
def parse_statements
results = []
while result = parse_statement
results << result
end
results
end
parse_statement
それ自体が最初にparse_content
を呼び出します それでも値が返されない場合は、parse_expression
を呼び出します。 。
def parse_statement
parse_content || parse_expression
end
parse_statement
に気づきましたか メソッドはstatement
と非常によく似たものになり始めています 文法のルール?これは、事前に文法を明示的に書くために時間をかけることが、正しい道を進んでいることを確認するのに大いに役立つ場所です。
次に、parse_content
を変更しましょう 最初のトークンだけを見るのではないようにする方法。これを行うには、追加の@position
を導入します。 初期化子のインスタンス変数を使用して、現在のトークンをフェッチします。
attr_reader :tokens, :position
def initialize(tokens)
@tokens = tokens
@position = 0
end
# ...
def parse_content
return unless token = tokens[position]
return unless token[0] == :CONTENT
@position += 1
Magicbars::Nodes::Content.new(token[1])
end
parse_content
メソッドは現在のトークンを調べ、そのタイプをチェックします。 CONTENT
の場合 トークンの場合、位置がインクリメントされ(現在のトークンが正常に解析されたため)、トークンのコンテンツを使用してContent
が作成されます。 ノード。現在のトークンがない場合(トークンの最後にあるため)、またはタイプが一致しない場合、メソッドは早期に終了し、nil
を返します。 。
改善されたparse_content
メソッドを配置したら、新しいparse_expression
に取り組みましょう メソッド。
def parse_expression
return unless token = tokens[position]
return unless token[0] == :OPEN_EXPRESSION
@position += 1
identifier = parse_identifier
arguments = parse_arguments
if !tokens[position] || tokens[position][0] != :CLOSE
raise "Unexpected token #{tokens[position][0]}. Expected :CLOSE."
end
@position += 1
Magicbars::Nodes::Expression.new(identifier, arguments)
end
まず、現在のトークンがあり、そのタイプがOPEN_EXPRESSION
であることを確認します。 。その場合は、次のトークンに進み、parse_identifier
を呼び出して、識別子と引数を解析します。 およびparse_arguments
、 それぞれ。どちらのメソッドもそれぞれのノードを返し、現在のトークンを進めます。それが完了すると、現在のトークンが存在し、:CLOSE
であることを確認します トークン。そうでない場合は、エラーが発生します。それ以外の場合は、新しく作成されたExpression
を返す前に、最後にもう一度位置を進めます。 ノード。
この時点で、いくつかのパターンが出現していることがわかります。次のトークンに数回進み、現在のトークンとそのタイプがあることも確認しています。そのためのコードは少し面倒なので、2つのヘルパーメソッドを紹介しましょう。
def expect(*expected_tokens)
upcoming = tokens[position, expected_tokens.size]
if upcoming.map(&:first) == expected_tokens
advance(expected_tokens.size)
upcoming
end
end
def advance(offset = 1)
@position += offset
end
expect
メソッドは可変数のトークンタイプを受け取り、トークンストリーム内の次のトークンと照合します。それらがすべて一致する場合、一致するトークンを超えて進み、それらを返します。 advance
メソッドは@position
をインクリメントするだけです 指定されたオフセットによるインスタンス変数。
次に予想されるトークンに関して柔軟性がない場合のために、トークンが一致しない場合に適切なエラーメッセージを表示するメソッドも導入します。
def need(*required_tokens)
upcoming = tokens[position, required_tokens.size]
expect(*required_tokens) or raise "Unexpected tokens. Expected #{required_tokens.inspect} but got #{upcoming.inspect}"
end
これらのヘルパーメソッドを使用することにより、parse_content
およびparse_expression
クリーンで読みやすくなりました。
def parse_content
if content = expect(:CONTENT)
Magicbars::Nodes::Content.new(content[0][1])
end
end
def parse_expression
return unless expect(:OPEN_EXPRESSION)
identifier = parse_identifier
arguments = parse_arguments
need(:CLOSE)
Magicbars::Nodes::Expression.new(identifier, arguments)
end
最後に、parse_identifier
も見てみましょう。 およびparse_arguments
。ヘルパーメソッドのおかげで、parse_identifier
メソッドはparse_content
と同じくらい簡単です 方法。唯一の違いは、別のノードタイプを返すことです。
def parse_identifier
if identifier = expect(:IDENTIFIER)
Magicbars::Nodes::Identifier.new(identifier[0][1])
end
end
parse_arguments
を実装する場合 メソッドでは、parse_statements
とほぼ同じであることがわかりました。 方法。唯一の違いは、parse_identifier
を呼び出すことです。 parse_statement
の代わりに 。別のヘルパーメソッドを導入することで、重複したロジックを取り除くことができます。
def repeat(method)
results = []
while result = send(method)
results << result
end
results
end
repeat
メソッドはsend
を使用します ノードを返さなくなるまで、指定されたメソッド名を呼び出します。それが発生すると、収集された結果(または単に空の配列)が返されます。このヘルパーを配置すると、両方のparse_statements
およびparse_arguments
1行のメソッドになります。
def parse_statements
repeat(:parse_statement)
end
def parse_arguments
repeat(:parse_identifier)
end
これらすべての変更が整ったら、トークンストリームを解析してみましょう。
Magicbars::Parser.parse(tokens)
# => #<Magicbars::Nodes::Template:0x00007f91a602f910
# @statements=
# [#<Magicbars::Nodes::Content:0x00007f91a58802c8 @content="Welcome to ">,
# #<Magicbars::Nodes::Expression:0x00007f91a602fcd0
# @arguments=[],
# @identifier=
# #<Magicbars::Nodes::Identifier:0x00007f91a5880138 @value=:name> >
読むのは少し難しいですが、実際には、正しい抽象構文木です。 Template
ノードにはContent
があります およびExpression
声明。 Content
ノードの値は"Welcome to "
およびExpression
ノードの識別子はIdentifier
です :name
のノード その値として。
ブロック式の解析
パーサーの実装を完了するには、ブロック式の解析を実装する必要があります。念のため、解析するテンプレートは次のとおりです。
Welcome to {{name}}!
{{#if subscribed}}
Thank you for subscribing to our mailing list.
{{else}}
Please sign up for our mailing list to be notified about new articles!
{{/if}}
Your friends at {{company_name}}
これを行うには、最初にBlockExpression
を紹介しましょう。 ノード。このノードはもう少し多くのデータを格納しますが、他には何もしないため、あまりエキサイティングではありません。
module Magicbars
module Nodes
class BlockExpression
attr_reader :identifier, :arguments, :statements, :inverse_statements
def initialize(identifier, arguments, statements, inverse_statements)
@identifier = identifier
@arguments = arguments
@statements = statements
@inverse_statements = inverse_statements
end
end
end
end
Expression
のように ノードには、識別子と引数が格納されます。さらに、ブロックと逆ブロックのステートメントも格納されます。
文法を振り返ると、ブロック式を解析するには、parse_statements
を修正する必要があることがわかります。 parse_block_expression
を呼び出すメソッド 。これで、文法のルールと同じようになります。
def parse_statement
parse_content || parse_expression || parse_block_expression
end
parse_block_expression
メソッド自体はもう少し複雑です。しかし、ヘルパーメソッドのおかげで、それでもかなり読みやすくなっています。
def parse_block_expression
return unless expect(:OPEN_BLOCK)
identifier = parse_identifier
arguments = parse_arguments
need(:CLOSE)
statements = parse_statements
if expect(:OPEN_INVERSE, :CLOSE)
inverse_statements = parse_statements
end
need(:OPEN_END_BLOCK)
if identifier.value != parse_identifier.value
raise("Error. Identifier in closing expression does not match identifier in opening expression")
end
need(:CLOSE)
Magicbars::Nodes::BlockExpression.new(identifier, arguments, statements, inverse_statements)
end
最初の部分は、parse_expression
と非常によく似ています。 方法。識別子と引数を使用して開始ブロック式を解析します。その後、parse_statements
を呼び出します ブロックの内部を解析します。
それが完了したら、{{else}}
を確認します OPEN_INVERSE
で識別される式 トークンの後にCLOSE
トークン。両方のトークンが見つかった場合、parse_statements
を呼び出します 再び逆ブロックを解析します。それ以外の場合は、その部分を完全にスキップします。
最後に、オープンブロック式と同じ識別子を使用するエンドブロック式があることを確認します。識別子が一致しない場合、エラーが発生します。それ以外の場合は、新しいBlockExpression
を作成します ノードとそれを返します。
高度なブロック式テンプレートのトークンを使用してパーサーを呼び出すと、テンプレートのASTが返されます。読みにくいので、ここでは出力例を含めません。代わりに、生成されたASTを視覚的に表現したものです。
parse_statements
を呼び出しているため parse_block_expression
の内部 、ブロックと逆ブロックの両方に、通常のコンテンツだけでなく、より多くの式、ブロック式が含まれる場合があります。
旅は続く…
私たちは、独自のテンプレート言語の実装に向けた旅でまともな進歩を遂げました。言語理論を少し理解した後、テンプレート言語の文法を定義し、それを使用してパーサーを最初から実装しました。
レクサーとパーサーの両方が配置されているため、テンプレートから補間された文字列を生成するためのインタープリターが不足しているだけです。この部分については、RubyMagicの次のエディションで説明します。 Ruby Magicメーリングリストに登録して、リリースされたときにアラートを受け取ります。
-
Ruby転置法を使用して行を列に変換する
今日は、Ruby転置法を使用してRubyでグリッドを処理する方法を学習します。 多次元配列の形をした完璧なグリッド、たとえば3×3の正方形があると想像してみてください。 そして、行を取得して列に変換する 。 なぜあなたはそれをしたいのですか? 1つの用途は、古典的なゲームであるtic-tac-toeです。 ボードをグリッドとして保存します。次に、勝利の動きを見つけるには、行を確認する必要があります 、列 &対角線 。 問題は、グリッドを配列として格納している場合、行に直接アクセスすることしかできないことです。 コラムズザハードウェイ 「直接アクセス」とは、配列を(eachで)調べ
-
Rubyでパーサーを構築する方法
構文解析は、一連の文字列を理解し、それらを理解できるものに変換する技術です。正規表現を使用することもできますが、必ずしもその仕事に適しているとは限りません。 たとえば、HTMLを正規表現で解析することはおそらく良い考えではないことは一般的な知識です。 Rubyには、この作業を実行できるnokogiriがありますが、独自のパーサーを作成することで多くのことを学ぶことができます。始めましょう! Rubyでの解析 パーサーの中核はStringScannerです クラス。 このクラスは、文字列のコピーと位置ポインタを保持します。ポインタを使用すると、特定のトークンを検索するために文字列をトラバ