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
, Expression
và Identifier
. Để 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_Template
và visit_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ứ đó!