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

Interpreter

นิยาม grammar พร้อม interpreter ที่​ประเมิน​ประโยค​ใน​ภาษา​นั้น

เมื่อ domain หนึ่ง​มี​ปัญหา​รูปแบบ​ซ้ำ ๆ ที่​อธิบาย​ได้​ด้วย “ภาษา​เล็ก ๆ” ของ​ตัวเอง (little language หรือ domain-specific language) — เช่น เงื่อนไข​การ​ค้นหา, สูตร​คำนวณ​ราคา, กฎ business rule, หรือ​นิพจน์​ทาง​คณิตศาสตร์ — การ hard-code ตรรกะ​เหล่า​นี้​กระจาย​อยู่​ใน if/else จำนวน​มาก​จะ​ทำให้​แก้ไข​และ​ขยาย​ยาก

Interpreter แก้​ปัญหา​นี้​โดย นิยาม representation ของ grammar พร้อม interpreter ที่​ใช้ representation นั้น​ประเมิน​ประโยค​ใน​ภาษา แนวคิด​หลัก​คือ:

  1. แต่ละ​กฎ​ใน grammar (terminal หรือ nonterminal) map เป็น class หนึ่ง
  2. ประโยค (sentence) ที่​ต้องการ​ตีความ​ถูก​แปลง​เป็นต้นไม้​ของ object เรียก​ว่า abstract syntax tree (AST)
  3. แต่ละ 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()

ตัวอย่าง​นี้​ขยาย​จาก 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 อีกแบบ — ตัวแปรที่ต้องเปิดดูจาก Context
public 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 ที่ระดับการสร้าง AST
public 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 ไม่​ดี