class · instance · method — และการประกอบทั้งคอร์สเข้าด้วยกัน
บทที่ 7 จบลงตรงที่ภาษา nok มี closure ที่จำ environment ของตัวเองได้ และมี resolver ที่ตัดสินล่วงหน้าว่าชื่อแต่ละตัวหมายถึงตัวแปรตัวไหน
บทสุดท้ายนี้เติมของที่ค้างอยู่ชิ้นเดียว คือ class
เรื่องที่น่าสนใจของมันไม่ใช่ว่ามันเพิ่มกลไกอะไรเข้ามา แต่คือมันแทบไม่เพิ่มอะไรเลย
this ทำงานด้วยกลไกของ closure จากบทที่ 7 ทั้งดุ้น ส่วนที่เก็บ property ที่แก้ได้ทั้งที่ถูกถือร่วมกันอยู่หลายที่
ก็ใช้เหตุผลเดียวกับ Environment ในบทที่ 5 เป๊ะๆ
บทนี้คัด code จาก scripts/rust-interpreter/src/class.rs เป็นหลัก และยกจาก src/main.rs เพิ่มในส่วน capstone
รันตามได้ด้วย cargo build ใน scripts/rust-interpreter แล้วสั่ง ./target/debug/nok examples/08-classes.nok
ผลที่ออกมาคือสิ่งเดียวกับที่หัวข้อสุดท้ายของบทแสดงไว้ทีละ byte
class ที่ประกาศแล้ว คือค่าตัวหนึ่งในภาษา
หัวข้อที่มีชื่อว่า “class ที่ประกาศแล้ว คือค่าตัวหนึ่งในภาษา”หัวข้อนี้แสดงว่า class ของ nok เก็บอะไรไว้บ้าง และทำไมการเรียกมันถึงกลายเป็นการสร้างวัตถุ
pub struct Class { pub name: String, /// ตาราง method · ไม่มีสาย superclass ให้ไล่ต่อ (ดูหมายเหตุหัว file) pub methods: HashMap<String, Rc<Function>>,}
impl Class { pub fn find_method(&self, name: &str) -> Option<Rc<Function>> { self.methods.get(name).cloned() }}หน้าตาของมันคือชื่อกับตารางที่แมป MethodMethodฟังก์ชันที่ผูกอยู่กับ class และเรียกผ่าน instance ได้ โดยรู้จักตัว instance ที่เรียกมันผ่านชื่อพิเศษ ไปหาตัว function ของมัน ไม่มีอะไรมากกว่านั้น
find_method ดูตารางของ class ตัวเองแล้วจบในบรรทัดเดียว ไม่มีที่ให้ไล่ต่อ
class ที่ประกาศแล้วถูกเก็บลง environment เหมือนตัวแปรทั่วไป มันจึงส่งเข้า function ได้ และพิมพ์ออกมาดูได้
การเรียกมันด้วยวงเล็บคือสิ่งที่ทำให้มันต่างจากค่าอื่น
Value::Class(c) => { let instance = Rc::new(Instance::new(Rc::clone(&c))); match c.find_method("init") { Some(init) => { check_arity(&c.name, init.arity(), args.len(), line)?; init.bind(&instance).call(self, args)?; } None => check_arity(&c.name, 0, args.len(), line)?, } Ok(Value::Instance(instance)) }init ไม่ใช่คำสงวนของภาษา มันเป็นชื่อธรรมดาที่ code หาด้วยข้อความตรงๆ ตอนสร้าง instance
แต่ชื่อนี้ก็ไม่ได้ธรรมดาเสียทีเดียว method ที่ชื่อ init ถูกทำเครื่องหมายไว้ให้คืน this เสมอ
และ resolver ในบทที่ 7 ก็ห้ามไม่ให้มัน return ค่าอื่นออกมา
class ที่ไม่มี init จึงรับ argument ไม่ได้เลย เพราะกรณีที่หาชื่อนั้นไม่เจอ บังคับ arity เป็นศูนย์
ราคาของการผูกพฤติกรรมไว้กับชื่อแทนที่จะผูกกับคำสงวน คือไม่มีด่านไหนทักเวลาพิมพ์ชื่อผิด
method ที่ตั้งใจให้เป็น init แต่พิมพ์เป็น innit จะกลายเป็น method ธรรมดาที่ไม่มีใครเรียก
แล้วความผิดพลาดจะไปโผล่ตอนสร้าง instance ในรูปของ arity ที่ไม่ตรง ซึ่งชี้ไปคนละที่กับต้นเหตุ
inheritance ถูกตัดออกจากภาษา และ code พูดเรื่องนั้นเอง
หัวข้อที่มีชื่อว่า “inheritance ถูกตัดออกจากภาษา และ code พูดเรื่องนั้นเอง”หัวข้อนี้บอกตรงๆ ว่าคอร์สไม่สอน inheritance และไล่ว่าต้องเติมอะไรบ้างถ้าจะทำต่อเอง
//! บทที่ 8 — class · instance · method · `this`//!//! **inheritance อยู่นอกขอบเขตของคอร์สนี้** และ file นี้ก็ไม่มีมันจริงๆ://! ไม่มีฟิลด์ `superclass` ไม่มีการไล่หา method ขึ้นไปตามสายเลือด//! `find_method` ดูตารางของ class ตัวเองแล้วจบ//!//! `this` ทำงานด้วยกลไกเดียวกับ closure ในบทที่ 7 ทั้งหมด ไม่มีอะไรใหม่://! ตอนหยิบ method ออกจาก instance เราสร้าง environment เล็กๆ ที่มี `this`//! อยู่ตัวเดียว แล้วให้ method ตัวใหม่ปิดทับ environment นั้น · ผลคือ method//! ที่ถูกหยิบออกมาเก็บใส่ตัวแปร ยังจำ instance ของมันได้ แม้ instance เดิม//! จะไม่มีใครอ้างถึงแล้วการตัดครั้งนี้ไม่ใช่การลืม และไม่ใช่การเก็บไว้เป็นบทที่เก้า มันคือขอบเขตที่เจ้าของงานเคาะไว้ตั้งแต่ต้น
ถ้าจะเติม inheritance เข้าไปเองหลังจบคอร์ส สิ่งที่ต้องแตะไม่ได้อยู่ที่ class.rs ที่เดียว
- scanner ต้องรู้จักคำสงวนตัวใหม่สำหรับอ้างถึง class แม่ ตอนนี้ตาราง keyword ยังไม่มีคำนั้น
- ไวยากรณ์กับ AST ต้องรับรูปประโยคที่บอกว่า class หนึ่งสืบทอดจากอีก class และเพิ่มรูป expression ตัวใหม่
Classต้องมีฟิลด์ที่ชี้ไปหา class แม่ และfind_methodต้องไล่ขึ้นไปต่อเมื่อหาในตารางตัวเองไม่เจอ- resolver ต้องมี scope พิเศษอีกชั้นสำหรับชื่อที่อ้างถึง class แม่ แบบเดียวกับที่ตอนนี้ทำกับ
this
เหตุผลที่มันถูกตัดจึงไม่ใช่ว่ากลไกยาก แต่เพราะการแก้สี่ด่านพร้อมกันคือคอร์สอีกครึ่งใบ
ข้อดีที่ได้กลับมาจากการไม่มีสายเลือดคือความล้มเหลวตรงไปตรงมา — หา method ไม่เจอในตารางเดียวคือจบ ไม่มีกรณีที่ method ถูกหยิบมาจากที่ที่คนอ่าน code ไม่ได้มอง
instance กับที่เก็บ property ของตัวเอง
หัวข้อที่มีชื่อว่า “instance กับที่เก็บ property ของตัวเอง”หัวข้อนี้อธิบายว่าทำไมที่เก็บ property ของ instance ต้องอยู่หลัง
RefCellและราคาของทางเลือกนั้น
/// วัตถุหนึ่งตัวที่สร้างจาก class////// `fields` เป็น `RefCell` ไม่ใช่ฟิลด์ธรรมดา เพราะ `obj.x = 1` ต้องแก้ instance/// ที่กำลังถูกถือร่วมกันอยู่หลายที่ (ตัวแปร · `this` ใน method · closure ที่ผูกไว้)/// เหตุผลเดียวกับ `Environment` ในบทที่ 5 เป๊ะๆpub struct Instance { pub class: Rc<Class>, fields: RefCell<HashMap<String, Value>>,}InstanceInstanceวัตถุที่ถูกสร้างจาก class หนึ่งตัว มีที่เก็บ property ของตัวเองแยกจากตัวอื่น ตัวหนึ่งถือ class ของมันไว้ด้วย Rc และถือที่เก็บ property ของตัวเองแยกจาก instance ตัวอื่น
จุดที่ต้องคิดคือคำว่า RefCell
คำสั่ง c.value = 1; แก้ค่าใน instance ที่ตอนนั้นมีคนถืออยู่หลายคนพร้อมกัน ทั้งตัวแปรที่ผู้ใช้ประกาศ
และ environment ของ method ที่กำลังทำงานอยู่
Rust ห้ามยืมแบบแก้ไขได้ขณะที่ยังมีการยืมแบบอ่านค้างอยู่ ตั้งแต่ตอนคอมไพล์ ทางออกจึงเป็นตัวเดียวกับบทที่ 5
pub fn set(&self, name: &str, value: Value) { self.fields.borrow_mut().insert(name.to_string(), value); }&self ตัวนี้อ่านแล้วขัดสายตา เพราะมันแก้ของทั้งที่รับมาแบบอ่านอย่างเดียว
นั่นคือทั้งหมดของคำว่า interior mutability กฎการยืมไม่ได้หายไป มันย้ายไปตรวจตอนรันแทน
ราคาที่จ่ายจึงชัด โปรแกรมที่ยืมซ้อนกันผิดจังหวะจะพังตอนรัน แทนที่จะคอมไพล์ไม่ผ่านตั้งแต่แรก
field บัง method ไม่ใช่กลับกัน
หัวข้อที่มีชื่อว่า “field บัง method ไม่ใช่กลับกัน”หัวข้อย่อยนี้แสดงลำดับการหา property และผลที่ตามมาเมื่อชื่อสองฝั่งชนกัน
/// หา property: field ก่อน แล้วค่อย method /// /// ลำดับนี้แปลว่า field ที่ชื่อชนกับ method จะบัง method ไว้ ซึ่งเป็นกฎที่ /// ต้องเลือกข้างให้ชัด · nok เลือกให้ field ชนะ เพราะมันคือสิ่งที่โปรแกรมเมอร์ /// เพิ่งเขียนลงไปเอง /// /// รับ `self: &Rc<Self>` ไม่ใช่ `&self` เพราะการผูก `this` ต้องเก็บ `Rc` /// ของ instance ตัวนี้ไว้ในนั้น — จาก `&self` เปล่าๆ หา `Rc` กลับไม่ได้ pub fn get(self: &Rc<Self>, name: &str) -> Option<Value> { if let Some(v) = self.fields.borrow().get(name) { return Some(v.clone()); } let method = self.class.find_method(name)?; Some(Value::Function(Rc::new(method.bind(self)))) }ลำดับนี้ตัดสินคำถามที่ต้องมีคำตอบเดียว คือถ้า field กับ method ชื่อเดียวกัน ใครชนะ
nok ให้ field ชนะ เหตุผลคือ field คือสิ่งที่โปรแกรมเมอร์เพิ่งเขียนลงไปเอง ส่วน method มาจากตอนประกาศ class
ราคาของกฎนี้คือ field ที่ตั้งชื่อชนกับ method จะบัง method นั้นไปเงียบๆ ไม่มีคำเตือนสักบรรทัด
อีกจุดที่ต้องอ่านช้าๆ คือ self: &Rc<Self> ที่รับมาแทน &self เปล่าๆ
การผูก this ต้องเก็บตัวชี้ของ instance ตัวนี้ไว้ในนั้น และจาก &self ธรรมดาหา Rc ที่ครอบมันอยู่กลับไม่ได้
this ไม่ใช่ของใหม่ มันคือ closure จากบทที่ 7
หัวข้อที่มีชื่อว่า “this ไม่ใช่ของใหม่ มันคือ closure จากบทที่ 7”หัวข้อนี้แสดงว่าการผูก
thisใช้กลไกเดียวกับ closure ทุกประการ ไม่มีเครื่องมือใหม่เข้ามาเลย
impl Function { /// ผูก method เข้ากับ instance หนึ่งตัว แล้วคืน function *ตัวใหม่* /// /// ไม่ได้แก้ของเดิม — method บน class ยังเป็นตัวที่ไม่ผูกกับใคร ทุกครั้งที่ /// หยิบมันออกจาก instance จะได้ function ที่ผูก `this` ไว้แล้วคนละตัว /// /// นี่คือจุดที่ `Rc` สร้างวงแหวนได้: instance ถือ class · class ถือ method · /// method ที่ผูกแล้วถือ environment · environment ถือ instance กลับมา /// หน่วยความจำก้อนนี้จึงไม่ถูกคืน · nok ไม่มี GC และรับข้อแลกนี้ไว้โดยตั้งใจ pub fn bind(&self, instance: &Rc<Instance>) -> Function { let scope = Environment::child(&self.closure); scope.borrow_mut().define("this", Value::Instance(Rc::clone(instance))); Function::new(Rc::clone(&self.decl), scope, self.is_initializer) }}สามบรรทัดในตัว function นี้คือทั้งหมดของคำว่า this
บรรทัดแรกสร้าง environment ลูกของ environment ที่ method ตัวนั้นถูกประกาศไว้
บรรทัดที่สองใส่ชื่อเดียวลงไปในนั้น ส่วนบรรทัดที่สามสร้าง function ตัวใหม่ที่ปิดทับ environment ก้อนนั้น
resolver ในบทที่ 7 นับชั้น scope ให้ตรงกับ environment ชุดนี้เป๊ะๆ สองฝั่งจึงต้องแก้พร้อมกันเสมอ
ผลที่ตามมาวัดได้จริง method ที่ถูกหยิบออกจาก instance แล้วเก็บใส่ตัวแปร ยังจำ instance ของมันได้ แม้จะไม่มีใครอ้างถึง instance ตัวนั้นด้วยชื่อเดิมแล้ว
ราคาของการผูกใหม่ทุกครั้ง
หัวข้อที่มีชื่อว่า “ราคาของการผูกใหม่ทุกครั้ง”หัวข้อย่อยนี้ระบุสิ่งที่ต้องจ่ายให้กับการที่
bindคืน function ตัวใหม่เสมอ
ของฟรีไม่มี และ bind คิดราคาสองทาง
ทางแรกคือทุกครั้งที่หยิบ method ออกจาก instance จะได้ function คนละตัว การหยิบ method เดิมสองครั้งแล้วเอามาเทียบกันจึงได้ผลว่าไม่เท่ากัน ซึ่งเป็นเรื่องที่คนใช้ภาษาต้องรู้ล่วงหน้า
ทางที่สองหนักกว่า และ code เขียนบอกไว้ตรงๆ ในหมายเหตุของ bind ว่าใครถือใครอยู่
- instance ถือ class ของมัน
- class ถือตาราง method
- method ที่ผูกแล้วถือ environment ที่
bindเพิ่งสร้าง - environment ก้อนนั้นถือ instance กลับมาที่จุดเริ่ม
วงแหวนแบบนี้ทำให้ตัวนับของ Rc ไม่มีวันถึงศูนย์ หน่วยความจำก้อนนั้นจึงไม่ถูกคืนจนกว่าโปรแกรมจะจบ
nok ไม่มี garbage collector และรับข้อแลกนี้ไว้โดยตั้งใจ ภาษาที่ทำงานจริงต้องแก้เรื่องนี้ ส่วนภาษาสำหรับเรียนไม่ต้อง
capstone — ทั้งคอร์สเรียงกันใน function เดียว
หัวข้อที่มีชื่อว่า “capstone — ทั้งคอร์สเรียงกันใน function เดียว”หัวข้อนี้แสดง function ที่รัน script ทั้ง file และชี้ว่าแต่ละด่านในนั้นมาจากบทไหน
fn run_file(path: &str) -> u8 { let source = match read_source(path) { Ok(s) => s, Err(code) => return code, };
let (tokens, scan_errors) = Scanner::new(&source).scan_tokens(); if !scan_errors.is_empty() { for e in &scan_errors { println!("{e}"); } return EXIT_STATIC; }
let parsed = parser::parse(tokens, 0); if !parsed.errors.is_empty() { for e in &parsed.errors { println!("{e}"); } return EXIT_STATIC; }
let resolved = resolver::resolve(&parsed.stmts); if !resolved.errors.is_empty() { for e in &resolved.errors { println!("{e}"); } return EXIT_STATIC; }
let mut interpreter = Interpreter::new(); interpreter.add_locals(resolved.locals); match interpreter.interpret(&parsed.stmts) { Ok(()) => 0, Err(e) => { println!("{e}"); EXIT_RUNTIME } }}อ่านจากบนลงล่างแล้วจะเห็นคอร์สทั้งใบเรียงกันอยู่
- scanner จากบทที่ 1 แปลงข้อความเป็นลำดับ token
- parser จากบทที่ 3 ประกอบ token เป็นต้นไม้ตามรูปที่บทที่ 2 ออกแบบไว้
- resolver จากบทที่ 7 ตอบล่วงหน้าว่าชื่อแต่ละตัวอยู่ scope ชั้นไหน
- interpreter จากบทที่ 4 ถึง 6 และบทนี้ เดินต้นไม้นั้นแล้วลงมือรัน
flowchart TD
A["ข้อความจาก file .nok"] --> B["scanner · บทที่ 1"]
B -->|"แบ่งคำไม่ผ่าน"| X["พิมพ์ทุกความผิดพลาด แล้วจบด้วย exit code 65"]
B --> C["parser · บทที่ 2 และ 3"]
C -->|"ไวยากรณ์ไม่ผ่าน"| X
C --> D["resolver · บทที่ 7"]
D -->|"ตรวจ scope ไม่ผ่าน"| X
D --> E["interpreter · บทที่ 4 ถึง 6 และบทที่ 8"]
E -->|"พังกลางคัน"| Y["พิมพ์ความผิดพลาด แล้วจบด้วย exit code 70"]
E --> F["รันครบทุกคำสั่ง แล้วจบด้วย exit code 0"]
คำบรรยายภาพ: สามด่านแรกหยุดก่อนที่โปรแกรมจะได้รันสักคำสั่ง มีแต่ด่านสุดท้ายเท่านั้นที่ความผิดพลาดเกิดขึ้นหลังจากบางส่วนของโปรแกรมทำงานไปแล้ว
ความต่างระหว่างสองแบบนั้นถูกบันทึกไว้ใน exit code
/// ใช้คำสั่งผิดรูป (EX_USAGE)const EXIT_USAGE: u8 = 64;/// อ่าน file ไม่ได้ (EX_NOINPUT)const EXIT_NO_INPUT: u8 = 66;/// ผิดพลาดตั้งแต่ก่อนรัน — แบ่งคำ · ไวยากรณ์ · ตรวจ scope (EX_DATAERR)const EXIT_STATIC: u8 = 65;/// ผิดพลาดระหว่างรัน (EX_SOFTWARE)const EXIT_RUNTIME: u8 = 70;การแยกสองกรณีนี้ไม่ได้มีไว้ให้คนอ่าน คนอ่านเห็นข้อความความผิดพลาดอยู่แล้ว
มันมีไว้ให้เครื่องอ่าน script ที่เรียก nok ต่ออีกทีจะได้แยกออกว่าโปรแกรมไม่ได้เริ่ม กับโปรแกรมเริ่มแล้วพังกลางทาง
REPL ต่างจากตัวรัน file ตรงที่มันต้องจำ
หัวข้อที่มีชื่อว่า “REPL ต่างจากตัวรัน file ตรงที่มันต้องจำ”หัวข้อย่อยนี้ชี้จุดเดียวใน REPL ที่พลาดแล้วทำให้คำตอบของ resolver จากบรรทัดก่อนใช้ไม่ได้
fn run_repl() -> u8 { let mut interpreter = Interpreter::new(); let stdin = io::stdin(); // เลข id ของ node เดินหน้าต่อไปเรื่อยๆ ข้ามบรรทัด · ถ้ารีเซ็ตเป็น 0 ทุกบรรทัด // คำตอบของ resolver จากบรรทัดก่อนจะถูกทับ แล้ว closure เก่าจะหาชื่อผิดตัว let mut next_id = 0usize;ตัวรัน file เห็นโปรแกรมทั้งก้อนในคราวเดียว ส่วน REPL เห็นทีละบรรทัดและต้องต่อของเก่าให้ติด
resolver ตอบคำถามเรื่อง scope โดยอ้างถึงแต่ละจุดในต้นไม้ด้วยเลขประจำตัว
ถ้าเลขนั้นเริ่มนับใหม่จากศูนย์ทุกบรรทัด คำตอบของบรรทัดก่อนจะถูกทับด้วยคำตอบของบรรทัดใหม่ที่บังเอิญได้เลขเดียวกัน
ผลคือ closure ที่สร้างไว้เมื่อบรรทัดก่อนจะไปหาชื่อผิดตัว โดยไม่มีอะไรฟ้อง
ลองรันจริง
หัวข้อที่มีชื่อว่า “ลองรันจริง”หัวข้อนี้แสดง script ตัวอย่างของบทและผลรันจริงของมัน พร้อมอ่านผลทีละบรรทัด
// บทที่ 8 — class · instance · method · this// inheritance ไม่อยู่ในภาษา nok โดยตั้งใจ ไม่มี superclass ให้เขียนตรงไหนเลยclass Counter { init(start) { this.value = start; }
bump() { this.value = this.value + 1; // คืน this ทำให้เรียกต่อกันเป็นสายได้ return this; }
show() { print this.value; }}
fun bumpAll(counter, times) { for (var i = 0; i < times; i = i + 1) counter.bump(); return counter;}
var c = Counter(0);bumpAll(c, 3).show();c.bump().bump().show();
class Greeter { init(name) { this.name = name; }
greet() { return "สวัสดี " + this.name; }}
var g = Greeter("nok");print g.greet();
// หยิบ method ออกมาเก็บใส่ตัวแปร — this ถูกผูกไว้ตั้งแต่ตอนหยิบvar bound = g.greet;g = nil;print bound();
print Counter;print c;
// อ่าน property ที่ไม่มีคือ error ตอนรัน ไม่ใช่ nil เงียบๆprint c.missing;script นี้ไม่ได้ใช้แค่ของในบทนี้ มันใช้ของจากทุกบทพร้อมกัน ทั้งตัวแปร ทั้ง loop ทั้ง function ที่รับ instance เข้าไป
35สวัสดี nokสวัสดี nok<class Counter><instance Counter>[บรรทัด 50] ผิดพลาดตอนรัน: ไม่มี property ชื่อ 'missing' บน instance ของ Counterอ่านผลนี้แล้วเห็นสี่อย่าง
- สองบรรทัดแรกคือ counter ตัวเดียวกัน ค่าเดินจากศูนย์ขึ้นไปเรื่อยๆ ตลอด script เพราะ function ที่รับ instance เข้าไปแก้ของตัวจริง ไม่ใช่สำเนา
- ข้อความทักทายออกมาสองครั้งเหมือนกันเป๊ะ ครั้งที่สองมาจาก method ที่ถูกหยิบเก็บใส่ตัวแปรไว้ก่อน
แล้วค่อยเรียกหลังจากตัวแปรเดิมถูกทับด้วย
nilไปแล้ว <class Counter>กับ<instance Counter>เป็นคนละอย่างกัน ตัวแรกคือแบบพิมพ์ ตัวหลังคือของที่ปั๊มออกมาจากแบบ- อ่าน property ที่ไม่มีคือความผิดพลาดตอนรัน ไม่ใช่
nilเงียบๆ ข้อความที่ได้คือ[บรรทัด 50] ผิดพลาดตอนรัน: ไม่มี property ชื่อ 'missing' บน instance ของ Counterแล้วโปรแกรมหยุดตรงนั้น
การเลือกให้มันเป็นความผิดพลาดแทนการคืน nil เงียบๆ คือการเลือกข้าง และราคาของมันคือความยืดหยุ่นที่หายไป
ภาษาที่คืน nil ให้ property ที่ไม่มี เขียน code แบบเดาไปก่อนได้ ส่วน nok บังคับให้ตั้งชื่อให้ถูกตั้งแต่แรก
คอร์สจบตรงนี้ ทางต่อไปเป็นคนละใบ
หัวข้อที่มีชื่อว่า “คอร์สจบตรงนี้ ทางต่อไปเป็นคนละใบ”หัวข้อนี้สรุปว่าแปดบทประกอบกันเป็นอะไร และบอกว่าอะไรอยู่นอกคอร์สนี้จริงๆ
สิ่งที่เดินมาตลอดแปดบทไม่ใช่การเพิ่มความสามารถทีละอย่าง แต่คือการแปลงข้อความหนึ่งก้อนให้เป็นโครงสร้างที่ลึกขึ้นทีละชั้น
แต่ละชั้นทำงานเดียวและตอบคำถามเดียว ชั้นที่ตอบไม่ได้ก็ส่งความผิดพลาดขึ้นมาพร้อมเลขบรรทัด แทนที่จะเดา
ที่สำคัญกว่านั้นคือทุกชั้นถูกออกแบบใหม่ให้เป็น Rust ไม่ใช่แปลจากตำราที่เขียนด้วย Java มาตรงๆ
enumกับmatchทำงานแทน Visitor pattern ได้ทั้งชุดResultทำงานแทน exception และคอมไพเลอร์บังคับให้ทุกคนที่รับมันต้องตัดสินใจ- ownership เป็นตัวบังคับให้ environment ต้องเป็น
Rc<RefCell<…>>ไม่ใช่รสนิยมของคนเขียน
ขั้นต่อไปไม่ใช่บทที่เก้า ทางที่คนถามถึงบ่อยที่สุดคือ bytecode virtual machine ซึ่งเป็นคนละเรื่องกับสิ่งที่ทำมาทั้งคอร์ส
tree-walking interpreter เดินบนต้นไม้โดยตรง ส่วน bytecode VM คอมไพล์ต้นไม้ลงเป็นชุดคำสั่งของเครื่องสมมติก่อนแล้วค่อยรัน นั่นคือ interpreter คนละตัว ไม่ใช่การต่อยอดจากตัวนี้
คอร์สนี้ไม่เคยอ้างตัวเลขความเร็วสักจุดเดียว จึงไม่มีข้ออ้างไหนต้องถอนคืนตรงนี้ มีแต่ขอบเขตที่ต้องบอกให้ครบ
นอกจาก inheritance แล้ว สิ่งที่ยังไม่มีคือตัวเก็บกวาดหน่วยความจำ และการทำให้เร็วขึ้นทุกชนิด
ทั้งสองอย่างเป็นงานที่เปลี่ยนรูปของ code ไปทั้งชุด ไม่ใช่ของที่เติมเข้ามาท้ายบท
- Crafting Interpreters — Robert Nystrom, บทที่ 12 “Classes”
(ตรวจแล้ว 2026-08-12) — โครงของ class ที่คอร์สนี้เดินตาม ทั้งลำดับที่หา field ก่อน method
และการผูก
thisด้วย environment เล็กๆ หนึ่งชั้น ตัว code ในตำราเขียนด้วย Java และมี inheritance อยู่ในบทถัดไป ซึ่งคอร์สนี้ตัดออก - The Rust Programming Language — บทที่ 15.5 “RefCell and the Interior Mutability Pattern”
(ตรวจแล้ว 2026-08-12) — ที่มาของการย้ายการตรวจกฎการยืมไปทำตอนรัน ซึ่งเป็นเหตุผลที่ที่เก็บ property
ของ instance อยู่หลัง
RefCell - The Rust Programming Language — บทที่ 15.6 “Reference Cycles Can Leak Memory”
(ตรวจแล้ว 2026-08-12) — คำอธิบายของวงแหวนที่
bindสร้างขึ้น และเหตุผลที่ตัวนับของRcไม่มีวันถึงศูนย์เมื่อของสองก้อนอ้างถึงกันเอง
เช็กความเข้าใจ — บทที่ 8
ข้อ 1 / 3ภาษา nok รัน script ที่ประกาศ class Box โดยให้ init ตั้ง this.size = 1 และมี method ชื่อ size ที่คืนค่า 99 จากนั้นเขียน var b = Box(); print b.size; ผลที่พิมพ์ออกมาคืออะไร