Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Xây dựng Interpreter Template Ruby: Từ Lexer đến Thực thi

Chúng ta hy vọng bạn đã sẵn sàng stroopwafel nung nóng trên ly cà phê vì hôm nay chúng ta gắn things up với sticky stroop (syrup làm dính hai nửa stroopwafel). Trong hai phần đầu series, chúng ta baked Lexer và Parser, giờ thêm Interpreter và gắn things together bằng cách đổ stroop lên chúng.

Nguyên liệu

Được thôi! Chuẩn bị kitchen cho baking và để ingredients trên bàn. Interpreter cần hai ingredient hoặc thông tin để làm việc: Abstract Syntax Tree (AST) đã generate trước đó và data muốn embed vào template. Chúng ta gọi data này là environment.

Để traverse AST, chúng ta implement interpreter dùng visitor pattern. Visitor (và do đó interpreter) implement generic visit method nhận node làm parameter, process node này và potentially gọi lại visit với một số (hoặc tất cả) children của node, tùy theo gì makes sense cho current node.

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

Trước khi tiếp tục, tạo nhỏ method Magicbars.render nhận template và environment, output rendered template.

module Magicbars
  def self.render(template, environment = {})
    tokens = Lexer.tokenize(template)
    ast = Parser.parse(tokens)
    Interpreter.render(ast, environment)
  end
end

Với này, test interpreter mà không cần construct AST bằng tay.

Magicbars.render('Welcome to {{name}}', name: 'Ruby Magic')
# => nil

Không ngạc nhiên, hiện tại không return gì. Bắt đầu implement method visit. Nhắc lại, AST cho template này như sau.

Đối với template này, phải process 4 node type: Template, Content, Expression, và Identifier. Có thể dùng huge case statement trong visit, nhưng nhanh chóng unreadable. Thay vào đó, dùng metaprogramming Ruby để code organized, readable hơn.

module Magicbars
  class Interpreter
    # ...
    def visit(node)
      short_name = node.class.to_s.split('::').last
      send("visit_#{short_name}", node)
    end
  end
end

Method nhận node, lấy class name, bỏ module (xem bài clean strings nếu quan tâm cách khác). Sau đó dùng send gọi method handle specific node type. Method name gồm demodulized class name và prefix visit_. Hơi unusual có capital letters trong method name, nhưng làm rõ intent.

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

Bắt đầu implement visit_Template. Nó chỉ process tất cả statements của node và join results.

def visit_Template(node)
  node.statements.map { |statement| visit(statement) }.join
end

Tiếp visit_Content. Content node chỉ wrap string, method simple như nó là.

def visit_Content(node)
  node.content
end

Tới visit_Expression nơi substitution placeholder với real value xảy ra.

def visit_Expression(node)
  key = visit(node.identifier)
  environment.fetch(key, '')
end

Cuối cùng, visit_Identifier để visit_Expression biết key nào fetch từ environment.

def visit_Identifier(node)
  node.value
end

Với 4 method này, render template lại cho desired result.

Magicbars.render('Welcome to {{name}}', name: 'Ruby Magic')
# => Welcome to Ruby Magic

Interpreting Block Expressions

Viết nhiều code implement những gì gsub đơn giản làm được. Chuyển sang ví dụ complex hơn.

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}}

Nhắc lại, AST tương ứng như sau.

Chỉ có một node type chưa handle: visit_BlockExpression. Nó similar visit_Expression, nhưng tùy value nó continue process statements hoặc inverse_statements của node 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

Nhận thấy hai branch rất similar, cũng giống visit_Template. Chúng đều handle visiting tất cả node của Array, extract method visit_Array để clean up.

def visit_Array(nodes)
  nodes.map { |node| visit(node) }
end

Với method mới, remove code từ visit_Template và 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

Giờ interpreter handle tất cả node type, thử render complex template.

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

Gần đúng. Nhưng nhìn kỹ, message nhắc sign up mailing list, dù cung cấp subscribed: true. Không đúng...

Thêm Support Helper Methods

Nhìn lại template, thấy if trong block expression. Thay vì lookup value subscribed trong environment, visit_BlockExpression lookup value if. Vì không có trong environment, return nil (false).

Có thể dừng ở đây declare không imitate Handlebars mà Mustache, bỏ if trong template, cho desired result.

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}}

Tại sao dừng khi đang vui? Đi extra mile implement helper methods. Chúng có thể handy cho việc khác.

Bắt đầu thêm helper method support cho simple expressions. Thêm helper reverse, reverse string passed. Thêm method debug cho biết class name của value.

def helpers
  @helpers ||= {
    reverse: ->(value) { value.to_s.reverse },
    debug: ->(value) { value.class }
  }
end

Dùng lambdas đơn giản implement helpers, store trong hash để lookup by name.

Sửa visit_Expression perform helper lookup trước khi try value lookup trong environment.

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

Nếu có helper match identifier, method visit tất cả arguments và try lookup values. Sau đó call method pass tất cả values làm arguments.

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

Với đó, implement if và unless helper. Ngoài arguments, pass hai lambdas để chúng decide continue interpret node's statements hay 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

Thay đổi cần thiết cho visit_BlockExpression tương tự visit_Expression, chỉ khác pass hai lambdas.

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

Với này, baking xong! Render complex template bắt đầu journey vào world của lexers, parsers, interpreters.

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

Chỉ Cào X سطح

Trong series 3 phần này, chúng ta cover basics tạo templating language. Khái niệm này cũng dùng tạo interpreted programming languages (như Ruby). Thừa nhận, chúng ta gloss over vài thứ (như proper error handling 🙀) và chỉ cào x superfície underpinnings ngôn ngữ lập trình ngày nay.

Hy vọng bạn enjoy series. Nếu muốn thêm, subscribe Ruby Magic list. Nếu thèm stroopwafel, drop us a line, có thể fuel bạn những cái đó!