เราหวังว่าคุณจะอุ่นสตรอพวาเฟลบนกาแฟของคุณ เพราะวันนี้เรากำลังติดสตรูปวาเฟลเหนียวหนึบ ในสองส่วนแรกของซีรีส์ เราอบ Lexer และ Parser และตอนนี้ เรากำลังเพิ่มล่ามและติดกาวเข้าด้วยกันโดยเทสตรูปลงไป
ส่วนผสม
ไม่เป็นอะไร! มาเตรียมครัวสำหรับการอบและใส่ส่วนผสมของเราลงบนโต๊ะกันเถอะ ล่ามของเราต้องการส่วนผสมหรือข้อมูลสองอย่างเพื่อทำงาน:Abstract Syntax Tree (AST) ที่สร้างไว้ก่อนหน้านี้และข้อมูลที่เราต้องการฝังลงในเทมเพลต เราจะเรียกข้อมูลนี้ว่า environment
.
ในการสำรวจ AST เราจะใช้ล่ามโดยใช้รูปแบบผู้เข้าชม ผู้เยี่ยมชม (และด้วยเหตุนี้ล่ามของเรา) ใช้วิธีการเยี่ยมชมทั่วไปที่ยอมรับโหนดเป็นพารามิเตอร์ ประมวลผลโหนดนี้ และอาจเรียกใช้ visit
วิธีการอีกครั้งกับลูกของโหนดบางส่วน (หรือทั้งหมด) ขึ้นอยู่กับความเหมาะสมสำหรับโหนดปัจจุบัน
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
. เล็กๆ กันก่อน วิธีการที่ยอมรับเทมเพลตและสภาพแวดล้อมและส่งออกเทมเพลตที่แสดงผล
module Magicbars
def self.render(template, environment = {})
tokens = Lexer.tokenize(template)
ast = Parser.parse(tokens)
Interpreter.render(ast, environment)
end
end
ด้วยสิ่งนี้ เราจะสามารถทดสอบล่ามได้โดยไม่ต้องสร้าง AST ด้วยมือ
Magicbars.render('Welcome to {{name}}', name: 'Ruby Magic')
# => nil
ไม่แปลกใจเลยที่ตอนนี้ไม่ส่งคืนอะไรเลย เรามาเริ่มใช้งาน visit
. กัน กระบวนการ. โปรดอย่าลืมว่า AST สำหรับเทมเพลตนี้หน้าตาเป็นอย่างไร
สำหรับเทมเพลตนี้ เราจะต้องประมวลผลโหนดสี่ประเภทที่แตกต่างกัน:Template
, Content
, Expression
และ Identifier
. ในการทำเช่นนี้ เราสามารถใส่ case
ขนาดใหญ่ได้ คำสั่งภายใน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
เมธอดนี้ยอมรับโหนด รับชื่อคลาส และลบโมดูลใดๆ ออกจากโหนด (โปรดอ่านบทความเกี่ยวกับการทำความสะอาดสตริง หากคุณสนใจวิธีต่างๆ ในการทำเช่นนี้) หลังจากนั้นเราใช้ send
เพื่อเรียกเมธอดที่จัดการโหนดประเภทนี้ ชื่อเมธอดสำหรับแต่ละประเภทประกอบด้วยชื่อคลาส demodulized และ visit_
คำนำหน้า เป็นเรื่องปกติเล็กน้อยที่จะมีอักษรตัวพิมพ์ใหญ่ในชื่อเมธอด แต่มันทำให้เจตจำนงของเมธอดค่อนข้างชัดเจน
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
เริ่มต้นด้วยการติดตั้ง visit_Template
กระบวนการ. ควรประมวลผล statements
. ทั้งหมด ของโหนดและเข้าร่วมผลลัพธ์
def visit_Template(node)
node.statements.map { |statement| visit(statement) }.join
end
ต่อไปมาดูที่ visit_Content
กระบวนการ. เนื่องจากโหนดเนื้อหาเพิ่งตัดสตริง วิธีการนี้จึงง่ายตามที่ได้รับ
def visit_Content(node)
node.content
end
ตอนนี้ ไปที่ visit_Expression
วิธีการที่จะแทนที่ตัวยึดตำแหน่งด้วยมูลค่าที่แท้จริง
def visit_Expression(node)
key = visit(node.identifier)
environment.fetch(key, '')
end
และสุดท้ายสำหรับ visit_Expression
วิธีที่จะรู้ว่าจะดึงคีย์ใดจากสภาพแวดล้อม มาใช้งาน visit_Identifier
วิธีการ
def visit_Identifier(node)
node.value
end
ด้วยวิธีการทั้งสี่นี้ เราจะได้ผลลัพธ์ที่ต้องการเมื่อเราพยายามแสดงเทมเพลตอีกครั้ง
Magicbars.render('Welcome to {{name}}', name: 'Ruby Magic')
# => Welcome to Ruby Magic
การตีความนิพจน์ที่ถูกบล็อก
เราเขียนโค้ดจำนวนมากเพื่อใช้งาน 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 ที่เกี่ยวข้องมีหน้าตาดังนี้
มีโหนดประเภทเดียวเท่านั้นที่เรายังไม่ได้จัดการ มันคือ visit_BlockExpression
โหนด ในทางที่คล้ายกับ visit_Expression
โหนด แต่ขึ้นอยู่กับค่า มันยังดำเนินการประมวลผล statements
. ต่อไป หรือ inverse_statements
ของ 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
กระบวนการ. พวกเขาทั้งหมดจัดการการเยี่ยมชมโหนดทั้งหมดของ 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
ตอนนี้ล่ามของเราจัดการโหนดทุกประเภทแล้ว มาลองสร้างเทมเพลตที่ซับซ้อนกัน
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
นั่น เกือบ ดูถูก แต่เมื่อมองให้ละเอียดยิ่งขึ้น เราสังเกตเห็นว่าข้อความดังกล่าวกำลังแจ้งให้เราลงชื่อสมัครใช้รายชื่อผู้รับจดหมาย แม้ว่าเราจะให้ subscribed: true
ในสภาพแวดล้อม ดูเหมือนจะไม่ถูกต้อง…
เพิ่มการรองรับวิธีการช่วยเหลือ
เมื่อมองย้อนกลับไปที่เทมเพลต เราพบว่ามี if
ในการแสดงออกของบล็อก แทนที่จะหาค่าของ subscribed
ในสภาพแวดล้อม visit_BlockExpression
กำลังค้นหาค่าของ if
. เนื่องจากไม่มีอยู่ในสภาพแวดล้อม การเรียกจึงกลับ nil
ซึ่งเป็นเท็จ
เราสามารถหยุดที่นี่และประกาศว่าเราไม่ได้พยายามเลียนแบบ 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}}
แต่ทำไมหยุดเมื่อเรามีความสนุกสนาน? ก้าวไปอีกขั้นและใช้วิธีช่วยเหลือ อาจมีประโยชน์สำหรับสิ่งอื่นเช่นกัน
เริ่มต้นด้วยการเพิ่มการรองรับวิธีการช่วยเหลือให้กับนิพจน์ทั่วไป เราจะเพิ่ม reverse
ผู้ช่วยที่ย้อนกลับสตริงที่ส่งผ่านไปยังมัน นอกจากนี้ เราจะเพิ่ม debug
เมธอดที่บอกชื่อคลาสของค่าที่กำหนด
def helpers
@helpers ||= {
reverse: ->(value) { value.to_s.reverse },
debug: ->(value) { value.class }
}
end
เราใช้แลมบ์ดาอย่างง่ายในการปรับใช้ตัวช่วยเหล่านี้และเก็บไว้ในแฮชเพื่อให้เราสามารถค้นหาโดยใช้ชื่อของพวกเขาได้
ต่อไป มาแก้ไข visit_Expression
เพื่อทำการค้นหาตัวช่วยก่อนที่จะลองค้นหาค่าในสภาพแวดล้อม
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
หากมีตัวช่วยที่ตรงกับตัวระบุที่ระบุ เมธอดจะเข้าไปที่อาร์กิวเมนต์ทั้งหมดและพยายามค้นหาค่าสำหรับอาร์กิวเมนต์ หลังจากนั้นจะเรียกใช้เมธอดและส่งค่าทั้งหมดเป็นอาร์กิวเมนต์
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
ผู้ช่วย นอกจากอาร์กิวเมนต์แล้ว เราจะส่งต่อแลมบ์ดาสองตัวให้กับพวกมัน เพื่อให้พวกเขาสามารถตัดสินใจได้ว่าเราควรตีความ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
เฉพาะครั้งนี้เท่านั้นที่เราผ่านแลมบ์ดาสองตัวด้วย
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
และด้วยสิ่งนี้ การอบของเราจึงเสร็จสิ้น! เราสามารถสร้างเทมเพลตที่ซับซ้อนซึ่งเริ่มต้นการเดินทางสู่โลกของ 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
เกาเฉพาะพื้นผิว
ในซีรีส์สามตอนนี้ เราได้กล่าวถึงพื้นฐานของการสร้างภาษาเทมเพลต แนวคิดเหล่านี้สามารถใช้เพื่อสร้างภาษาโปรแกรมที่ตีความได้ (เช่น Ruby) เป็นที่ยอมรับว่าเรามองข้ามไปสองสามอย่าง (เช่น การจัดการข้อผิดพลาดที่เหมาะสม 🙀) และขีดข่วนเพียงพื้นผิวของรากฐานของภาษาการเขียนโปรแกรมในปัจจุบัน
เราหวังว่าคุณจะสนุกกับซีรีส์นี้ และหากคุณต้องการมากกว่านี้ โปรดสมัครรับรายการ Ruby Magic หากคุณกำลังหิวสำหรับ stroopwafels โทรหาเราและเราอาจจะสามารถเติมพลังให้คุณด้วย!