กระรอกดินต้นฉบับกำลังส่งเมล็ดจากสายพานทีละเมล็ดลงตะกร้าที่มีของอยู่แล้ว

Go slices.AppendSeq: เติม iterator ลง slice โดยไม่ต้องเขียนลูปเอง

พี่โชว์Engineering

เมื่อ pipeline คืนค่าเป็น iter.Seq แต่ปลายทางต้องเก็บต่อจาก slice เดิม เรามักเขียน for range แล้ว append เองครับ slices.AppendSeq รวมงานนั้นเป็นคำสั่งเดียว โดยยังรักษาลำดับของค่าจาก iterator และใช้ capacity เดิมได้เมื่อพอ

WHY: เติมของลงตะกร้าเดิม ไม่ต้องย้ายตะกร้าทุกใบ

นึกถึงสายพานที่ส่งของทีละชิ้นเข้าตะกร้าที่มีของอยู่แล้ว เราไม่จำเป็นต้องเทของจากสายพานเป็นกองชั่วคราวก่อน AppendSeq เหมาะเมื่อ boundary ถัดไปต้องการ slice จริง แต่ถ้าผู้บริโภคอ่านทีละค่าและหยุดเร็วได้ การเก็บทั้งหมดก็ยังสิ้นเปลืองโดยไม่จำเป็น

package main

import (
	"iter"
	"slices"
	"testing"
)

func positives(values []int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for _, value := range values {
			if value > 0 && !yield(value) {
				return
			}
		}
	}
}

func TestAppendSeq(t *testing.T) {
	base := make([]int, 1, 4)
	base[0] = 9
	got := slices.AppendSeq(base, positives([]int{-1, 2, 3}))
	if !slices.Equal(got, []int{9, 2, 3}) {
		t.Fatalf("unexpected result: %#v", got)
	}
	if !slices.Equal(slices.AppendSeq([]int{}, positives(nil)), []int{}) {
		t.Fatal("empty sequence must not add values")
	}
}

ขอบเขตที่ควรตัดสินใจให้ชัด

AppendSeq consume iterator จนจบ จึงไม่เหมาะกับ sequence ไม่สิ้นสุด และไม่ได้เพิ่ม validation ให้ข้อมูล หากค่าที่เข้ามาถือ pointer หรือ slice ย่อย ownership ยังเป็นของเดิม การ copy ตัว slice ชั้นนอกไม่ได้ clone object ข้างใน

ถ้ารู้จำนวนล่วงหน้าให้ reserve capacity เพื่อเลี่ยงการย้าย backing array แต่ไม่ควรเดาขนาดสูงเกินจริง ส่วน iterator ใน Go โดยทั่วไปควรมองว่าใช้ครั้งเดียว หากต้องวนซ้ำให้สร้างใหม่จาก source ที่ทำซ้ำได้

Recap

  • ใช้ AppendSeq เมื่อปลายทางต้องการ slice จริง
  • อ่านตรงจาก iterator หากหยุดเร็วหรือข้อมูลใหญ่มาก
  • reserve capacity เมื่อรู้ขนาดอย่างมีหลักฐาน
  • กำหนด ownership ของ element ที่เป็น reference แยกจาก slice