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

Ruby Templating:การอบล่าม

เราหวังว่าคุณจะอุ่นสตรอพวาเฟลบนกาแฟของคุณ เพราะวันนี้เรากำลังติดสตรูปวาเฟลเหนียวหนึบ ในสองส่วนแรกของซีรีส์ เราอบ 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 โทรหาเราและเราอาจจะสามารถเติมพลังให้คุณด้วย!