Keywords: mini projects, tooling, error handling, testing
อ่านแบบคน Python:
- ถ้าอยากเอา “ภาพรวม” ก่อน: หนึ่งโปรเจกต์ = หนึ่ง crate เพื่อให้ทดลองได้เต็มที่
- ถ้าอยาก “ลงมือทำ”: ทำให้ compile ผ่านก่อน แล้วค่อยเพิ่ม test ทีละเคส
- ถ้าติด: เปิด 12-learning-playbook.md
บทนี้คือชุด mini-projects สำหรับฝึก Rust แบบค่อยเป็นค่อยไป โดยตั้งใจให้ ปลอดภัย และ โฟกัสแก่นภาษา:
- ทำงานแบบ local/offline
- ไม่แตะ network/proxy
- ไม่ควบคุมระบบ/โปรเซส
- ไม่ยุ่งกับข้อมูลลับ (token/key/credential)
เป้าหมายไม่ใช่ “ทำของให้ใหญ่” แต่คือ “ฝึกโครงสร้างที่ถูก” ให้เป็นนิสัย: parse → validate → run → test
- หนึ่งโปรเจกต์ = หนึ่ง crate (
cargo new ...) เพื่อแยกสนามทดลอง - ทำงานเป็นวงจร:
cargo check→ แก้ทีละจุด → เติม tests →cargo fmt/cargo clippy - ฝึก 4 แกนหลัก:
- error path (ทำให้พังแล้วบอกชัด)
- owned vs borrowed (รับเป็น
&strแต่เก็บเป็นString) - enum/match (บังคับให้เคสครบ)
- tests (พิสูจน์พฤติกรรมแทนการเดา)
- อย่าเริ่มจาก feature ครบทุกอย่าง
- เริ่มจากเวอร์ชันที่รันได้ แล้วค่อย tighten ทีละชั้น
- ถ้าเริ่มรู้สึกว่าโค้ดเริ่มมี
if/elseยาว ๆ ให้หยุดแล้วหา “type ที่ควรมี” (เช่นenum Command,struct Config)
Rust compiler ให้ข้อมูลเยอะมาก ถ้าคุณแก้ทีละจุด จะเห็น pattern เร็ว
- อย่า refactor + เปลี่ยน API + เพิ่ม feature พร้อมกัน
- ถ้าจะแก้เรื่อง ownership/borrow ให้แก้จน
cargo checkผ่านก่อน แล้วค่อยเพิ่ม feature
แนวที่ปลอดภัยและชัด:
- clone ที่ boundary:
- ตอนรับ input แล้วต้องเก็บลง state (
&str→String) - ตอนต้องปล่อย borrow ให้สั้นลง (เพื่อไม่ชน borrow checker)
- ตอนรับ input แล้วต้องเก็บลง state (
สิ่งที่ควรระวัง:
- ถ้าเห็น
clone()โผล่หลายจุดเพื่อ “ให้คอมไพล์ผ่าน” ให้หยุดและทบทวน ownership model
ถือว่าจบโปรเจกต์หนึ่งเมื่อ:
cargo checkผ่าน- มี
cargo testอย่างน้อย 3 เคส (happy path + edge case + invalid) - boundary ชัด: parse / validate / run แยกกันพอสมควร
- output ไม่ dump ทั้งโลก: พิมพ์เฉพาะ summary ที่คนใช้ต้องการ
เริ่มแบบนี้ก่อน (พอทำเสร็จค่อยขยาย):
src/
main.rs # รับ args + I/O + print output
lib.rs # (optional) เอา logic ออกมาเพื่อให้ test ง่าย
แนวคิด:
main.rs= boundary (args/file/stdout)lib.rs= pure-ish logic (parse/validate/run) ที่ทดสอบง่าย
Output (example):
(no output — structure suggestion)
ถ้าคุณทำ test แล้วรู้สึกว่า “ต้องเตรียม args/file เยอะ” นั่นคือสัญญาณว่า logic ควรถูกย้ายไป lib.rs
- รับ path ผ่าน CLI:
app config path/to/config.json - อ่านไฟล์ (I/O)
- parse JSON เป็น struct (schema)
- validate semantics ที่ serde จับไม่ได้ (เช่นห้ามว่าง/ห้ามซ้ำ)
- แสดงสรุป config แบบสั้น ๆ (ไม่ dump ทั้งไฟล์)
Result+?+ การออกแบบ error message- แยก “parse” ออกจาก “validate” (ทำให้ test ง่าย)
- owned/borrowed: input เป็น
&strแต่ state เก็บเป็นString
- อ่านไฟล์ local เท่านั้น (ไม่รับ URL)
- หลีกเลี่ยงการพิมพ์ secrets (ถ้ามี field แนว
token,keyให้แสดงเป็น"(redacted)"หรือไม่แสดง) - ไม่ทำ network calls
app_name: String(ห้ามว่าง)port: u16(ควรอยู่ในช่วง 1..=65535)log_level: Stringหรือenum LogLevel(ถ้าพร้อม)features: Vec<String>(ห้ามซ้ำ)
{
"app_name": "demo",
"port": 8080,
"log_level": "info",
"features": ["pretty_print", "strict_validation"]
}use std::path::Path;
fn parse_config_str(input: &str) -> Result<Config, ConfigError> { /* ... */ }
fn validate_config(cfg: &Config) -> Result<(), ConfigError> { /* ... */ }
fn load_config_file(path: &Path) -> Result<Config, ConfigError> { /* ... */ }Output (example):
(no output — function signatures only)
- ทำ I/O ให้จบก่อน
- อ่านไฟล์เป็น
String - ถ้าอ่านไม่ได้ ให้ error มี context ว่าอ่านไฟล์ไหน
- ทำ parse ให้ผ่าน
- เริ่มจาก
#[derive(Deserialize)] struct Config { ... } - ถ้า schema ยังไม่นิ่ง: เริ่มจาก
serde_json::Valueแล้วค่อย tighten (ย้อนดูบท 09)
- ทำ validate เพิ่ม (semantics)
app_name.trim().is_empty()ต้อง fail- features ห้ามซ้ำ (เช็คด้วย
HashSet)
- ทำ output summary
- พิมพ์เฉพาะ:
app_name,port,log_level,feature_count
parse_ok_and_validate_okparse_ok_but_validate_fail(เช่นapp_name=""หรือ features ซ้ำ)parse_fail_invalid_json
- เพิ่ม
#[serde(deny_unknown_fields)]แล้วลองพิมพ์ key ผิด - แยก
ConfigRawและValidatedConfig(ตามบท 09: validate + convert)
- ออกแบบ
LogLevelเป็น enum และกำหนด ordering ให้ชัด - filter log ตามระดับ (
min_level) - format และพิมพ์ไปที่ console
- enum +
match+ ordering - string → enum (
FromStrหรือTryFrom<&str>) - data ที่สื่อความหมาย:
LogRecord
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum LogLevel { Error, Warn, Info, Debug }
#[derive(Debug, Clone)]
struct LogRecord {
level: LogLevel,
target: String,
message: String,
}
fn should_emit(level: LogLevel, min_level: LogLevel) -> bool { /* ... */ }Output (example):
(no output — type definitions only)
- ต้องการ
min_level=Infoแล้ว “ปล่อย Info/Warn/Error” แต่ไม่ปล่อย Debug - ถ้า ordering ไม่ชัด คุณจะ filter ผิดโดยไม่รู้ตัว
- parse:
"INFO"→LogLevel::Info - filter:
min=Infoต้องไม่ปล่อย Debug - property ง่าย ๆ:
should_emit(level, level) == true
- แยก sink: info ไป stdout, error ไป stderr (โยงบท 08)
- เติม field อย่าง timestamp แบบง่าย ๆ (ไม่ต้องใช้เวลา/clock จริงก็ได้ แค่ string placeholder)
- ออกแบบ
AppStateเก็บข้อมูล in-memory - ทำคำสั่ง CLI 3 แบบ:
add,list,reset - ให้คำสั่ง “ทำอย่างเดียว” และคืน error ชัด
นี่คือการฝึก “รวมศูนย์ state” (centralized state) แทนการกระจาย mutable หลายจุดแบบ Python ซึ่งพอโตแล้วหลุดง่าย
#[derive(Default, Debug)]
struct AppState { items: Vec<String> }
enum Command {
Add { text: String },
List,
Reset,
}
fn parse_args(args: &[String]) -> Result<Command, String> { /* ... */ }
fn run_command(state: &mut AppState, cmd: Command) -> Result<String, String> { /* ... */ }Output (example):
(no output — type/function signatures only)
- ทำ
listก่อน
- ถ้าว่าง ให้คืน
"(empty)"
- ทำ
add
- รวมข้อความจาก args ให้เป็น
Stringเดียว (boundary) - เก็บลง
state.items
- ทำ
reset
clear()แล้วคืนข้อความยืนยัน
- parse args ถูกทุกคำสั่ง
run_commandเปลี่ยน state ถูก (add แล้ว list มี)- reset แล้ว list กลับว่าง
- เพิ่ม
remove INDEX(ฝึก error message เวลา index ไม่ถูก) - ทำ output ให้ deterministic (เช่น list เรียงตามลำดับที่ add)
- ใช้
Arc<Mutex<usize>>แชร์ตัวนับระหว่างหลาย threads - spawn threads หลายตัวให้เพิ่มค่า แล้วรวมผล (join)
- ทำไมต้อง
Arc(shared ownership ระหว่าง threads) - ทำไมต้อง
Mutex(กัน data race) - ทำ lock ให้สั้นที่สุด (ลด deadlock/risk)
- ตั้งค่าทดลองให้เล็กก่อน
- เช่น 4 threads, thread ละ 10_000 increments
-
clone
Arcเข้า thread -
ใน loop
- lock → increment → drop lock
- join ทุก thread แล้ว assert ผลรวม
- เขียนฟังก์ชันที่คืนค่าผลลัพธ์ (ไม่ print) แล้ว test ด้วย
assert_eq! - รันหลายครั้งเพื่อดูว่าผล deterministic
- ลองเปลี่ยนเป็น
AtomicUsizeแล้วเทียบ mental model (ไม่ต้องทำ performance)
หลังทำครบ 4 โปรเจกต์ คุณควรจะ:
- อ่าน compiler error ได้ไวขึ้น และแยกได้ว่าเป็น type/ownership/lifetime
- ออกแบบ state ให้ชัดขึ้น (อะไรควร owned, อะไรควร borrowed)
- ใช้ enum + match เพื่อบังคับให้เคสครบ (ไม่ลืม edge case)
- เขียน tests เพื่อยืนยันพฤติกรรม (validate) แทนการเดา
ถ้าติดระหว่างทำ ให้เปิดบทถัดไป 12-learning-playbook.md แล้วไล่ checklist ทีละข้อ