Closures & Iterators
TypeScript arrow functions vs Rust closures
หัวข้อที่มีชื่อว่า “TypeScript arrow functions vs Rust closures”ใน TypeScript arrow functions สามารถจับตัวแปรจาก enclosing scope ได้อย่างอิสระ Rust closures ทำสิ่งเดียวกัน แต่ compiler ติดตามอย่างชัดเจนว่า closure ใช้ค่าที่จับไว้อย่างไร และบังคับ ownership rules ตอน compile time
// TypeScript — arrow function captures 'factor' by referenceconst factor = 3;const triple = (x: number) => x * factor;console.log(triple(5)); // 15
// Closures in higher-order functionsconst numbers = [1, 2, 3, 4, 5];const doubled = numbers.map(x => x * 2);console.log(doubled); // [2, 4, 6, 8, 10]
// Capturing mutable state — counterlet count = 0;const increment = () => { count++; };increment();increment();console.log(count); // 2fn main() { // Closure captures 'factor' by reference (immutable) let factor = 3; let triple = |x| x * factor; println!("{}", triple(5)); // 15
// Iterator adapters: map, filter, collect let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect(); println!("{:?}", doubled); // [2, 4, 6, 8, 10]
// FnMut closure — captures count mutably let mut count = 0; let mut increment = || { count += 1; }; increment(); increment(); drop(increment); // release borrow so we can use count again println!("{count}"); // 2}ลำดับชั้นของ Fn traits
หัวข้อที่มีชื่อว่า “ลำดับชั้นของ Fn traits”Rust จำลองความสามารถของ closure เป็นสาม traits การเข้าใจสิ่งเหล่านี้มีความสำคัญเมื่อคุณส่ง closures ไปยังฟังก์ชันหรือเก็บไว้ใน structs
| Trait | ทำอะไรได้ | คู่เทียบ TypeScript |
|---|---|---|
FnOnce | เรียกได้ครั้งเดียว; สามารถ move ค่าที่จับไว้ออกได้ | One-shot callback ที่ consume ค่าที่จับไว้ |
FnMut | เรียกได้หลายครั้ง; เปลี่ยนแปลงค่าที่จับไว้ | Stateful callback ที่แก้ไขตัวแปรที่ close over |
Fn | เรียกได้หลายครั้ง; อ่านค่าที่จับไว้เท่านั้น (หรือไม่จับเลย) | Pure arrow function ไม่มี side effects บนตัวแปรที่จับ |
ทุก Fn เป็น FnMut ด้วย; ทุก FnMut เป็น FnOnce ด้วย เมื่อคุณรับ parameter &dyn Fn(T) -> U คุณยอมรับ requirement ที่เข้มงวดที่สุด — closure ต้องเรียกได้หลายครั้งโดยไม่ mutate
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(f(x))}
fn main() { let double = |x| x * 2; println!("{}", apply_twice(double, 3)); // 12}Async closure
หัวข้อที่มีชื่อว่า “Async closure”ใน TypeScript async () => { ... } เป็นแค่ function ที่ return promise syntax ไม่ได้เปลี่ยนอะไรเลย ส่วนฝั่ง Rust เรื่องนี้เคยงุ่มง่ามอยู่หลายปี: closure ที่ return async { ... } จะได้ future ที่ borrow จาก capture ของ closure ไม่ได้ อะไรก็ตามที่ future แตะต้องจึงต้อง clone หรือ move ออกมาก่อน
Rust 1.85 เพิ่ม async closure ของจริง — async || { ... } — พร้อม trait AsyncFn, AsyncFnMut และ AsyncFnOnce ที่ล้อกับ Fn ทั้งสามตัวข้างบน future ที่มัน return borrow จาก capture ได้ นี่คือสิ่งที่ทำให้ code แบบธรรมชาติ compile ผ่าน
// TypeScript: async arrow functionconst runTwice = async (f: (url: string) => Promise<number>) => (await f("https://a.example")) + (await f("https://bb.example"));
const log: string[] = [];const total = await runTwice(async (url) => { const n = await fetchLen(url); log.push(`${url} -> ${n}`); // closes over 'log' freely return n;});// Rust: async closure, bound with AsyncFnMutasync fn run_twice<F>(mut f: F) -> usizewhere F: AsyncFnMut(&str) -> usize,{ f("https://a.example").await + f("https://bb.example").await}
let mut log: Vec<String> = Vec::new();let total = run_twice(async |url: &str| { let n = fetch_len(url).await; log.push(format!("{url} -> {n}")); // borrows 'log' across the await n}).await;เลือก bound ให้ตรงกับวิธีที่ closure ถูกใช้ เหมือนกับ Fn/FnMut/FnOnce เป๊ะ: AsyncFn ถ้าอ่าน capture อย่างเดียว, AsyncFnMut ถ้าแก้ไข capture (ตัวอย่างนี้ push ลง log), AsyncFnOnce ถ้าใช้ capture จนหมด
// @skip-verify crate: tokioasync fn fetch_len(url: &str) -> usize { url.len() // pretend this awaits the network}
// Takes an async closure: AsyncFnMut can mutate what it captures.async fn run_twice<F>(mut f: F) -> usizewhere F: AsyncFnMut(&str) -> usize,{ f("https://a.example").await + f("https://bb.example").await}
#[tokio::main]async fn main() { let mut log: Vec<String> = Vec::new();
let total = run_twice(async |url: &str| { let n = fetch_len(url).await; log.push(format!("{url} -> {n}")); n }) .await;
println!("total = {total}"); for line in &log { println!("{line}"); }}Compiling…
Iterator adapters — lazy โดย default
หัวข้อที่มีชื่อว่า “Iterator adapters — lazy โดย default”.map(), .filter(), และ .reduce() ของ TypeScript eager: จัดสรร array ใหม่ในแต่ละขั้น Iterator adapters ของ Rust lazy: ไม่มีการคำนวณจนกว่าคุณจะเรียก consuming method เช่น .collect(), .sum(), หรือ .for_each() หมายความว่า chain ยาวของ .map().filter().take() ไม่จัดสรร vectors ระหว่างกลางเลย
// TypeScript — each step allocates a new arrayconst result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] .filter(x => x % 2 === 0) // new array [2,4,6,8,10] .map(x => x * x) // new array [4,16,36,64,100] .slice(0, 3); // new array [4,16,36]console.log(result);fn main() { // Rust — no intermediate allocations until collect() let result: Vec<i32> = (1..=10) .filter(|x| x % 2 == 0) // lazy .map(|x| x * x) // lazy .take(3) // lazy .collect(); // ONE allocation here println!("{:?}", result); // [4, 16, 36]
// Common adapters let sum: i32 = (1..=100).sum(); println!("sum 1..100 = {sum}");
let words = vec!["hello", "world", "rust"]; let upper: Vec<String> = words.iter() .map(|w| w.to_uppercase()) .collect(); println!("{:?}", upper);
// flat_map (like Array.flatMap) let sentences = vec!["hello world", "foo bar"]; let all_words: Vec<&str> = sentences.iter() .flat_map(|s| s.split_whitespace()) .collect(); println!("{:?}", all_words);}ลองเล่นดู
หัวข้อที่มีชื่อว่า “ลองเล่นดู”fn main() { let multiplier = 3; let triple = |x| x * multiplier; println!("triple(5) = {}", triple(5));
let numbers = vec![1, 2, 3, 4, 5, 6]; let evens_doubled: Vec<i32> = numbers .iter() .filter(|&&x| x % 2 == 0) .map(|&x| x * 2) .collect(); println!("evens_doubled = {:?}", evens_doubled);
let sum: i32 = numbers.iter().sum(); println!("sum = {sum}");
let result: Vec<String> = (1..=3) .map(|x| x * x) .map(|x| format!("sq:{x}")) .collect(); println!("{:?}", result);}Compiling…