Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การสร้างตัวแปลภาษาเทมเพลต Ruby: จาก Lexer สู่การรันโค้ด

หวังว่าคุณได้ทำ stroopwafels ให้อุ่นบนกาแฟแล้ว เพราะวันนี้เรากำลังติดสิ่งต่างๆ ด้วย stroop ที่ติด (น้ำเชื่อมที่ทำให้สองครึ่งของ stroopwafel ติดกัน) ในสองส่วนแรกของซีรีส์ เราได้อบ Lexer และ Parser และตอนนี้ เรากำลังเพิ่ม Interpreter และติดทุกอย่างเข้าด้วยกันโดยการเท stroop ทับพวกมัน

ส่วนผสม

แล้ว! มาเตรียมห้องครัวสำหรับการอบและวางส่วนผสมบนโต๊ะ ตัวแปลภาษา (interpreter) ของเราต้องการส่วนผสมหรือข้อมูลสองชิ้นเพื่อทำงาน: Abstract Syntax Tree (AST) ที่สร้างขึ้นก่อนหน้านี้ และข้อมูลที่เราต้องการฝังในเทมเพลต เราจะเรียกข้อมูลนี้ว่า environment

เพื่อเดินทางผ่าน AST เราจะ implement interpreter โดยใช้ visitor pattern Visitor (และดังนั้น interpreter ของเรา) implement generic visit method ที่รับ node เป็นพารามิเตอร์ ประมวลผล node นี้ และอาจเรียกเมธอด visit อีกครั้งกับบาง (หรือทั้งหมด) ของลูกของ node ขึ้นอยู่กับว่าสิ่งใดสมเหตุสมผลสำหรับ 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

ก่อนต่อไป มาสร้างเมธอด Magicbars.render เล็กๆ ที่รับเทมเพลตและ environment และ output เทมเพลตที่ render แล้ว

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

ด้วยสิ่งนี้ เราจะสามารถทดสอบ interpreter ได้โดยไม่ต้องสร้าง AST ด้วยมือ

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

ไม่แปลกใจที่มันไม่คืนค่าอะไรเลย 所以 เริ่ม implement เมธอด visit กัน 為了提醒 这里是这个模板的 AST 的样子。

สำหรับเทมเพลตนี้ เราจะต้องประมวลผลสี่ประเภทของ node: Template, Content, Expression, และ Identifier เพื่อทำเช่นนี้ เราสามารถใส่ case statement ใหญ่ๆ ลงในเมธอด visit แต่จะกลายเป็นอ่านยากเร็วมาก แทนที่จะทำเช่นนั้น มาใช้ความสามารถ metaprogramming ของ Ruby เพื่อให้โค้ดเป็นระเบียบและอ่านง่ายขึ้น

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

เมธอดรับ node รับชื่อคลาส และลบ modules ออก (ดูบทความของเราเกี่ยวกับการทำความสะอาดสตริงหากสนใจวิธีอื่นๆ) หลังจากนั้น เราใช้ send เพื่อเรียกเมธอดที่จัดการประเภท node นี้ ชื่อเมธอดสำหรับแต่ละประเภทประกอบด้วยชื่อคลาสที่ demodulized และ prefix visit_ มีตัวอักษรใหญ่ในชื่อเมธอดค่อนข้าง unusual แต่ทำให้เจตนาของเมธอดชัดเจน

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

เริ่ม implement เมธอด visit_Template มันควรประมวลผล statements ทั้งหมดของ node และ join ผลลัพธ์

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

ต่อไป มาดูเมธอด visit_Content เนื่องจาก content node แค่ wrap สตริง เมธอดจึงง่ายที่สุด

def visit_Content(node) node.content end

ตอนนี้ ไปที่เมธอด visit_Expression ที่การแทนที่ placeholder ด้วยค่าจริงเกิดขึ้น

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

และสุดท้าย เพื่อให้เมธอด visit_Expression รู้ว่าต้อง fetch key อะไรจาก environment มา implement เมธอด visit_Identifier

def visit_Identifier(node) node.value end

ด้วยเมธอดทั้งสี่นี้ เราได้ผลลัพธ์ที่ต้องการเมื่อลอง render เทมเพลตอีกครั้ง

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

การแปลภาษา Block Expressions

เราเขียนโค้ดมากมายเพื่อ implement สิ่งที่ 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 的 样子。

มีเพียงประเภท node เดียวที่เรายังไม่จัดการ คือ node visit_BlockExpression ในทางหนึ่ง มันคล้ายกับ node visit_Expression แต่ขึ้นอยู่กับค่า มันจะประมวลผล statements หรือ inverse_statements ของ 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

ดูเมธอดนี้ เราสังเกตว่าสองสาขาคล้ายกันมาก และยังคล้ายกับเมธอด visit_Template พวกมันทั้งหมดจัดการการ visit nodes ทั้งหมดของ 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

ตอนที่ interpreter ของเราจัดการทุกประเภท node แล้ว มาลอง render เทมเพลตที่ซับซ้อน

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

那 几乎 看起來 对了。但仔细 一看,我们 发现 信息 提示 我们 注册 邮件 列表,即使 我们 在 environment 中 提供了 subscribed: true。那 不 似乎 对...

เพิ่มการรองรับ Helper Methods

มองกลับที่เทมเพลต เราสังเกตว่ามี if ใน block expression แทนที่จะค้นหาค่า subscribed ใน environment เมธอด visit_BlockExpression กำลังค้นหาค่า if เนื่องจากไม่มีใน environment การเรียกจึงคืน nil ซึ่งเป็น false

เราอาจหยุดที่นี่และประกาศว่าเราไม่พยายเลียนแบบ Handlebars แต่เป็น Mustache และกำจัด 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}}

แต่ ทำไมต้องหยุดเมื่อเรากำลังสนุก? มา邁出額外的一步 实现 helper methods 它们 可能 对 其他 事情 也 有帮助。

เริ่มต้นด้วยการเพิ่มการรองรับ helper method ให้กับ simple expressions เราจะเพิ่ม helper reverse ที่กลับสตริงที่ส่งให้ และเพิ่มเมธอด debug ที่บอกชื่อคลาสของค่าที่ให้

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

เราใช้ lambdas 简单 来 实现 这些 helpers 并 将 它们 存储 在 hash 中 以便 按 名称 查找。

ต่อไป 修改 visit_Expression 以 在 尝试 在 environment 中 查找 值 之前 执行 helper 查找。

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

如果 有 与 给定 identifier 匹配 的 helper 该 方法 将 visit 所有 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

有了 这个,让我们 最终 实现 if 和 unless helper 除了 arguments 之外 我们 将 传递 两个 lambdas 给 它们 以便 它们 可以 决定 我们 是否 应该 继续 解释 节点 的 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 所做 的,只是 这次 我们 也 传递 两个 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

有了 这个,我们的 烘焙 完成 了!我们 可以 渲染 开始 这段 旅程 的 复杂 模板,进入 词法 分析器、解析器 和 解释器 的 世界。

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

仅仅触及表面

在 这 三部分 的 系列 中 我们 涵盖 了 创建 模板 语言 的 基础 这些 概念 也 可以 用于 创建 解释型 编程 语言 (如 Ruby) 诚然 我们 掩盖 了 几 件 事 (像 适当 的 错误 处理 🙀) 并 仅仅 触及 了 今天 编程 语言 底层 的 表面。

我们 希望 你 喜欢 这个 系列 如果 你 想要 更多 那样 的 内容 请 订阅 Ruby Magic 列表 如果 你 现在 想吃 stroopwafels 给 我们 发 一 行 我们 可能 能 为 你 提供 那些 作为 燃料!