ข้ามไปยังเนื้อหา

Interpreter

Interpreter ให้วิธีแทน grammar ของภาษาเล็ก ๆ ในรูปของลำดับชั้น class และประเมินประโยคในภาษานั้นด้วยการเดินผ่านต้นไม้ที่สร้างขึ้นจาก class เหล่านั้น

บางครั้งปัญหาแสดงออกได้อย่างเป็นธรรมชาติในรูปของภาษาเล็ก ๆ เช่น ตัวกรองการค้นหา, กฎการตั้งราคา, เงื่อนไข feature-flag, สูตรคำนวณ คุณอาจ hard-code แต่ละความผันแปร แต่ความผันแปรก็มีเข้ามาเรื่อย ๆ สิ่งที่คุณต้องการจริง ๆ คือปล่อยให้ผู้ใช้เขียนนิพจน์อย่าง true AND (false OR true) แล้วให้โปรแกรมประเมินผลนิพจน์นั้นได้

Interpreter โมเดลกฎแต่ละข้อของ grammar ในรูปของ class ค่าคงที่ (literal) และตัวแปรกลายเป็นนิพจน์แบบ terminal ส่วนตัวดำเนินการอย่าง AND และ OR กลายเป็นนิพจน์แบบ non-terminal ที่ถือนิพจน์ย่อยไว้ นิพจน์ในภาษานี้กลายเป็นต้นไม้ของ object เหล่านี้ — abstract syntax tree — และการประเมินผลหมายถึงการขอให้ราก (root) ตีความตัวเอง ซึ่งจะเรียกซ้ำลงไปยังลูก ๆ ของตัวเอง grammar อาศัยอยู่ในระบบชนิด (type system) ดังนั้นการขยายภาษาจึงเป็นเรื่องของการเพิ่ม class นิพจน์ pattern นี้เปล่งประกายกับ grammar เล็ก ๆ ที่นิ่งแล้ว และกลายเป็นของเทอะทะหากภาษาเติบโตใหญ่ขึ้น ในกรณีนั้น parser จริง ๆ และกลยุทธ์การประเมินผลแบบอื่นเหมาะกว่า

classDiagram
  class Expr {
    <<interface>>
    +interpret() bool
  }
  class Literal {
    -value: bool
    +interpret() bool
  }
  class And {
    -left: Expr
    -right: Expr
    +interpret() bool
  }
  class Or {
    -left: Expr
    -right: Expr
    +interpret() bool
  }
  Expr <|.. Literal
  Expr <|.. And
  Expr <|.. Or
  And o-- Expr : left and right
  Or o-- Expr : left and right
ตัวดำเนินการเป็นนิพจน์แบบ non-terminal ที่ถือนิพจน์ย่อยไว้ ส่วน literal เป็น terminal
  • Expr — interface ร่วม ทุกโหนดรู้วิธี interpret ตัวเองและคืนผลลัพธ์
  • Literal — นิพจน์แบบ terminal นั่นคือค่าคงที่ที่ไม่มีลูก
  • And, Or — นิพจน์แบบ non-terminal แต่ละตัวถือนิพจน์ย่อยไว้และรวมผลลัพธ์ของนิพจน์ย่อยเหล่านั้น
  • Client — ประกอบต้นไม้ของนิพจน์ (ด้วยมือหรือผ่าน parser) แล้วเรียก interpret ที่ราก

ตัวประเมินนิพจน์บูลีนจิ๋ว เราสร้างต้นไม้สำหรับ true AND (false OR true) โดยตรงแล้วตีความต้นไม้นั้น ผลลัพธ์คือ true

interface Expr {
interpret(): boolean;
}
class Literal implements Expr {
constructor(private value: boolean) {}
interpret(): boolean {
return this.value;
}
}
class And implements Expr {
constructor(private left: Expr, private right: Expr) {}
interpret(): boolean {
return this.left.interpret() && this.right.interpret();
}
}
class Or implements Expr {
constructor(private left: Expr, private right: Expr) {}
interpret(): boolean {
return this.left.interpret() || this.right.interpret();
}
}
// true AND (false OR true)
const expr = new And(new Literal(true), new Or(new Literal(false), new Literal(true)));
console.log(expr.interpret()); // true
  • ข้อดี: grammar เล็ก ๆ ที่นิ่งแล้วจับคู่ลงตัวกับหนึ่ง class ต่อหนึ่งกฎ และแต่ละ class อ่านและทดสอบแยกได้ง่าย
  • ข้อดี: การขยายภาษามักหมายถึงการเพิ่ม class นิพจน์หนึ่งตัว ไม่ใช่การแก้ไขตัวประเมินผลแบบโมโนลิธ
  • ข้อเสีย: กฎทุกข้อของ grammar กลายเป็น class ดังนั้น grammar ขนาดใหญ่จึงผลิต class เล็ก ๆ ระเบิดออกมาจำนวนมากซึ่งดูแลรักษายาก
  • ข้อเสีย: การสร้างต้นไม้ด้วยมือนั้นน่าเบื่อ คุณมักยังต้องมี parser เพื่อแปลงข้อความให้เป็น object นิพจน์อยู่ดี
  • ข้อเสีย: การตีความแบบเดินผ่านต้นไม้ช้าสำหรับ hot path เมื่อเทียบกับแนวทางแบบคอมไพล์หรือ bytecode
  • Composite อธิบายรูปทรงต้นไม้ที่นิพจน์ของ interpreter ก่อร่างขึ้น นิพจน์แบบ non-terminal คือ composite ของนิพจน์ย่อย
  • Visitor มักเสริม Interpreter แทนที่จะใส่ interpret ไว้บนทุกโหนด คุณสามารถย้ายการประเมินผล, การพิมพ์, หรือการปรับให้เหมาะสมไปไว้ใน visitor แยกต่างหากบน syntax tree ได้
InterpreterCompositeVisitor
จุดประสงค์ตีความ grammar ของภาษาtree ที่ treat leaf = branchoperation ใหม่บน structure เดิม
โครงสร้างexpression treecomponent treevisitor + element
เพิ่ม rule ใหม่เพิ่ม expression classเพิ่ม componentเพิ่ม visit method
ตัวอย่างregex, SQL parser, template enginefile system, DOMAST transformer
Interpreter pattern แทนอะไรในรูปของลำดับชั้น class?
นิพจน์แบบ non-terminal อย่าง AND ผลิตผลลัพธ์ของตัวเองอย่างไร?
เมื่อใดที่ Interpreter pattern กลายเป็นตัวเลือกที่ไม่เหมาะ?