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 - 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()); // truefrom __future__ import annotationsfrom abc import ABC, abstractmethod
class Expr(ABC): @abstractmethod def interpret(self) -> bool: ...
class Literal(Expr): def __init__(self, value: bool) -> None: self._value = value
def interpret(self) -> bool: return self._value
class And(Expr): def __init__(self, left: Expr, right: Expr) -> None: self._left = left self._right = right
def interpret(self) -> bool: return self._left.interpret() and self._right.interpret()
class Or(Expr): def __init__(self, left: Expr, right: Expr) -> None: self._left = left self._right = right
def interpret(self) -> bool: return self._left.interpret() or self._right.interpret()
# true AND (false OR true)expr = And(Literal(True), Or(Literal(False), Literal(True)))print(expr.interpret()) # Truepackage main
import "fmt"
// Expr is any node in the boolean expression tree.type Expr interface { Interpret() bool}
type Literal struct{ value bool }
func (l Literal) Interpret() bool { return l.value}
type And struct{ left, right Expr }
func (a And) Interpret() bool { return a.left.Interpret() && a.right.Interpret()}
type Or struct{ left, right Expr }
func (o Or) Interpret() bool { return o.left.Interpret() || o.right.Interpret()}
func main() { // true AND (false OR true) expr := And{ left: Literal{value: true}, right: Or{left: Literal{value: false}, right: Literal{value: true}}, } fmt.Println(expr.Interpret()) // true}// Every node in the grammar knows how to interpret itself.trait Expr { fn interpret(&self) -> bool;}
struct Literal(bool);impl Expr for Literal { fn interpret(&self) -> bool { self.0 }}
struct And(Box<dyn Expr>, Box<dyn Expr>);impl Expr for And { fn interpret(&self) -> bool { self.0.interpret() && self.1.interpret() }}
struct Or(Box<dyn Expr>, Box<dyn Expr>);impl Expr for Or { fn interpret(&self) -> bool { self.0.interpret() || self.1.interpret() }}
fn main() { // true AND (false OR true) let expr = And( Box::new(Literal(true)), Box::new(Or(Box::new(Literal(false)), Box::new(Literal(true)))), ); println!("{}", expr.interpret()); // true}ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”- ข้อดี: grammar เล็ก ๆ ที่นิ่งแล้วจับคู่ลงตัวกับหนึ่ง class ต่อหนึ่งกฎ และแต่ละ class อ่านและทดสอบแยกได้ง่าย
- ข้อดี: การขยายภาษามักหมายถึงการเพิ่ม class นิพจน์หนึ่งตัว ไม่ใช่การแก้ไขตัวประเมินผลแบบโมโนลิธ
- ข้อเสีย: กฎทุกข้อของ grammar กลายเป็น class ดังนั้น grammar ขนาดใหญ่จึงผลิต class เล็ก ๆ ระเบิดออกมาจำนวนมากซึ่งดูแลรักษายาก
- ข้อเสีย: การสร้างต้นไม้ด้วยมือนั้นน่าเบื่อ คุณมักยังต้องมี parser เพื่อแปลงข้อความให้เป็น object นิพจน์อยู่ดี
- ข้อเสีย: การตีความแบบเดินผ่านต้นไม้ช้าสำหรับ hot path เมื่อเทียบกับแนวทางแบบคอมไพล์หรือ bytecode
pattern ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “pattern ที่เกี่ยวข้อง”- Composite อธิบายรูปทรงต้นไม้ที่นิพจน์ของ interpreter ก่อร่างขึ้น นิพจน์แบบ non-terminal คือ composite ของนิพจน์ย่อย
- Visitor มักเสริม Interpreter แทนที่จะใส่
interpretไว้บนทุกโหนด คุณสามารถย้ายการประเมินผล, การพิมพ์, หรือการปรับให้เหมาะสมไปไว้ใน visitor แยกต่างหากบน syntax tree ได้
| Interpreter | Composite | Visitor | |
|---|---|---|---|
| จุดประสงค์ | ตีความ grammar ของภาษา | tree ที่ treat leaf = branch | operation ใหม่บน structure เดิม |
| โครงสร้าง | expression tree | component tree | visitor + element |
| เพิ่ม rule ใหม่ | เพิ่ม expression class | เพิ่ม component | เพิ่ม visit method |
| ตัวอย่าง | regex, SQL parser, template engine | file system, DOM | AST transformer |