Interpreter
นิยาม grammar พร้อม interpreter ที่ประเมินประโยคในภาษานั้น
เมื่อ domain หนึ่งมีปัญหารูปแบบซ้ำ ๆ ที่อธิบายได้ด้วย “ภาษาเล็ก ๆ” ของตัวเอง (little language หรือ domain-specific language) — เช่น เงื่อนไขการค้นหา, สูตรคำนวณราคา, กฎ business rule, หรือนิพจน์ทางคณิตศาสตร์ — การ hard-code ตรรกะเหล่านี้กระจายอยู่ใน if/else จำนวนมากจะทำให้แก้ไขและขยายยาก
Interpreter แก้ปัญหานี้โดย นิยาม representation ของ grammar พร้อม interpreter ที่ใช้ representation นั้นประเมินประโยคในภาษา แนวคิดหลักคือ:
- แต่ละกฎใน grammar (terminal หรือ nonterminal) map เป็น class หนึ่ง
- ประโยค (sentence) ที่ต้องการตีความถูกแปลงเป็นต้นไม้ของ object เรียกว่า abstract syntax tree (AST)
- แต่ละ node มี method
Interpret()ที่ประเมินตัวเองแบบ recursive โดยอาศัยผลจาก node ลูก
ผลลัพธ์คือเมื่อกฎในภาษาเปลี่ยน หรือมีนิพจน์ชนิดใหม่เพิ่มเข้ามา เราเพิ่ม class ใหม่1 class ได้โดยไม่กระทบ class เดิม — เป็นการนำหลัก Open/Closed มาใช้กับปัญหาเชิงภาษา
Interpreter เหมาะกับ grammar ที่ เรียบง่ายและค่อนข้างคงที่ เท่านั้น หากไวยากรณ์ซับซ้อนขึ้นเรื่อย ๆ (มี operator precedence, ambiguity, error recovery ที่ซับซ้อน) ควรพิจารณาใช้ parser generator เฉพาะทาง (เช่น ANTLR) แทนการเขียน AST ด้วยมือ
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”AST มีโครงสร้างเป็น Composite — terminal expression เป็น leaf ส่วน nonterminal expression เป็น composite ที่ถือ reference ไปยังนิพจน์ลูก ทุกนิพจน์ implement interface ร่วมกันหนึ่งตัวที่มี method Interpret()
classDiagram
class Context {
+Variables
+Lookup(name)
}
class AbstractExpression {
<<interface>>
+Interpret(ctx) int
}
class TerminalExpression {
-value
+Interpret(ctx) int
}
class NonterminalExpression {
-left
-right
+Interpret(ctx) int
}
class Client {
+Build()
}
AbstractExpression <|.. TerminalExpression
AbstractExpression <|.. NonterminalExpression
NonterminalExpression o-- AbstractExpression
NonterminalExpression ..> Context
TerminalExpression ..> Context
Client ..> AbstractExpression
- AbstractExpression — interface หรือ abstract class ที่ประกาศ method
Interpret()ร่วมของทุกนิพจน์ - TerminalExpression — นิพจน์ใบ (leaf) เช่น ตัวเลขหรือตัวแปรตัวเดียว ไม่มีลูก คืนค่าตรง ๆ
- NonterminalExpression — นิพจน์ประกอบ (เช่น
Add,And,Repeat) ถือ reference ไปยังนิพจน์ลูกหนึ่งตัวหรือมากกว่า และประเมินโดยเรียกInterpret()ของลูกแล้วรวมผล - Context — เก็บสถานะร่วมระหว่างการตีความ เช่น ค่าตัวแปร, ตำแหน่งใน input stream — ถูกส่งผ่านลงไปทุก node
- Client — สร้าง (หรือ parse) AST ขึ้นมาจากประโยค แล้วเรียก
Interpret()ที่ราก
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”ขั้นตอนทั่วไปคือ: client แปลงประโยคดิบเป็น AST (อาจ parse เอง หรือสร้างด้วยมือ) → สร้าง Context → เรียก Interpret(context) ที่ node รากของ AST → การเรียกนี้ไล่ recursive ลงไปจนถึง terminal node แล้วรวมผลกลับขึ้นมา
sequenceDiagram
participant Client
participant Add as NonterminalExpression Add
participant Mul as NonterminalExpression Mul
participant Num as TerminalExpression Num
Client->>Add: Interpret(ctx)
Add->>Num: Interpret(ctx)
Num-->>Add: 3
Add->>Mul: Interpret(ctx)
Mul->>Num: Interpret(ctx)
Num-->>Mul: 4
Mul->>Num: Interpret(ctx)
Num-->>Mul: 2
Mul-->>Add: 8
Add-->>Client: 11
ตัวอย่างนี้ตีความ 3 + (4 * 2) — โครงสร้าง AST ถูกสร้างล่วงหน้าตาม operator precedence (Mul ถูกซ้อนอยู่ใต้กิ่งขวาของ Add) แล้วแต่ละ node ประเมินตัวเองโดยพึ่งพาผลจากลูกเท่านั้น ไม่รู้จักโครงสร้างทั้งต้นไม้ — นี่คือจุดที่ทำให้ pattern นี้ประกอบ (compose) กฎใหม่ ๆ ได้ง่ายเพียงเพิ่ม class NonterminalExpression ใหม่
หากต้องเพิ่ม operation บน AST เดิม (เช่น pretty-print, optimize, type-check) โดยไม่แก้ class expression เดิม ให้แยก operation ออกไปด้วย Visitor แทนที่จะยัด method ใหม่เข้าไปใน Interpret()
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่างนี้ขยายจาก terminal/nonterminal พื้นฐานให้เป็น interpreter สำหรับนิพจน์บวก-คูณ พร้อมตัวแปร (ใช้ Context เก็บค่าตัวแปร):
// Context — เก็บสถานะร่วม (ค่าตัวแปร) ระหว่างการตีความpublic class Context{ private readonly Dictionary<string, int> _variables = new();
public void SetVariable(string name, int value) => _variables[name] = value;
public int Lookup(string name) { if (!_variables.TryGetValue(name, out var value)) throw new InvalidOperationException($"ไม่พบตัวแปร '{name}'"); return value; }}
// AbstractExpression — สัญญาร่วมของทุกนิพจน์public interface IExpression{ int Interpret(Context context);}
// TerminalExpression — ค่าคงที่ตัวเลข (leaf, ไม่มีลูก)public class NumberExpression : IExpression{ private readonly int _value; public NumberExpression(int value) => _value = value; public int Interpret(Context context) => _value;}
// TerminalExpression อีกแบบ — ตัวแปรที่ต้องเปิดดูจาก Contextpublic class VariableExpression : IExpression{ private readonly string _name; public VariableExpression(string name) => _name = name; public int Interpret(Context context) => context.Lookup(_name);}
// NonterminalExpression — บวก: ประเมินลูกซ้ายขวาแล้วรวมผลpublic class AddExpression : IExpression{ private readonly IExpression _left; private readonly IExpression _right;
public AddExpression(IExpression left, IExpression right) { _left = left; _right = right; }
public int Interpret(Context context) => _left.Interpret(context) + _right.Interpret(context);}
// NonterminalExpression — คูณ: มี precedence สูงกว่า Add ที่ระดับการสร้าง ASTpublic class MultiplyExpression : IExpression{ private readonly IExpression _left; private readonly IExpression _right;
public MultiplyExpression(IExpression left, IExpression right) { _left = left; _right = right; }
public int Interpret(Context context) => _left.Interpret(context) * _right.Interpret(context);}
// Client — ประกอบ AST ด้วยมือให้แทนนิพจน์ "x + 4 * 2" แล้วตีความpublic class Program{ public static void Main() { var context = new Context(); context.SetVariable("x", 3);
// AST: Add(Variable(x), Multiply(Number(4), Number(2))) IExpression expression = new AddExpression( new VariableExpression("x"), new MultiplyExpression(new NumberExpression(4), new NumberExpression(2)));
int result = expression.Interpret(context); // 3 + (4 * 2) = 11 Console.WriteLine(result); }}ในระบบจริง ขั้นตอนสร้าง AST (parsing) มักแยกออกเป็น lexer/parser ต่างหาก แล้วส่งต้นไม้ที่ได้เข้ามาให้ Interpret() ประเมินเท่านั้น — ตัว pattern เองไม่ได้ครอบคลุมการ parse
เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- domain มีกฎหรือนิพจน์ที่อธิบายได้เป็น grammar ง่าย ๆ และซ้ำบ่อยพอที่จะคุ้มค่าสร้างเป็น “ภาษาเล็ก ๆ”
- ต้องการให้ผู้ใช้ปลายทาง (หรือ config) กำหนดกฎ/สูตร/เงื่อนไขได้เอง โดยไม่ต้อง recompile code
- grammar ค่อนข้างคงที่ ไม่ซับซ้อนมาก และ map เป็น class hierarchy ได้เป็นธรรมชาติ
- ต้องการให้ AST ที่มีอยู่รองรับการตีความหลายแบบ (evaluate, pretty-print) ผ่านการเพิ่ม expression class ใหม่
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- grammar ซับซ้อน มี operator precedence, ambiguity หรือ error-recovery ที่ซับซ้อน — ควรใช้ parser generator (เช่น ANTLR, Irony) แทนการเขียน AST class ด้วยมือ
- จำนวนกฎ/นิพจน์ในภาษาจะมีมากและเติบโตเรื่อย ๆ เพราะจำนวน class จะเพิ่มตามจำนวนกฎ ทำให้ codebase บวม
- ประสิทธิภาพสำคัญมาก เพราะการ traverse ต้นไม้แบบ recursive มี overhead สูงกว่า interpreter ที่ compile เป็น bytecode หรือ closure
- ปัญหาแก้ได้ง่ายกว่าด้วย function ธรรมดา, Strategy หรือ expression tree ของภาษาที่มีอยู่แล้ว (เช่น
System.Linq.Expressions) โดยไม่ต้องออกแบบ grammar เอง
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | เพิ่มกฎ/นิพจน์ใหม่ทำได้ง่ายด้วยการเพิ่ม class (Open/Closed) |
| ข้อดี | โครงสร้าง class สะท้อน grammar โดยตรง อ่านแล้วเข้าใจภาษาได้ |
| ข้อดี | แยก concern ของ “การตีความ” ออกจาก “การสร้างต้นไม้” ได้ชัดเจน |
| ข้อเสีย | grammar ที่ซับซ้อนทำให้จำนวน class ระเบิด (class explosion) |
| ข้อเสีย | recursive traversal มี overhead ด้าน performance |
| ข้อเสีย | ไม่ครอบคลุมการ parse — ต้องมีกลไกแปลงข้อความดิบเป็น AST แยกต่างหาก |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Composite — AST ของ Interpreter คือโครงสร้าง Composite โดยตรง
- Visitor — ใช้เพิ่ม operation ใหม่บน AST เดิมโดยไม่แก้ class expression
- Strategy — ทางเลือกเมื่อไม่ต้องการ grammar เต็มรูปแบบ เพียงสลับพฤติกรรมเป็นชิ้น ๆ
- Chain of Responsibility — อีก pattern ที่ส่ง request ผ่านลำดับของ object เช่นเดียวกับการไหลของ
Interpret() - Message Chains — กลิ่น code ที่อาจเกิดขึ้นได้หากออกแบบ AST traversal ไม่ดี