Rubyテンプレート:インタプリタを焼く
今日は粘着性のあるストロープ(ストロープワッフルの2つの半分をくっつけるシロップ)で物を接着しているので、コーヒーの上にストロープワッフルを温めてください。シリーズの最初の2つのパートでは、レクサーとパーサーを焼きました。現在は、インタープリターを追加し、それらの上にストループを注いで物を接着しています。
材料
大丈夫!キッチンを焼く準備をして、材料をテーブルに置きましょう。私たちの通訳者は、その仕事をするために2つの要素または情報を必要とします。以前に生成された抽象構文木(AST)と、テンプレートに埋め込みたいデータです。このデータをenvironment
と呼びます 。
ASTをトラバースするために、ビジターパターンを使用してインタープリターを実装します。ビジター(したがってインタープリター)は、ノードをパラメーターとして受け入れ、このノードを処理し、潜在的にvisit
を呼び出す一般的なvisitメソッドを実装します。 現在のノードにとって何が意味があるかに応じて、ノードの子の一部(またはすべて)を使用して再度メソッドを実行します。
module Magicbars
class Interpreter
attr_reader :root, :environment
def self.render(root, environment = {})
new(root, environment).render
end
def initialize(root, environment = {})
@root = root
@environment = environment
end
def render
visit(root)
end
def visit(node)
# Process node
end
end
end
続行する前に、小さなMagicbars.render
も作成しましょう。 テンプレートと環境を受け入れ、レンダリングされたテンプレートを出力するメソッド。
module Magicbars
def self.render(template, environment = {})
tokens = Lexer.tokenize(template)
ast = Parser.parse(tokens)
Interpreter.render(ast, environment)
end
end
これにより、ASTを手動で作成しなくてもインタープリターをテストできるようになります。
Magicbars.render('Welcome to {{name}}', name: 'Ruby Magic')
# => nil
当然のことながら、現在は何も返されません。それでは、visit
の実装を始めましょう 方法。簡単に言うと、このテンプレートのASTは次のようになります。
このテンプレートでは、4つの異なるノードタイプを処理する必要があります:Template
、Content
、Expression
、およびIdentifier
。これを行うには、巨大なcase
を置くだけです。 visit
内のステートメント 方法。ただし、これはすぐに判読できなくなります。代わりに、Rubyのメタプログラミング機能を利用して、コードをもう少し整理して読みやすくしてみましょう。
module Magicbars
class Interpreter
# ...
def visit(node)
short_name = node.class.to_s.split('::').last
send("visit_#{short_name}", node)
end
end
end
このメソッドはノードを受け入れ、そのクラス名を取得し、ノードからすべてのモジュールを削除します(これを行うさまざまな方法に興味がある場合は、文字列のクリーニングに関する記事を確認してください)。その後、send
を使用します この特定のタイプのノードを処理するメソッドを呼び出します。各タイプのメソッド名は、モジュール化されていないクラス名とvisit_
で構成されます。 プレフィックス。メソッド名に大文字が含まれるのは少し珍しいことですが、メソッドの意図がかなり明確になります。
module Magicbars
class Interpreter
# ...
def visit_Template(node)
# Process template nodes
end
def visit_Content(node)
# Process content nodes
end
def visit_Expression(node)
# Process expression nodes
end
def visit_Identifier(node)
# Process identifier nodes
end
end
end
visit_Template
を実装することから始めましょう 方法。すべてのstatements
を処理する必要があります ノードの結果を結合します。
def visit_Template(node)
node.statements.map { |statement| visit(statement) }.join
end
次に、visit_Content
を見てみましょう。 方法。コンテンツノードは文字列をラップするだけなので、メソッドは非常に簡単です。
def visit_Content(node)
node.content
end
それでは、visit_Expression
に移りましょう。 プレースホルダーが実際の値に置き換えられる方法。
def visit_Expression(node)
key = visit(node.identifier)
environment.fetch(key, '')
end
そして最後に、visit_Expression
環境からフェッチするキーを知るためのメソッド、visit_Identifier
を実装しましょう メソッド。
def visit_Identifier(node)
node.value
end
これらの4つの方法を使用すると、テンプレートを再度レンダリングしようとすると、目的の結果が得られます。
Magicbars.render('Welcome to {{name}}', name: 'Ruby Magic')
# => Welcome to Ruby Magic
ブロック式の解釈
単純なgsub
を実装するために多くのコードを作成しました 出来ました。それでは、より複雑な例に移りましょう。
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}}
念のため、対応するASTは次のようになります。
まだ処理していないノードタイプは1つだけです。 visit_BlockExpression
ノード。ある意味、visit_Expression
に似ています ノードですが、値に応じて、statements
の処理を続行します。 またはinverse_statements
BlockExpression
の ノード。
def visit_BlockExpression(node)
key = visit(node.identifier)
if environment[key]
node.statements.map { |statement| visit(statement) }.join
else
node.inverse_statements.map { |statement| visit(statement) }.join
end
end
メソッドを見ると、2つのブランチが非常に似ており、visit_Template
にも似ていることがわかります。 方法。それらはすべて、Array
のすべてのノードの訪問を処理します 、それではvisit_Array
を抽出しましょう 物事を少しきれいにする方法。
def visit_Array(nodes)
nodes.map { |node| visit(node) }
end
新しいメソッドを使用すると、visit_Template
から一部のコードを削除できます。 およびvisit_BlockExpression
メソッド。
def visit_Template(node)
visit(node.statements).join
end
def visit_BlockExpression(node)
key = visit(node.identifier)
if environment[key]
visit(node.statements).join
else
visit(node.inverse_statements).join
end
end
インタプリタがすべてのノードタイプを処理するようになったので、複雑なテンプレートをレンダリングしてみましょう。
Magicbars.render(template, { name: 'Ruby Magic', subscribed: true, company_name: 'AppSignal' })
# => Welcome to Ruby Magic!
#
#
# Please sign up for our mailing list to be notified about new articles!
#
#
# Your friends at AppSignal
そのほぼ 正しく見えます。しかし、よく見ると、subscribed: true
を提供したにもかかわらず、メッセージがメーリングリストへの登録を促していることがわかります。 環境で。それは正しくないようです…
ヘルパーメソッドのサポートの追加
テンプレートを振り返ると、if
があることがわかります。 ブロック式で。 subscribed
の値を検索する代わりに 環境では、visit_BlockExpression
if
の値を検索しています 。環境に存在しないため、呼び出しはnil
を返します 、これは誤りです。
ここで停止して、ハンドルバーではなく口ひげを模倣しようとしていることを宣言し、if
を取り除くことができます。 テンプレートで、望ましい結果が得られます。
Welcome to {{name}}!
{{#subscribed}}
Thank you for subscribing to our mailing list.
{{else}}
Please sign up for our mailing list to be notified about new articles!
{{/subscribed}}
Your friends at {{company_name}}
しかし、私たちが楽しんでいるときになぜ停止するのですか?さらに一歩進んで、ヘルパーメソッドを実装しましょう。他のことにも役立つかもしれません。
単純な式にヘルパーメソッドのサポートを追加することから始めましょう。 reverse
を追加します ヘルパー、渡された文字列を逆にします。さらに、debug
を追加します 指定された値のクラス名を通知するメソッド。
def helpers
@helpers ||= {
reverse: ->(value) { value.to_s.reverse },
debug: ->(value) { value.class }
}
end
単純なラムダを使用してこれらのヘルパーを実装し、ハッシュに格納して、名前で検索できるようにします。
次に、visit_Expression
を変更しましょう 環境で値のルックアップを試行する前にヘルパールックアップを実行します。
def visit_Expression(node)
key = visit(node.identifier)
if helper = helpers[key]
arguments = visit(node.arguments).map { |k| environment[k] }
return helper.call(*arguments)
end
environment[key]
end
指定された識別子に一致するヘルパーがある場合、メソッドはすべての引数にアクセスし、それらの値を検索しようとします。その後、メソッドを呼び出し、すべての値を引数として渡します。
Magicbars.render('Welcome to {{reverse name}}', name: 'Ruby Magic')
# => Welcome to cigaM ybuR
Magicbars.render('Welcome to {{debug name}}', name: 'Ruby Magic')
# => Welcome to String
それが整ったら、最後にif
を実装しましょう およびunless
ヘルパー。引数に加えて、2つのラムダを渡して、ノードのstatements
の解釈を続行するかどうかを決定できるようにします。 またはinverse_statements
。
def helpers
@helpers ||= {
if: ->(value, block:, inverse_block:) { value ? block.call : inverse_block.call },
unless: ->(value, block:, inverse_block:) { value ? inverse_block.call : block.call },
# ...
}
end
visit_BlockExpression
に必要な変更 visit_Expression
で行ったことと似ています 、今回だけ、2つのラムダも渡します。
def visit_BlockExpression(node)
key = visit(node.identifier)
if helper = helpers[key]
arguments = visit(node.arguments).map { |k| environment[k] }
return helper.call(
*arguments,
block: -> { visit(node.statements).join },
inverse_block: -> { visit(node.inverse_statements).join }
)
end
if environment[key]
visit(node.statements).join
else
visit(node.inverse_statements).join
end
end
そしてこれで、私たちのベーキングが完了しました!この旅を始めた複雑なテンプレートを、レクサー、パーサー、インタープリターの世界にレンダリングできます。
Magicbars.render(template, { name: 'Ruby Magic', subscribed: true, company_name: 'AppSignal' })
# => Welcome to Ruby Magic!
#
#
# Thank you for subscribing to our mailing list.
#
#
# Your friends at AppSignal
表面を傷つけるだけ
この3部構成のシリーズでは、テンプレート言語の作成の基本について説明しました。これらの概念は、インタプリタプログラミング言語(Rubyなど)を作成するためにも使用できます。確かに、私たちはいくつかのこと(適切なエラー処理など)をざっと見て、今日のプログラミング言語の基盤の表面をかじっただけでした。
このシリーズを楽しんでいただければ幸いです。さらに詳しく知りたい場合は、RubyMagicリストに登録してください。ストロープワッフルに飢えている場合は、私たちに連絡してください。ストロープワッフルで燃料を補給できる可能性があります。
-
Rubyメソッドルックアップを理解する
メソッドを呼び出すとどうなると思いますか?同じ名前の別のメソッドがある場合、Rubyはどのメソッドを呼び出すかをどのように決定しますか?メソッドがどこに格納または供給されているのか疑問に思ったことはありますか? Rubyは、定義された「ウェイ」または「パターン」を使用して、呼び出す適切なメソッドと「メソッドエラーなし」を返す適切なタイミングを決定します。この「ウェイ」をRubyメソッドルックアップパス 。このチュートリアルでは、Rubyのメソッドルックアップについて詳しく説明します。最後に、Rubyがオブジェクトの階層をどのように通過して、参照しているメソッドを判別するかを十分に理解できます
-
Rubyの実用的なリンクリスト
これは、「Rubyの実用的なコンピュータサイエンス」シリーズの3番目のエントリです。今日はリンクリストについてお話します。 では、リンクリストとは何ですか? 名前が示すように、リンクリストはデータをリスト形式で保存する方法です(ありがとう、キャプテンオブビシャス!)。 「リンクされた」部分は、データがノードに格納され、これらのノードが順番に相互にリンクされているという事実に由来します。 これはアレイとどう違うのですか? リンクリストと配列 リンクリストには、配列とは異なるパフォーマンス特性があります。これが、どちらかを選択する理由の1つです。 これは、リンクリストが配列よりも