Computer >> Máy Tính >  >> Lập trình >> Ruby

Ruby Templating:Nướng một thông dịch viên

Chúng tôi hy vọng bạn đã hâm nóng stroopwafel bên trên tách cà phê của mình vì hôm nay chúng tôi sẽ kết dính mọi thứ bằng keo vuốt dính (loại xi-rô làm cho hai nửa của bột nếp dính vào nhau). Trong hai phần đầu tiên của loạt bài, chúng tôi đã tạo ra một chiếc Lexer và một bộ phân tích cú pháp và bây giờ, chúng tôi đang thêm Trình thông dịch và dán các thứ lại với nhau bằng cách đổ keo lên chúng.

Thành phần

Được rồi! Hãy chuẩn bị bếp sẵn sàng để nướng và đặt các nguyên liệu của chúng ta lên bàn. Trình thông dịch của chúng tôi cần hai thành phần hoặc phần thông tin để thực hiện công việc của nó:Cây cú pháp trừu tượng (AST) đã tạo trước đó và dữ liệu chúng tôi muốn nhúng vào mẫu. Chúng tôi sẽ gọi dữ liệu này là environment .

Để xem xét AST, chúng tôi sẽ triển khai trình thông dịch bằng cách sử dụng mẫu khách truy cập. Khách truy cập (và do đó là trình thông dịch của chúng tôi) triển khai phương thức truy cập chung chấp nhận một nút làm tham số, xử lý nút này và có khả năng gọi visit phương pháp lại với một số (hoặc tất cả) nút con, tùy thuộc vào điều gì phù hợp với nút hiện tại.

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, hãy cũng tạo một Magicbars.render nhỏ phương thức chấp nhận một mẫu và một môi trường và xuất ra mẫu được kết xuất.

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

Với điều này, chúng tôi sẽ có thể kiểm tra trình thông dịch mà không cần phải xây dựng AST bằng tay.

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

Không có gì ngạc nhiên, nó hiện không trả lại bất cứ điều gì. Vì vậy, hãy bắt đầu triển khai visit phương pháp. Xin nhắc lại nhanh, đây là giao diện của AST cho mẫu này.

Đối với mẫu này, chúng tôi sẽ phải xử lý bốn loại nút khác nhau:Template , Content , ExpressionIdentifier . Để làm điều này, chúng tôi chỉ có thể đặt một case rất lớn tuyên bố bên trong visit của chúng tôi phương pháp. Tuy nhiên, điều này sẽ trở nên không thể đọc được khá nhanh chóng. Thay vào đó, hãy sử dụng khả năng lập trình siêu ứng dụng của Ruby để giữ cho mã của chúng ta có tổ chức và dễ đọc hơn một chút.

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

Phương thức chấp nhận một nút, lấy tên lớp của nó và xóa bất kỳ mô-đun nào khỏi nó (hãy xem bài viết của chúng tôi về cách làm sạch chuỗi nếu bạn quan tâm đến các cách khác nhau để thực hiện việc này). Sau đó, chúng tôi sử dụng send để gọi một phương thức xử lý loại nút cụ thể này. Tên phương thức cho mỗi loại được tạo thành từ tên lớp được giải mã và visit_ tiếp đầu ngữ. Có một chút bất thường khi có các chữ cái viết hoa trong tên phương thức, nhưng nó làm cho ý định của phương pháp khá rõ ràng.

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

Hãy bắt đầu bằng cách triển khai visit_Template phương pháp. Nó sẽ chỉ xử lý tất cả các câu lệnh statements của nút và kết hợp các kết quả.

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

Tiếp theo, hãy xem visit_Content phương pháp. Vì một nút nội dung chỉ bao bọc một chuỗi, nên phương thức này cũng đơn giản như nó nhận được.

def visit_Content(node)
  node.content
end

Bây giờ, hãy chuyển sang visit_Expression phương pháp trong đó việc thay thế trình giữ chỗ bằng giá trị thực xảy ra.

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

Và cuối cùng, đối với visit_Expression để biết khóa nào cần tìm nạp từ môi trường, hãy triển khai visit_Identifier phương pháp.

def visit_Identifier(node)
  node.value
end

Với bốn phương pháp này, chúng tôi nhận được kết quả mong muốn khi cố gắng hiển thị lại mẫu.

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

Biểu thức khối thông dịch

Chúng tôi đã viết rất nhiều mã để triển khai gsub đơn giản Có thể làm. Vì vậy, hãy chuyển sang một ví dụ phức tạp 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}}

Xin nhắc lại, đây là diện mạo của AST tương ứng.

Chỉ có một loại nút mà chúng tôi chưa xử lý. Đó là visit_BlockExpression nút. Theo một cách nào đó, nó tương tự như visit_Expression nhưng tùy thuộc vào giá trị mà nó tiếp tục xử lý các câu lệnh statements hoặc inverse_statements của BlockExpression nút.

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 vào phương thức, chúng tôi nhận thấy rằng hai nhánh rất giống nhau và chúng cũng giống với visit_Template phương pháp. Tất cả chúng đều xử lý việc truy cập tất cả các nút của một Array , vậy chúng ta hãy trích xuất một visit_Array phương pháp để dọn dẹp mọi thứ một chút.

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

Với phương pháp mới được áp dụng, chúng tôi có thể xóa một số mã khỏi visit_Templatevisit_BlockExpression phương pháp.

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

Bây giờ trình thông dịch của chúng tôi xử lý tất cả các loại nút, hãy thử và hiển thị mẫu phức tạp.

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

Điều đó gần như có vẻ đúng. Nhưng khi xem xét kỹ hơn, chúng tôi nhận thấy rằng thông báo nhắc chúng tôi đăng ký danh sách gửi thư, mặc dù chúng tôi đã cung cấp subscribed: true trong môi trường. Điều đó có vẻ không đúng…

Thêm hỗ trợ cho các phương pháp của người trợ giúp

Nhìn lại mẫu, chúng tôi nhận thấy rằng có một if trong biểu thức khối. Thay vì tra cứu giá trị của subscribed trong môi trường, visit_BlockExpression đang tìm kiếm giá trị của if . Vì nó không có trong môi trường, cuộc gọi trả về nil , sai.

Chúng tôi có thể dừng lại ở đây và tuyên bố rằng chúng tôi không cố gắng bắt chước Tay lái mà là Bộ ria mép và loại bỏ if trong mẫu, điều này sẽ cung cấp cho chúng tôi kết quả mong muốn.

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

Nhưng tại sao lại dừng lại khi chúng ta đang vui? Hãy tiến xa hơn và triển khai các phương pháp trợ giúp. Chúng cũng có thể hữu ích cho những việc khác.

Hãy bắt đầu bằng cách thêm hỗ trợ phương thức helper vào các biểu thức đơn giản. Chúng tôi sẽ thêm một reverse helper, đảo ngược các chuỗi được chuyển đến nó. Ngoài ra, chúng tôi sẽ thêm một debug phương thức cho chúng ta biết tên lớp của một giá trị nhất định.

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

Chúng tôi sử dụng các lambdas đơn giản để triển khai các trình trợ giúp này và lưu trữ chúng trong một hàm băm để chúng tôi có thể tra cứu chúng theo tên của chúng.

Tiếp theo, hãy sửa đổi visit_Expression để thực hiện tra cứu trình trợ giúp trước khi thử tra cứu giá trị trong môi trường.

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ó một trình trợ giúp khớp với số nhận dạng đã cho, phương thức sẽ truy cập tất cả các đối số và cố gắng tìm kiếm các giá trị cho chúng. Sau đó, nó sẽ gọi phương thức và chuyển tất cả các giá trị làm đối số.

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 điều đó, cuối cùng chúng ta hãy triển khai if và một unless người giúp đỡ. Ngoài các đối số, chúng tôi sẽ chuyển hai lambdas cho chúng để chúng có thể quyết định xem chúng tôi có nên tiếp tục diễn giải các câu lệnh statements của nút hay không hoặc 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
 

Những thay đổi cần thiết đối với visit_BlockExpression tương tự như những gì chúng tôi đã làm với visit_Expression , chỉ lần này, chúng tôi cũng vượt qua hai con 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à với điều này, món nướng của chúng ta đã hoàn thành! Chúng tôi có thể hiển thị mẫu phức tạp bắt đầu cuộc hành trình này vào thế giới của lexer, trình phân tích cú pháp và thông dịch viên.

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ỉ làm xước bề mặt

Trong loạt bài gồm ba phần này, chúng tôi đã trình bày những kiến ​​thức cơ bản về việc tạo một ngôn ngữ tạo khuôn mẫu. Những khái niệm này cũng có thể được sử dụng để tạo ngôn ngữ lập trình thông dịch (như Ruby). Phải thừa nhận rằng chúng tôi đã đề cập đến một số điều (như xử lý lỗi thích hợp 🙀) và chỉ làm xước bề mặt của nền tảng của các ngôn ngữ lập trình ngày nay.

Chúng tôi hy vọng bạn thích bộ truyện và nếu bạn muốn nhiều hơn thế, hãy đăng ký theo dõi danh sách Ruby Magic. Nếu bây giờ bạn đang khao khát đồ ăn nhẹ, hãy đặt hàng cho chúng tôi và chúng tôi cũng có thể cung cấp cho bạn những thứ đó!