ปูสีส้มแดงต้นฉบับกำลังวางก้อนกรวดสีเดียวกันเป็นช่วงติดกันบนโต๊ะทำงานสีมิ้นต์

Rust slice::chunk_by: แบ่งข้อมูลติดกันตามกติกาโดยไม่ copy

พี่โชว์Engineering

ข้อมูลจาก sensor หรือ event log มักมาเป็น “ช่วง” เช่นสถานะปกติติดต่อกันหลายรายการ แล้วเปลี่ยนเป็นเตือน ถ้าเราอยากแบ่งช่วงโดยรักษาลำดับเดิม การสร้าง Vec<Vec<T>> ใหม่ตั้งแต่ต้นเปลืองทั้ง allocation และ copy ครับ slice::chunk_by ให้ iterator ของ subslice ที่ยืมข้อมูลเดิม โดย predicate ตอบว่าคู่ที่อยู่ติดกันควรอยู่กลุ่มเดียวกันหรือไม่

WHY: เกณฑ์อยู่ระหว่างสมาชิก ไม่ได้อยู่ที่สมาชิกตัวเดียว

ความต่างจาก split คือ split หา “ตัวคั่น” แล้วตัดมันทิ้ง แต่ chunk_by เก็บสมาชิกทุกตัวและถามความสัมพันธ์ของเพื่อนบ้าน เหมือนแบ่งขบวนรถไฟเป็นตอนเมื่อสีตู้เปลี่ยน ไม่มีตู้ไหนหายไป

ตัวอย่างแบ่งจำนวนเต็มเป็นช่วงติดกันตามเครื่องหมาย ศูนย์เป็นกลุ่มของตัวเองเพื่อให้กติกาชัดเจน:

fn runs_by_sign(values: &[i32]) -> Vec<&[i32]> {
    values
        .chunk_by(|left, right| left.signum() == right.signum())
        .collect()
}

fn main() {
    let values = [-3, -1, 0, 2, 8, -4];
    let runs = runs_by_sign(&values);
    assert_eq!(runs, vec![&[-3, -1][..], &[0][..], &[2, 8][..], &[-4][..]]);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn keeps_every_element_in_order() {
        let values = [-3, -1, 0, 2, 8, -4];
        assert_eq!(
            runs_by_sign(&values),
            vec![&[-3, -1][..], &[0][..], &[2, 8][..], &[-4][..]]
        );
    }

    #[test]
    fn empty_input_has_no_chunks() {
        assert!(runs_by_sign(&[]).is_empty());
    }
}

Trade-off ที่ต้องเห็น

เวลาเป็น O(n) เพราะดูแต่ละรอยต่อครั้งเดียว ส่วน iterator และ subslice ไม่ copy ข้อมูล; Vec ในตัวอย่างมีไว้เก็บ reference เพื่อทดสอบเท่านั้น ถ้า consumer ประมวลผลทีละช่วงก็ใช้หน่วยความจำเสริม O(1) ได้ ข้อแลกเปลี่ยนคือผลลัพธ์ยืม slice ต้นฉบับ จึงเก็บนานกว่าข้อมูลต้นทางไม่ได้ และ predicate ที่มี state ซับซ้อนจะทำให้โค้ดอ่านยาก

ถ้าต้องจัดกลุ่มสมาชิกที่กระจายอยู่คนละตำแหน่ง ให้ใช้ map ตาม key แทน ถ้าต้องแบ่งทุก N ตัว ใช้ chunks หรือ chunks_exact และถ้าต้องแก้ข้อมูลในแต่ละช่วง ใช้ chunk_by_mut พร้อมระวัง borrow rule

Recap

  • ใช้เมื่อ “ความต่อเนื่อง” สำคัญกว่าการ group ทั้งชุด
  • นิยาม predicate ของสมาชิกติดกันให้รีวิวได้ง่าย
  • ทดสอบ input ว่าง จุดเปลี่ยนติดกัน และกลุ่มเดียวทั้ง slice
  • ประมวลผล iterator ตรง ๆ ถ้าไม่จำเป็นต้องเก็บทุก subslice