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 đó!