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

การ​ควบคุม​ทิศทาง และ function ที่​เป็น​ค่าตัว​หนึ่ง

จนถึง​ตอน​นี้​โปรแกรม​ของ​ภาษา nok เดิน​เป็น​เส้นตรง​จาก​บน​ลง​ล่าง คำ​สั่ง​ละ​หนึ่ง​ครั้ง แล้ว​จบ

บท​นี้​เติม​สอง​อย่าง​ที่​ทำให้​มัน​เลิก​เป็น​เส้นตรง — ทาง​แยก​กับ​การวน​ซ้ำ และ​ชื่อ​ที่​เรียก​ซ้ำ​ได้

ของ​ทั้ง​สอง​อย่าง​วาง​อยู่​บน​กลไก​ที่​มี​อยู่​แล้ว​เกือบ​ทั้งหมด สิ่ง​ที่​ต้อง​เขียนใหม่จริงๆ มี​น้อย​กว่า​ที่​คิด

สาม​คำ​สั่ง​ควบคุม​ทิศทาง แต่​ต้นไม้​รู้จัก​แค่​สอง​อัน

หัวข้อ​ที่​มีชื่อ​ว่า “สาม​คำ​สั่ง​ควบคุม​ทิศทาง แต่​ต้นไม้​รู้จัก​แค่​สอง​อัน”

หัวข้อ​นี้​แสดง​ว่า if กับ while เขียนตรงๆ ลงใน evaluator ได้ ส่วน for ถูก​คลี่​เป็น while ไป​แล้ว​ตั้งแต่​ตอน parse

if กับ while แทบ​ไม่​ต้อง​ออกแรง​คิด เพราะ Rust มี​ทั้ง​สอง​อย่าง​อยู่​แล้ว และ​ความหมาย​ของ​มัน​ตรง​กัน

src/interpreter.rs · if กับ while ยืม​ของ Rust มาตรงๆ (ตัด​มา​บาง​ส่วน)
Stmt::If { cond, then_branch, else_branch } => {
if self.evaluate(cond)?.is_truthy() {
self.execute(then_branch)?;
} else if let Some(alt) = else_branch {
self.execute(alt)?;
}
}
Stmt::While { cond, body } => {
while self.evaluate(cond)?.is_truthy() {
self.execute(body)?;
}
}

while ของ nok คือ while ของ Rust จริงๆ ไม่ใช่​การ​จำลอง ตัว​ที่​ควบคุม​การวน​คือ is_truthy ซึ่ง​เป็น​กฎ​ที่​บท​ที่ 4 ตรึง​ไว้​แล้ว​ว่า​มี​แค่ nil กับ false เท่านั้น​ที่​เป็น​เท็จ

ส่วน for ไม่มี node ของ​ตัวเอง​ใน​ต้นไม้​เลย​สัก​ตัว มัน​ถูก​ประกอบ​ขึ้น​จาก​ของ​ที่​มี​อยู่​แล้ว​ตั้งแต่​ตอน parse

src/parser.rs · for ถูก​คลี่​เป็น while ตอน parse (ตัด​มา​บาง​ส่วน)
let mut body = self.statement()?;
if let Some(incr) = incr {
body = Stmt::Block(vec![body, Stmt::Expression(incr)]);
}
body = Stmt::While { cond, body: Box::new(body) };
if let Some(init) = init {
body = Stmt::Block(vec![init, body]);
}
Ok(body)

อ่าน​จาก​ล่าง​ขึ้น​บน​แล้ว​เห็น​รูปร่าง​ที่​ได้ — ส่วน​เริ่มต้น​ถูก​ห่อ​ไว้​ชั้น​นอก​สุด ถัด​เข้า​มา​คือ while และ​ส่วน​เพิ่ม​ค่า​ถูก​ต่อ​ท้าย body ให้​กลาย​เป็น​คำ​สั่ง​สุดท้าย​ของ​แต่ละ​รอบ

เทคนิค​นี้​เรียก​ว่า desugaring และ​กำไร​ของ​มัน​ชัดเจน คือ evaluator ไม่​ต้อง​รู้จัก for เลย​สัก​บรรทัด

ราคา​ของ desugaring

สิ่ง​ที่​ผู้​เขียน​พิมพ์ กับ​สิ่ง​ที่​เครื่องมือ​หลัง​จาก​นี้​เห็น ไม่ใช่​ของ​อย่าง​เดียวกัน​อีก​ต่อ​ไป

pretty-printer ของ​บท​ที่ 2 และ resolver ของ​บท​ที่ 7 เดิน​บน​ต้นไม้​ที่​คลี่​แล้ว จึง​พูด​ถึง for ไม่​ได้ เพราะ​มัน​ไม่มี​อยู่​ตรง​นั้น

และ​ถ้า​วัน​หนึ่ง​ภาษา​อยาก​มี continue การ​คลี่​แบบ​นี้​จะ​กลาย​เป็น​ปัญหา​ทันที — continue ต้อง​ไป​ทำ ส่วน​เพิ่ม​ค่า​ก่อน​แล้ว​ค่อย​วน​รอบ​ใหม่ แต่​ใน​ต้นไม้​ที่​คลี่​แล้ว ส่วน​เพิ่ม​ค่า​เป็น​แค่​คำ​สั่ง​ธรรมดา ตัว​หนึ่ง​ใน block ไม่ใช่​ส่วน​หัว​ของ​การวน​ที่​กระโดด​ข้าม​ไป​หา​ได้

หัวข้อ​นี้​อธิบาย​ว่า operator สอง​ตัว​นี้​ตัดสิน​จาก​ฝั่ง​ซ้าย​ก่อน และ​ค่าที่​คืน​คือ​ค่า​เดิม​ที่​ยัง​ไม่​ถูก​แปลง

src/interpreter.rs · and / or ลัดวงจร (ตัด​มา​บาง​ส่วน)
// `and` / `or` ลัดวงจร: ถ้าฝั่งซ้ายตัดสินผลได้แล้ว ฝั่งขวาไม่ถูกประเมินเลย
// และค่าที่คืนคือ *ค่าเดิม* ไม่ใช่ boolean ที่แปลงแล้ว
Expr::Logical { left, op, right } => {
let l = self.evaluate(left)?;
let decided = match op {
LogicalOp::Or => l.is_truthy(),
LogicalOp::And => !l.is_truthy(),
};
if decided {
return Ok(l);
}
self.evaluate(right)
}

จุด​ที่​ต้อง​อ่าน​ช้าๆ คือ return Ok(l) มัน​คืน l ตัว​เดิม ไม่​ได้​คืน Value::Bool ที่​แปลง​แล้ว

ผล​คือ and กับ or ใช้​เลือก​ค่า​ได้ ไม่ใช่​แค่​ถาม​จริง​เท็จ ตัวอย่าง​ของ​บท​นี้​ใช้ nil or "ค่าสำรอง" เป็น​ค่า​สำรอง​เวลา​ฝั่ง​ซ้าย​เป็น nil ซึ่ง​เป็น​สำนวน​ที่​คน​เขียน JavaScript คุ้น​อยู่​แล้ว

ราคา​ของ​มัน​คือ​ความ​ประหลาด​ใจ​ตอน​อ่าน code คน​อื่น ค่าที่​ออก​มา​จาก or อาจ​เป็น​ชนิด​ใด​ก็ได้ คน​ที่​คิด​ว่า​ได้ boolean แน่ๆ แล้ว​เอา​ไป​ต่อ จะ​เจอ​ชนิด​ที่​ไม่​ได้​คาด​ตอน​รัน ไม่ใช่​ตอน​คอมไพล์

function เป็น​ค่าตัว​หนึ่ง ไม่ใช่​ชื่อ​ที่​แปะ​ไว้​เฉยๆ

หัวข้อ​ที่​มีชื่อ​ว่า “function เป็น​ค่าตัว​หนึ่ง ไม่ใช่​ชื่อ​ที่​แปะ​ไว้​เฉยๆ”

หัวข้อ​นี้​แสดง​โครงสร้าง​ที่​เก็บ function ที่​ผู้​ใช้​เขียน และ​บอกว่า​สาม​ฟิลด์ใน​นั้น​ทำ​หน้าที่​อะไร

ใน​ภาษา nok function เป็น First-Class FunctionFirst-Class Functionฟังก์ชัน​ที่​ถูก​ปฏิบัติ​เหมือน​ค่าอื่นๆ — เก็บ​ใส่​ตัวแปร ส่ง​เป็น​อาร์กิวเมนต์ และ​คืน​ออก​จาก​ฟังก์ชัน​ได้ จริงๆ คือ​มัน​เป็น​ค่าตัว​หนึ่ง​เหมือน​ตัวเลข​หรือ​ข้อความ

src/callable.rs · function ที่​ผู้​ใช้​เขียน​เอง
/// function ที่ผู้ใช้เขียนเอง
pub struct Function {
pub decl: Rc<FunctionDecl>,
/// environment ที่ function นี้ถูก *ประกาศ* ไว้ — ไม่ใช่ที่มันถูกเรียก
///
/// ฟิลด์เดียวนี้คือทั้งหมดของคำว่า closure · และมันคือเหตุผลที่ `Environment`
/// ต้องเป็น `Rc<RefCell<…>>`: function ถือ environment นั้นไว้ต่อ แม้ scope
/// ที่สร้างมันจะจบไปแล้ว
pub closure: EnvRef,
/// `true` เฉพาะ method ชื่อ `init` — บทที่ 8 ใช้บังคับให้มันคืน `this` เสมอ
pub is_initializer: bool,
}

decl คือ node ของ​ต้นไม้​ที่ parser สร้าง​ไว้ ส่วน is_initializer เป็น​เรื่อง​ของ​บท​ที่ 8

ฟิลด์กลาง​คือ closure และ​บท​ที่ 7 ทั้ง​บท​มี​ไว้​อธิบาย​มัน​ฟิลด์เดียว บท​นี้​ขอ​แค่​ให้​สังเกต​ว่า​มัน​มี​อยู่

ผล​ของ​การ​ที่ function เป็น​ค่า คือ​มัน​ถูก​พิมพ์​ออก​มา​ได้​เหมือน​ค่า​อื่น ตัวอย่าง​ของ​บท​นี้​พิมพ์ <fn greet> และ <native fn clock> ออก​มา​โดย​ไม่​ต้อง​เรียก​มัน​สัก​ครั้ง

ราคา​ที่​จ่าย​ไป​แล้ว​อยู่​ที่​บท​ที่ 5 — เมื่อ function เป็น​ค่าที่​รอด​ออก​มา​จาก scope ที่​สร้าง​มัน​ได้ environment ก็​ต้อง​มี​เจ้าของ​ได้​หลาย​คน นั่น​คือ​เหตุผล​ที่​มัน​เป็น Rc<RefCell<…>> ไม่ใช่ Box

หัวข้อ​นี้​ไล่​ลำดับ​ของ​การ​เรียก​หนึ่ง​ครั้ง ตั้งแต่​สร้าง scope ใหม่ ผูก parameter จนถึง​การ​รับ​ค่าที่ return ออก​มา

src/callable.rs · เรียก function หนึ่ง​ครั้ง
/// เรียก function
///
/// environment ของการเรียกครั้งนี้เป็นลูกของ `closure` ไม่ใช่ลูกของ scope
/// ที่กำลังเรียกอยู่ — สลับสองอย่างนี้เมื่อไหร่ ได้ dynamic scope แทน
/// lexical scope ทันที และผลลัพธ์จะขึ้นกับว่า *ใคร* เรียก ไม่ใช่ว่า code เขียนไว้ที่ไหน
pub fn call(
&self,
interpreter: &mut Interpreter,
args: Vec<Value>,
) -> Result<Value, RuntimeError> {
let scope = Environment::child(&self.closure);
for (param, arg) in self.decl.params.iter().zip(args) {
scope.borrow_mut().define(param.name.clone(), arg);
}
match interpreter.execute_block(&self.decl.body, scope) {
Ok(()) => Ok(self.default_result()),
Err(Flow::Return(value)) => {
if self.is_initializer {
Ok(self.default_result())
} else {
Ok(value)
}
}
Err(Flow::Error(e)) => Err(e),
}
}

บรรทัด​แรก​ของ body คือ​ทั้งหมด​ของ lexical scope — scope ใหม่​เป็น​ลูก​ของ closure ไม่ใช่​ลูก​ของ​ที่​ที่​กำลัง​เรียก​อยู่

parameter ถูก​ผูก​ด้วย zip ซึ่ง​จะ​หยุด​ที่​ฝั่ง​ที่​สั้น​กว่า และ​นั่น​คือ​เหตุผล​ที่​ต้อง​มี​การ​ตรวจ​อีก​ชั้น​ก่อน​ถึง​ตรง​นี้

Flow::Return เดินทาง​กลับ​มา​เป็น Err ทั้ง​ที่​ไม่มี​อะไร​ผิด เพราะ​ทั้ง return และ​ความ​ผิดพลาด ต่าง​ก็​เป็น “การ​จบ​ก่อน​กำหนด” เหมือน​กัน ต่าง​กัน​แค่​ปลายทาง​เป็น​ค่า​หรือ​เป็น error

หัวข้อ​ย่อย​นี้​อธิบาย​ว่าการ​ตรวจ ArityArityจำนวน​อาร์กิวเมนต์​ที่​ฟังก์ชัน​ตัว​หนึ่ง​รับ — ตรวจ​ตอน​เรียก​เพื่อ​ให้ error บอก​ได้​ว่า​คาด​กี่​ตัว​แต่​ได้​กี่​ตัว เกิด​ขึ้น​ก่อน​การ​เรียก​จริง จึง​ทำให้ error ชี้​ไป​ที่​จุด​เรียก

src/callable.rs · จำนวน parameter ของ function
/// จำนวน parameter · ตรวจก่อนเรียกทุกครั้ง (ดู `Interpreter::call`)
pub fn arity(&self) -> usize {
self.decl.params.len()
}

ตัวเลข​นั้น​ถูก​เอา​ไป​เทียบ​กับ​จำนวน​ที่​ผู้​เรียก​ส่ง​มา​จริง ก่อน​ที่ body จะ​ได้​ทำงาน​สัก​บรรทัด

src/interpreter.rs · ตรวจ arity ก่อน​เรียก​เสมอ
/// ตรวจจำนวน argument ก่อนเรียกเสมอ
///
/// ภาษาที่ไม่ตรวจ arity จะเงียบแล้วส่ง `nil` เข้าไปแทนตัวที่ขาด ซึ่งกลายเป็น
/// error คนละที่คนละบรรทัดกับจุดที่ผิดจริง
fn check_arity(name: &str, want: usize, got: usize, line: usize) -> Result<(), RuntimeError> {
if want == got {
return Ok(());
}
Err(RuntimeError {
line, message: format!("{name}() รับ argument {want} ตัว แต่ได้รับ {got} ตัว")
})
}

line ที่​ส่ง​เข้า​มา​คือ​บรรทัด​ของ จุด​เรียก ไม่ใช่​บรรทัด​ที่​ประกาศ function ข้อความ​ที่​ได้​จึง​ชี้​ไป​ที่ ที่​ที่​ผู้​เขียน​พิมพ์​ผิด​จริง

ราคา​ที่​ติด​มา​กับ​ความ​เข้มงวด​นี้​คือ​ภาษา​ที่​ไม่มี parameter ที่​ใส่​หรือ​ไม่​ใส่​ก็ได้ และ​ไม่มี function ที่​รับ​ได้​ไม่​จำกัด​จำนวน การ​เติม parameter ให้ function หนึ่ง​ตัว จึง​ทำให้​ทุก​จุด​ที่​เรียก​มัน​พัง​พร้อม​กัน

flowchart TD
    A["Expr::Call · เจอวงเล็บเปิดหลังค่า"] --> B["ประเมินตัวที่ถูกเรียก"]
    B --> C["ประเมิน argument ทีละตัว ซ้ายไปขวา"]
    C --> D{"check_arity · จำนวนตรงไหม"}
    D -->|ไม่ตรง| E["RuntimeError ที่บรรทัดของจุดเรียก"]
    D -->|ตรง| F{"ค่านี้เป็นอะไร"}
    F -->|Function| G["scope ใหม่เป็นลูกของ closure · ผูก parameter · รัน body"]
    F -->|Native| H["เรียก fn pointer ของ Rust ตรงๆ"]

คำ​บรรยาย​ภาพ: เส้นทาง​ของ​การ​เรียก​หนึ่ง​ครั้ง · การ​ตรวจ arity ยืน​อยู่​ก่อน​ทาง​แยก​สุดท้าย ทั้ง function ที่​ผู้​ใช้​เขียน​และ native function จึง​ถูก​ตรวจ​ด้วย​กฎ​เดียวกัน

หัวข้อ​นี้​แสดง​ว่า Native FunctionNative Functionฟังก์ชัน​ที่​ภาษา​เรียก​ได้​แต่​ตัว​มัน​เขียน​ด้วย​ภาษา​ที่​ใช้​สร้าง interpreter ไม่​ได้​เขียน​ด้วย​ภาษา​นั้น​เอง — เป็น​สะพาน​ออก​ไป​หา​โลก​ภายนอก คือ​อะไร เขียน​ด้วย​อะไร และ​การ​เพิ่ม​ตัว​ใหม่​แตะ code ส่วน​ไหน​บ้าง

ภาษา nok ที่​มี​แค่ print ทำ​อะไร​กับ​โลก​ภายนอก​ไม่​ได้​เลย มัน​บวก​เลข​เก่ง​แต่​ดู​นาฬิกา​ไม่​เป็น

ทางออก​คือ function ที่ เรียก​จาก nok ได้ แต่ ตัว​มัน​เขียน​ด้วย Rust

src/callable.rs · native function ทั้งหมด​ของ​ภาษา
/// function ที่เขียนด้วย Rust ไม่ใช่ด้วย nok
///
/// `func` เป็น function pointer ธรรมดา ไม่ใช่ `Box<dyn Fn>` เพราะ native ทุกตัว
/// ของเราไม่ต้องจับตัวแปรอะไรจากรอบข้าง · pointer เปล่าจึงพอ และ `Value`
/// ยังคง `Clone` ได้ราคาถูกอยู่
pub struct NativeFn {
pub name: &'static str,
pub arity: usize,
pub func: fn(&[Value]) -> Result<Value, String>,
}
/// ติดตั้ง native function ลง global scope
///
/// นี่คือด่านเดียวที่ภาษา nok ติดต่อโลกภายนอกได้ · อยากให้ภาษาทำอะไรได้เพิ่ม
/// ก็เพิ่มที่นี่ ไม่ต้องแตะ scanner · parser · หรือ interpreter เลยสักบรรทัด
pub fn install_natives(globals: &EnvRef) {
let mut scope = globals.borrow_mut();
for native in [
NativeFn { name: "clock", arity: 0, func: native_clock },
NativeFn { name: "sqrt", arity: 1, func: native_sqrt },
] {
scope.define(native.name, Value::Native(Rc::new(native)));
}
}

native ถูก define ลง global scope ตอน​สร้าง interpreter มัน​จึง​เป็น​ชื่อ​ธรรมดา​ที่​ผู้​ใช้​เขียน​ทับ​ได้ และ​ไม่ใช่​คำ​สงวน​ของ​ภาษา

func เป็น function pointer เปล่าๆ ไม่ใช่ Box<dyn Fn> ราคา​ของ​ทาง​เลือก​นั้น​คือ native จับ​ค่า​จากรอบ​ข้าง​ไม่​ได้​เลย​สัก​ตัว

ถ้า​วัน​หนึ่ง​อยาก​ได้ native ที่​ถือ handle ของ file หรือ connection ไว้ ชนิด​ของ​ฟิลด์นี้​ต้อง​เปลี่ยน เป็น​ชนิด​ที่​จับ​ค่า​ได้ ซึ่ง​ลาก​เรื่อง​อายุ​ของ​ค่าที่​มัน​จับ​ไว้​เข้า​มา​ทั้ง​ชุด

ทำไม​ตัวอย่าง​พิมพ์ clock ออก​มา​ดู แต่​ไม่​เคย​เรียก​มัน

clock คืน​เวลา​ปัจจุบัน ผล​ของ​มัน​จึง​เปลี่ยน​ทุก​ครั้ง​ที่​รัน

ผล​รัน​ของ​ทั้ง​คอร์ส​ถูก​เทียบ​ที​ละ byte กับ expected/ ค่าที่​เปลี่ยน​ตลอด​เวลา​จึง​เข้าไป​อยู่​ที่​นั่น​ไม่​ได้ ตัวอย่าง​ของ​บท​นี้​เลย​พิมพ์​แค่​ตัว function ออก​มา​ดู ไม่​เรียก​มัน

หัวข้อ​นี้​เอา​ทุก​อย่าง​ข้าง​บน​มา​รัน​ใน file เดียว แล้ว​อ่าน​ผล​ที่​ได้ที​ละ​จุด

examples/06-control-flow.nok
// บทที่ 6 — if · while · for · function · arity · native function
var n = 5;
if (n > 3) print "n มากกว่า 3"; else print "n ไม่เกิน 3";
var i = 0;
while (i < 3) {
print i;
i = i + 1;
}
for (var j = 0; j < 3; j = j + 1) print j;
// and / or ลัดวงจร — ฝั่งขวาไม่ถูกประเมินเลย function ที่ไม่มีอยู่จริงจึงไม่ทำให้พัง
print false and neverCalled();
print nil or "ค่าสำรอง";
fun fib(n) {
if (n < 2) return n;
return fib(n - 2) + fib(n - 1);
}
print fib(10);
fun greet(who) {
print "hello, " + who;
}
greet("nok");
// function เป็นค่าตัวหนึ่ง พิมพ์มันออกมาได้เหมือนค่าอื่น
print greet;
print clock;
print sqrt(144);
// จำนวน argument ผิดคือ error ที่จุดเรียก ไม่ใช่ nil ที่โผล่ไปพังที่อื่นทีหลัง
sqrt(1, 2);
expected/06-control-flow.txt
n มากกว่า 3
0
1
2
0
1
2
false
ค่าสำรอง
55
hello, nok
<fn greet>
<native fn clock>
12
[บรรทัด 34] ผิดพลาดตอนรัน: sqrt() รับ argument 1 ตัว แต่ได้รับ 2 ตัว

อ่าน​ผล​นี้​แล้ว​เห็น​สี่​อย่าง​ที่​หัวข้อ​ก่อนหน้า​พูด​ถึง

  • while กับ for ให้​ผล​ชุด​เดียวกันเป๊ะ — เลข​สาม​ตัว​เดิม​ซ้ำ​สอง​รอบ ซึ่ง​เป็น​สิ่ง​ที่​ต้อง​เป็น เพราะ​ทั้ง​คู่​กลาย​เป็นต้นไม้​รูป​เดียวกัน​ตั้งแต่​ตอน parse
  • neverCalled ไม่มี​อยู่​จริง แต่​ไม่มี error สัก​บรรทัดfalse and ตัด​จบ​ตั้งแต่​ฝั่ง​ซ้าย ฝั่ง​ขวา​จึง​ไม่​เคย​ถูก​ประเมิน
  • print greet; ให้ <fn greet> ไม่ใช่​ผล​ของ​การ​เรียก — เพราะ​ไม่มี​วงเล็บ มัน​จึง​เป็น​แค่​การ​อ่าน​ค่า ของ​ตัวแปร​ตัว​หนึ่ง​ออก​มา​พิมพ์
  • บรรทัด​สุดท้าย​พัง​ที่​จุด​เรียก ไม่ใช่​ข้าง​ใน sqrt — ข้อความ​บอก​ทั้ง​ชื่อ จำนวน​ที่​คาด และ​จำนวน​ที่​ได้​รับ ครบ​พอที่​จะ​แก้​โดย​ไม่​ต้อง​เปิด​ไป​ดู code ของ sqrt

fib(10) ที่​ให้ 55 เป็น​หลักฐาน​ของ​อีก​เรื่อง​หนึ่ง คือ function เรียก​ตัวเอง​ได้ ซึ่ง​ไม่ใช่​ของ​ฟรี — มัน​มา​จาก​การ​ที่​ชื่อ​ของ function ถูก​ประกาศ​ลง environment ก่อน ที่ body จะ​ถูก​เดิน​เข้าไป


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

เช็กความเข้าใจ — บทที่ 6

ข้อ 1 / 3

หลัง parser อ่าน for (var j = 0; j < 3; j = j + 1) print j; จบ ต้นไม้ที่ได้มีหน้าตาอย่างไร