Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

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つの異なるノードタイプを処理する必要があります:TemplateContentExpression 、および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リストに登録してください。ストロープワッフルに飢えている場合は、私たちに連絡してください。ストロープワッフルで燃料を補給できる可能性があります。


  1. Rubyメソッドルックアップを理解する

    メソッドを呼び出すとどうなると思いますか?同じ名前の別のメソッドがある場合、Rubyはどのメソッドを呼び出すかをどのように決定しますか?メソッドがどこに格納または供給されているのか疑問に思ったことはありますか? Rubyは、定義された「ウェイ」または「パターン」を使用して、呼び出す適切なメソッドと「メソッドエラーなし」を返す適切なタイミングを決定します。この「ウェイ」をRubyメソッドルックアップパス 。このチュートリアルでは、Rubyのメソッドルックアップについて詳しく説明します。最後に、Rubyがオブジェクトの階層をどのように通過して、参照しているメソッドを判別するかを十分に理解できます

  2. Rubyの実用的なリンクリスト

    これは、「Rubyの実用的なコンピュータサイエンス」シリーズの3番目のエントリです。今日はリンクリストについてお話します。 では、リンクリストとは何ですか? 名前が示すように、リンクリストはデータをリスト形式で保存する方法です(ありがとう、キャプテンオブビシャス!)。 「リンクされた」部分は、データがノードに格納され、これらのノードが順番に相互にリンクされているという事実に由来します。 これはアレイとどう違うのですか? リンクリストと配列 リンクリストには、配列とは異なるパフォーマンス特性があります。これが、どちらかを選択する理由の1つです。 これは、リンクリストが配列よりも