Skip to content

Latest commit

 

History

History
492 lines (363 loc) · 20.8 KB

File metadata and controls

492 lines (363 loc) · 20.8 KB

Learn Rust From Python (Start Basic)

TOC · Prev · Next

Keywords: ownership, borrowing, error handling, macros, serde_json

อ่านแบบคน Python:

  • ถ้าอยากเอา “ภาพรวม” ก่อน: อ่านคำอธิบายแล้วค่อยดูโค้ด
  • ถ้าอยาก “ลงมือทำ”: ดู Python → ดู Rust → ดู Output แล้วค่อยย้อนกลับมาอ่านเหตุผลอีกรอบ
  • ถ้าติดระหว่างทาง: เปิด 12-learning-playbook.md

บทนี้คือ “จุดตั้งต้น” สำหรับคนเขียน Python ที่อยากเริ่ม Rust แบบไม่หลงทาง

เป้าหมายไม่ใช่ให้คุณท่อง syntax แล้วไปต่อเอง แต่ให้คุณเริ่ม คิด/ออกแบบแบบ Rust ได้ตั้งแต่วันแรก โดยยึด 3 แกนนี้:

  1. Ownership/borrowing — ข้อมูลก้อนนี้ใครเป็นเจ้าของ, ใครแค่ยืม, ยืมอ่านหรือยืมแก้
  2. Error path — จุดที่พังต้องถูกมองเห็น (ไม่กลืนเงียบ ๆ)
  3. Boundary ชัด — แยก “รับ input / I/O” ออกจาก “core logic” ให้เร็วที่สุด

วิธีอ่านที่เวิร์ค: อ่านคำอธิบาย → ดู Python → ดู Rust → ดู Output → แล้วตอบตัวเองว่า

  1. “Rust บังคับแบบนี้เพื่อกันบั๊กอะไร?”
  2. “ถ้าเป็น Python โค้ดแบบนี้พังแบบไหนได้บ้างตอน runtime?”

หมายเหตุ:

  • Output (example) คือ output ตัวอย่างเพื่อให้เห็นภาพ (ของจริงอาจต่างได้เล็กน้อยตาม environment/เวอร์ชัน)
  • (no output — ...) หมายถึง snippet นั้นเป็นแค่การประกาศ/นิยาม/assign ยังไม่ได้รัน logic ที่พิมพ์อะไร

0) Rust คืออะไร (แบบเข้าใจเร็วสำหรับคนเขียน Python)

Rust เป็นภาษา static typing ที่ออกแบบให้:

  • ปลอดภัยด้านหน่วยความจำ โดยไม่ต้องใช้ garbage collector (GC)
  • คุม concurrency ได้มั่นใจขึ้น (โดยเฉพาะเรื่อง data race ใน safe code)
  • refactor ได้มั่นใจ เพราะ compiler ช่วยเช็คโครงสร้างได้เยอะมาก

มุมมองแบบ Python → Rust ที่ควรตั้งไว้ตั้งแต่ต้น:

  • Python เก่งมากที่ “เขียนเร็ว ทดลองเร็ว” แต่บั๊กหลายแบบจะโผล่ตอน runtime (ชนิดข้อมูล/ขอบเขต reference/state กระจาย)
  • Rust ทำให้คุณ “คิดให้ชัดตั้งแต่ก่อนรัน” โดยให้ compiler ช่วยกันบั๊กจำนวนมากให้ตั้งแต่แรก (แลกกับความ strict)

คำศัพท์ที่เจอบ่อยและควรทำความคุ้น (จะวนกลับมาทั้งชุดเอกสาร):

  • ownership: ใครเป็นเจ้าของข้อมูลก้อนนี้ (ใครเป็นคน drop)
  • borrowing: ใครกำลัง “ยืมอ่าน/ยืมแก้” ข้อมูลอยู่
  • Option/Result: บังคับให้เส้นทาง “ไม่มีค่า/ล้มเหลว” ถูกมองเห็นใน type

1) Setup พื้นฐาน (Start Here)

1.1 ติดตั้ง Rust toolchain

Rust มี toolchain หลัก ๆ 2 ตัวที่คุณจะใช้แทบทุกวัน:

  • rustc (compiler)
  • cargo (build/test/dependency manager)

rustup คือ tool ที่ช่วยจัดการเวอร์ชัน toolchain (เทียบแนวคิดได้กับ pyenv/conda แต่โฟกัสที่ Rust toolchain)

ตรวจสอบว่าเครื่องมี Rust แล้ว:

  • rustc --version
  • cargo --version

อ้างอิง (ทางการ):

ทิปสำหรับคนเริ่ม (ช่วยลดสะดุด):

  • ถ้า IDE รองรับ ให้ติดตั้ง rust-analyzer (autocomplete + jump-to-def + error inline)
  • ถ้าเห็น error แปลก ๆ หลังอัปเดต ให้ลอง rustup update แล้วรันใหม่

1.2 สร้างโปรเจกต์แรกด้วย Cargo

cargo new hello_rust
cd hello_rust
cargo run

Output (example):

... (cargo build output)
Hello, world!

อ่านแบบ Python mindset:

  • Cargo.toml = manifest ของโปรเจกต์ (dependencies + metadata) คล้าย pyproject.toml + lockfile concept
  • src/main.rs = entry point ของโปรแกรม (คล้าย main.py)

ทิป: ถ้าตอนนี้คุณยังไม่อยาก build/run จริงทุกครั้ง ใช้ cargo check ได้ (เร็วกว่า build/run) — จะอธิบายเป็นระบบในบท workflow (06-tooling-and-workflow.md)

1.3 เพิ่ม dependencies (สำหรับ JSON/serde)

ถ้าคุณจะทำตัวอย่างที่เกี่ยวกับ JSON ให้เพิ่ม serde/serde_json ใน Cargo.toml:

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Output (example):

(no output — you updated Cargo.toml)

อ่านแบบ Python:

  • serde ≈ เครื่องมือ serialize/deserialize ให้ struct/enum (เหมือนคุณทำ dataclass + (de)serialize)
  • features = ["derive"] คือการ “เปิดความสามารถ” เพื่อใช้ #[derive(Deserialize, Serialize)] ได้

2) Hello World: Python ↔ Rust

Python:

print("Hello")

Output (example):

Hello

Rust:

fn main() {
    println!("Hello");
}

Output (example):

Hello

มุมมองคน Python:

  • fn main() คือจุดเริ่มของโปรแกรม (คล้าย if __name__ == "__main__": ... ในเชิงบทบาท)
  • println! มี ! เพราะมันเป็น macro (ไม่ใช่ฟังก์ชันธรรมดา)

ทำไม println! เป็น macro?

  • รองรับรูปแบบการ format หลายแบบ
  • ช่วยตรวจความสัมพันธ์ระหว่าง placeholder {} กับจำนวนค่าที่ส่งเข้าไปได้ตั้งแต่ compile-time

อ่านต่อแบบละเอียด: 18-rust-macros.md


3) ตัวแปรและชนิดข้อมูล (Variables & Types)

3.1 Rust “strict” ตรงไหน

Python:

x = 1  # ชนิดข้อมูลถูกตรวจ/ยืนยันตอน runtime

Rust:

let x = 1;      // compiler อนุมาน type ตอน compile-time
let x: i32 = 1; // ถ้าจะกำหนดชัด

จุดสำคัญสำหรับคนเริ่ม:

  • Rust ไม่ได้ “ยืดหยุ่นน้อย” เพื่อกวนใจ แต่เพื่อให้ระบบใหญ่แล้วไม่พังเงียบ ๆ
  • การมี type ที่ชัด ทำให้ refactor ได้มั่นใจขึ้นมาก (compiler เป็นเหมือน reviewer ที่เฝ้าอยู่ตลอด)

3.2 mutable / immutable

Python เปลี่ยนค่าได้ตลอด (ชื่อชี้ object เปลี่ยนได้ง่าย)

Rust default เป็น immutable (ต้องเขียน mut ถ้าตั้งใจให้เปลี่ยนค่า):

let x = 1;
// x = 2; // error

let mut y = 1;
y = 2; // ok

Output (example):

(no output — this snippet only assigns variables)

ทำไม Rust ทำแบบนี้?

  • เพราะ “state ที่เปลี่ยนได้” เป็นแหล่ง bug ใหญ่สุดในระบบจริง
  • Rust ทำให้จุดที่ state เปลี่ยนเป็นจุดที่คุณตั้งใจ (อ่านโค้ดแล้วไล่เหตุผลได้ง่าย)

ทิป practical:

  • เวลาอ่านโค้ด Rust ให้มอง mut เป็น “ป้ายเตือน” ว่าตรงนี้มี state change
  • ถ้าเจอ mut เยอะผิดปกติ ให้ถามว่า “กำลังออกแบบให้ข้อมูลถูกแก้หลายที่เกินไปไหม”

4) Ownership / Borrowing (หัวใจของ Rust)

นี่คือจุดที่คนมาจาก Python มักสะดุด แต่ถ้าจับหลักได้ Rust จะลื่นมาก เพราะ compiler จะกันบั๊กหนัก ๆ ให้ตั้งแต่แรก

คำอธิบายสั้นแบบจับต้องได้:

  • ใน Rust “ข้อมูลก้อนหนึ่ง” ต้องมีคนรับผิดชอบเรื่องอายุการใช้งาน (ใคร drop)
  • การยืมคือการขอสิทธิ์ใช้ข้อมูลช่วงสั้น ๆ แบบที่ปลอดภัย (ยืมอ่าน/ยืมแก้)

4.1 Ownership แบบง่าย

  • ค่าหนึ่งมี owner เดียว
  • เมื่อ assign/ส่งค่าไปฟังก์ชัน บาง type จะเกิด move (ย้ายความเป็นเจ้าของ)

ตัวอย่าง:

fn main() {
    let s1 = String::from("hi");
    let s2 = s1; // move

    // println!("{}", s1); // error: s1 ถูกย้ายไปแล้ว
    println!("{}", s2);
}

Output (example):

hi

อ่านแบบ Python:

  • Python: s2 = s1 คือให้ชื่อใหม่ชี้ object เดิม (alias)
  • Rust: บาง type ไม่ยอมให้ “เผลอแชร์ความเป็นเจ้าของ” เพราะมันกัน bug ระดับหน่วยความจำ (เช่น double-free)

4.2 Borrowing (ยืม) เพื่อไม่ให้ move

  • &T = ยืมอ่าน
  • &mut T = ยืมแก้ (มีได้ทีละหนึ่งในช่วงเดียวกัน)

Python (แนวคิดคล้ายกัน):

def len_of(s: str) -> int:
    return len(s)


def main() -> None:
    s = "hello"
    n = len_of(s)
    print(s, n)


main()

Output (example):

hello 5

Rust:

fn len_of(s: &str) -> usize {
    s.len()
}

fn main() {
    let s = String::from("hello");
    let n = len_of(&s);
    println!("{} {}", s, n);
}

Output (example):

hello 5

สรุป mindset:

  • Python: ส่ง object ไปมาแล้วแก้ได้ง่าย แต่บางทีแก้ “ไกลเกินที่ตั้งใจ”
  • Rust: ต้องชัดว่า “จุดไหนอ่าน” และ “จุดไหนแก้” เพื่อให้ความปลอดภัยชัดตั้งแต่ต้น

อ่านลึกขึ้น (บทที่สำคัญที่สุดของชุดนี้): 02-ownership-and-borrowing.md


5) Option และ Result: แทน None และ try/except

แก่นของแนวคิดนี้คือ “ทำให้เคสไม่มีค่า/เคสพังอยู่ใน type” เพื่อให้คนอ่าน + compiler เห็นตั้งแต่แรก

Python bug → Rust blocks: fallback กลืน error (bug เงียบ)

Python ที่เจอบ่อยมาก (เริ่มง่าย ๆ แล้วค่อย ๆ กลายเป็น bug):

  • parse พัง → คืนค่า default → โปรแกรมยังรันต่อเหมือนไม่มีอะไรเกิดขึ้น

ตัวอย่าง (Python):

def parse_port(text: str) -> int:
    try:
        return int(text)
    except ValueError:
        # BUG: กลืน error แล้ว “ทำให้เหมือนสำเร็จ”
        return 0

port = parse_port("not-a-number")
print(port)  # 0 (แต่คุณอาจไม่รู้ว่า parse พัง)

ใน Rust คุณมักเขียนให้ “ความเป็นไปได้ที่พัง” อยู่ใน type ตั้งแต่แรก:

fn parse_port(text: &str) -> Result<u16, std::num::ParseIntError> {
    text.parse::<u16>()
}

ผลที่ได้ (สำคัญมาก):

  • คนเรียก ต้อง เลือกว่า “จะ handle error ยังไง” (เช่น return ต่อ / log / fallback แบบตั้งใจ)
  • bug แบบ “ค่าผิดแต่ไหลต่อเงียบ ๆ” ลดลงเยอะ

5.1 Option<T>

Option<T> คือการบอกอย่างตรงไปตรงมาว่า “ค่านี้อาจไม่มี”

  • ข้อดีคือคนอ่าน/IDE/compiler จะเห็นตั้งแต่แรกว่ามีเคส missing value ต้อง handle

Python:

x = None

Output (example):

(no output — assignment only)

Rust:

let x: Option<i32> = None;
let y: Option<i32> = Some(3);

Output (example):

(no output — declarations only)

ใช้ match เพื่อจัดการ:

match y {
    Some(v) => println!("v={}", v),
    None => println!("no value"),
}

Output (example):

v=3

5.2 Result<T, E>

Result<T, E> คือการบอกว่า “การทำงานนี้สำเร็จหรือพัง”

  • Ok(T) = สำเร็จ
  • Err(E) = พัง พร้อมข้อมูล error

Python:

def parse_num(text: str) -> int:
    try:
        return int(text)
    except ValueError:
        return 0

Output (example):

(no output — function definition only)

Rust:

fn parse_num(text: &str) -> Result<i32, std::num::ParseIntError> {
    text.parse::<i32>()
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let n = parse_num("42")?;
    println!("{}", n);
    Ok(())
}

Output (example):

42

อ่านให้ลึกขึ้น:

  • ? คือ “ถ้า Err ให้ return ออกไปทันที” (early return)
  • ในโปรเจกต์จริง main มักเป็นจุดตัดสินใจว่า “log ยังไง / exit code เท่าไร” แทนการกลืน error

อ่านต่อแบบเป็นระบบ: 03-error-handling.md


6) enum + match: แทน string actions

ปัญหาคลาสสิกใน Python: ใช้ action เป็น string แล้วเขียน if/elif ยาว ๆ ซึ่งโตขึ้นแล้วพลาดเคสได้ง่าย

Rust แนะนำ:

  • แปลง string → enum Action
  • ใช้ match เพื่อบังคับให้ handle ครบทุกกรณี (ถ้าลืม compiler จะเตือน)

ตัวอย่างแนวคิด (ใช้เป็น placeholder ของ subcommand):

enum Action {
    Status,
    Version,
}

fn parse_action(s: &str) -> Option<Action> {
    match s {
        "status" => Some(Action::Status),
        "version" => Some(Action::Version),
        _ => None,
    }
}

Output (example):

(no output — this snippet defines types/functions)

แนวคิดที่อยากให้เห็น:

  • สตริงพิมพ์ผิด = runtime bug (และบางทีหลุดไป production)
  • enum + match ทำให้ “คำสั่งที่อนุญาต” ถูก encode อยู่ใน type (อ่านง่ายและ refactor ง่าย)

7) Config จาก JSON: json.load(...)serde + serde_json

Python มักเริ่มจาก dict แล้วค่อยไล่ validate key ทีหลัง ซึ่งใช้ได้ แต่เมื่อ schema โตขึ้นจะเริ่มหลุดง่าย

แนวทางแบบ Rust (ที่ช่วยคุม bug ได้ดี):

  • ทำ struct EndpointConfig ให้ schema ชัด
  • อ่านไฟล์ → parse JSON → ได้ค่า typed
  • ถ้า schema ไม่ตรง: deserialize fail แบบ explicit (fail fast)

Skeleton:

use serde::Deserialize;

#[derive(Deserialize)]
struct EndpointConfig {
    addrlists: Vec<String>,
    // เติม field เพิ่มตามไฟล์จริง
}

fn load_endpoint_config(path: &str) -> Result<EndpointConfig, Box<dyn std::error::Error>> {
    let text = std::fs::read_to_string(path)?;
    Ok(serde_json::from_str(&text)?)
}

Output (example):

(no output — this snippet only defines types/functions)

จุดที่ควรจับ:

  • “พิมพ์ key ผิด” หรือ “type ไม่ตรง” ควร fail ให้เร็ว ไม่ใช่ default เงียบ ๆ
  • ยิ่งแปลงเป็น typed struct เร็วเท่าไร ขอบเขตของ bug จะเล็กลงเท่านั้น

อ่านต่อแบบละเอียด: 09-config-and-serde-json.md


8) เทคนิคที่ควรรู้เพิ่ม (Rust tips สำหรับคนเขียน Python)

  • ทำให้คอมไพล์ผ่านก่อน แล้วค่อยปรับ: compiler message คือ feedback ที่ดีที่สุดช่วงแรก
  • หลีกเลี่ยง global mutable ถ้าไม่จำเป็น: ใช้ struct AppState แล้วส่งผ่าน function parameters
  • ทำ workflow ให้เป็นนิสัย: cargo checkcargo testcargo fmtcargo clippy

(ดูบท workflow แบบรวมใน 06-tooling-and-workflow.md)


9) แบบฝึกหัด (Practice)

  1. เขียน enum Action + parse_action() ให้รองรับ action เพิ่มอีก 2 ตัว
  2. สร้าง struct AppState ที่มี field อย่างน้อย 2 อัน (เช่น messages_count: u64, last_error: Option<String>)
  3. ทำ JSON string สั้น ๆ แล้วลอง deserialize เป็น struct (เริ่มจาก field เดียวก่อน)

10) References