ปูต้นฉบับสีส้มแดงกำลังเลื่อนลูกปัดสะสมบนรางข้อมูลสีมิ้นต์ทีละชิ้น

Rust Iterator::scan: พก state ไปทีละรายการโดยไม่หลุดจาก pipeline

พี่โชว์Engineering

บาง pipeline ต้องจำค่าจากสมาชิกก่อนหน้า เช่น ยอดสะสม สถานะ parser หรืออุณหภูมิล่าสุด Iterator::scan ให้ closure ถือ state แล้วปล่อยผลใหม่ออกมาทีละรายการ โดยยังต่อกับ filter, map และ collect ได้ครับ

WHY: state อยู่ในขอบเขตที่มองเห็น

ลองนึกถึงแม่ค้าจดเลขยอดรวมหลังรับออร์เดอร์แต่ละโต๊ะ สมุดยอดรวมคือ state ส่วนตัวเลขที่ประกาศหลังแต่ละออร์เดอร์คือ item ที่ iterator ปล่อยออกมา scan ทำให้สองอย่างนี้อยู่ใน closure เดียว ไม่ต้องสร้างตัวแปร mutable ไว้นอก pipeline

fn running_balances(deltas: &[i32]) -> Vec<i32> {
    deltas
        .iter()
        .copied()
        .scan(0, |balance, delta| {
            *balance += delta;
            Some(*balance)
        })
        .collect()
}

fn main() {
    assert_eq!(running_balances(&[100, -30, 20]), vec![100, 70, 90]);
    assert!(running_balances(&[]).is_empty());
}

#[test]
fn keeps_state_between_items() {
    assert_eq!(running_balances(&[5, 5, -3]), vec![5, 10, 7]);
}

Closure รับ &mut St จึงแก้ state ได้โดยไม่ย้ายมันออก ทุกครั้งต้องคืน Some(item) เพื่อเดินต่อ หรือ None เพื่อบอก iterator ว่าไม่มี item รอบนั้น อย่างไรก็ตามเอกสารเตือนว่าพฤติกรรมหลัง closure คืน None ไม่ควรถูกใช้เป็นสัญญาหยุดถาวรแบบกำกวม หากต้องการหยุดตามเงื่อนไขชัด ๆ ให้ต่อ map_while/take_while หรือเขียน loop ที่สื่อเจตนาดีกว่า

เมื่อไร loop อ่านง่ายกว่า

scan เหมาะเมื่อหนึ่ง input สร้างได้ศูนย์หรือหนึ่ง output และ state มีขนาดเล็ก ถ้าต้องรายงาน error หลายชนิด, ส่งหลาย output ต่อ input, rollback state หรือ break หลายชั้น for loop กับ enum ผลลัพธ์มักดูแลง่ายกว่า ความกระชับไม่ควรแลกกับ state machine ที่ซ่อนจนคนรีวิวต้องแกะครับ

Recap

  • ใช้ scan เมื่อ pipeline ต้องมี running state
  • เก็บ state ให้เล็กและความหมายเดียว
  • อย่าพึ่งพาพฤติกรรมหลังคืน None แบบคลุมเครือ
  • เปลี่ยนเป็น loop เมื่อ error/rollback สำคัญกว่า chaining