รูปแบบตัวสะสม
ส่งต่อสถานะผ่านการเรียกซ้ำ
รูปแบบตัวสะสม เป็นบทเรียน Scala for Backend Engineering & Functional Programming ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Scala for Backend Engineering & Functional Programming และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Scala for Backend Engineering & Functional Programming มีบทเรียนทั้งหมด 4 บทเรียน
เหตุผลที่ต้องใช้ตัวสะสม
การเรียกซ้ำแบบทั่วไปจะสร้างผลลัพธ์ขณะย้อนกลับขึ้นมาตามสแตกการเรียก หลังจากการเรียกซ้ำส่งค่ากลับมาแล้ว
ตัวสะสมจะเก็บผลลัพธ์ที่คำนวณอยู่และส่งต่อเข้าไปในการเรียกแต่ละครั้งแทน ทำให้ได้คำตอบทันทีเมื่อถึงกรณีฐาน
การปรับเปลี่ยนเล็กน้อยนี้เปิดทางให้ใช้การเรียกซ้ำแบบหางและใช้สแตกในปริมาณคงที่
ฟังก์ชันช่วย
รูปแบบตัวสะสมใช้ฟังก์ชันช่วยภายในที่รับพารามิเตอร์เพิ่มเติม ซึ่งก็คือผลลัพธ์ที่สะสมได้จนถึงขณะนั้น
ฟังก์ชันภายนอกเพียงเริ่มการทำงานด้วยค่าเริ่มต้น ซึ่งมักเป็น 0 หรือลิสต์ว่าง
def sum(xs: List[Int]): Int = {
def loop(rest: List[Int], acc: Int): Int = rest match {
case Nil => acc
case h :: t => loop(t, acc + h)
}
loop(xs, 0)
}การทำงานของตัวสะสม
ในที่นี้ โปรแกรมฉบับเต็มจะหาผลรวมของลิสต์โดยใช้ตัวสะสม
สังเกตว่ากรณีฐานคืนค่า acc โดยตรง ไม่ใช่ 0 เพราะผลรวมถูกสะสมขึ้นระหว่างที่เราเคลื่อนลงมาตามลิสต์
def sum(xs: List[Int]): Int = {
def loop(rest: List[Int], acc: Int): Int = rest match {
case Nil => acc
case h :: t => loop(t, acc + h)
}
loop(xs, 0)
}
@main def run(): Unit =
println(sum(List(1, 2, 3, 4))) // 10เปรียบเทียบโครงสร้างทั้งสองแบบ
ในการเรียกซ้ำแบบทั่วไป ขั้นตอนการรวมค่า (h + ...) ต้องรอการเรียกด้านใน
ในแบบใช้ตัวสะสม การรวมค่าเกิดขึ้นก่อนการเรียก และการเรียกเป็นสิ่งสุดท้ายที่ฟังก์ชันทำ
คุณสมบัติที่การเรียกเป็นสิ่งสุดท้ายนี้เองที่ทำให้เป็นการเรียกซ้ำแบบหาง
// Plain: combine after the call
case h :: t => h + sum(t)
// Accumulator: combine before the call
case h :: t => loop(t, acc + h)การเรียกซ้ำแบบหาง
การเรียกซ้ำแบบหางคือการเรียกที่การเรียกซ้ำเป็นการกระทำสุดท้ายของฟังก์ชัน โดยไม่มีงานใดต้องทำต่อหลังจากนั้น
Scala สามารถปรับรูปแบบนี้ให้ทำงานเป็นลูป โดยนำเฟรมสแตกเดียวกลับมาใช้ซ้ำ จึงไม่ทำให้สแตกเต็มไม่ว่าจะเรียกลึกเพียงใด
import scala.annotation.tailrec
@tailrec
def countDown(n: Int): Unit =
if (n < 0) ()
else { println(n); countDown(n - 1) }คำอธิบายประกอบ @tailrec
การเพิ่ม @tailrec จะขอให้คอมไพเลอร์ตรวจสอบว่าฟังก์ชันเป็นการเรียกซ้ำแบบหางจริง
หากไม่ใช่ การคอมไพล์จะล้มเหลวพร้อมข้อผิดพลาดที่ชัดเจน วิธีนี้เปลี่ยนกับดักด้านประสิทธิภาพที่อาจไม่แสดงอาการ ให้กลายเป็นการรับประกันตั้งแต่เวลาสร้างโปรแกรม
import scala.annotation.tailrec
def sum(xs: List[Int]): Int = {
@tailrec
def loop(rest: List[Int], acc: Int): Int = rest match {
case Nil => acc
case h :: t => loop(t, acc + h)
}
loop(xs, 0)
}
@main def run(): Unit = println(sum((1 to 100000).toList))การสะสมเป็นลิสต์
ตัวสะสมไม่จำเป็นต้องเก็บตัวเลขเท่านั้น แต่สามารถใช้สร้างคอลเลกชันได้เช่นกัน
ฟังก์ชัน reverse นี้จะนำส่วนหัวแต่ละตัวไปเติมด้านหน้าของตัวสะสม ทำให้ลำดับกลับด้านโดยธรรมชาติ การเติมด้านหน้าด้วย :: ทำได้รวดเร็ว จึงมีประสิทธิภาพ
def reverse[A](xs: List[A]): List[A] = {
def loop(rest: List[A], acc: List[A]): List[A] = rest match {
case Nil => acc
case h :: t => loop(t, h :: acc)
}
loop(xs, Nil)
}การทำงานของ reverse
ตัวสะสมเริ่มต้นเป็นค่าว่างและขยายขึ้นเมื่อเราใช้ข้อมูลอินพุตไปเรื่อย ๆ
เนื่องจากส่วนหัวแต่ละตัวถูกผลักไปไว้ด้านหน้าของ acc สมาชิกตัวแรกจึงไปอยู่ท้ายสุด ทำให้ได้ลิสต์ที่กลับลำดับโดยใช้พื้นที่สแตกคงที่
def reverse[A](xs: List[A]): List[A] = {
def loop(rest: List[A], acc: List[A]): List[A] = rest match {
case Nil => acc
case h :: t => loop(t, h :: acc)
}
loop(xs, Nil)
}
@main def run(): Unit =
println(reverse(List(1, 2, 3))) // List(3, 2, 1)ตัวสะสมหลายตัว
ฟังก์ชันช่วยสามารถเก็บตัวสะสมหลายตัวพร้อมกันได้
ในที่นี้ เราติดตามผลคูณสะสมและจำนวนสมาชิกในลูปเดียวกัน แล้วคืนค่าทั้งสองเป็นทูเพิล
ตัวสะสมแต่ละตัวจะส่งค่าที่อัปเดตแล้วต่อไปยังการเรียกครั้งถัดไป
def stats(xs: List[Int]): (Int, Int) = {
def loop(rest: List[Int], prod: Int, count: Int): (Int, Int) =
rest match {
case Nil => (prod, count)
case h :: t => loop(t, prod * h, count + 1)
}
loop(xs, 1, 0)
}การเลือกค่าเริ่มต้น
ตัวสะสมเริ่มต้นต้องเป็นเอกลักษณ์ของการดำเนินการที่คุณใช้
สำหรับการบวกให้ใช้ 0 สำหรับการคูณให้ใช้ 1 สำหรับการสร้างลิสต์ให้ใช้ลิสต์ว่าง และสำหรับการเชื่อมสตริงให้ใช้สตริงว่าง
ค่าเริ่มต้นที่ไม่ถูกต้องจะทำให้ได้คำตอบผิดโดยไม่แสดงอาการชัดเจน
// addition -> seed 0
// product -> seed 1
// list -> seed Nil
// string -> seed ""ลำดับของผลลัพธ์
การเรียกซ้ำด้วยตัวสะสมจะประมวลผลสมาชิกจากซ้ายไปขวา แต่ตัวสะสมที่เติมสมาชิกด้านหน้าจะกลับลำดับสมาชิก
หากต้องการรักษาลำดับขณะสร้างลิสต์ ให้กลับลิสต์เมื่อจบ หรือใช้การเติมด้านท้าย แม้การเติมด้านท้ายจะช้ากว่า โดยทั่วไปนิยมเติมด้านหน้าแล้วกลับลิสต์
def mapInc(xs: List[Int]): List[Int] = {
def loop(rest: List[Int], acc: List[Int]): List[Int] = rest match {
case Nil => acc.reverse
case h :: t => loop(t, (h + 1) :: acc)
}
loop(xs, Nil)
}ตรวจสอบความเข้าใจ
เลือกข้อความที่ถูกต้องเกี่ยวกับการเรียกซ้ำด้วยตัวสะสม
สรุปทบทวน
ตัวสะสมจะส่งผลลัพธ์ที่กำลังคำนวณผ่านการเรียกซ้ำแต่ละครั้งลงไป ทำให้กรณีฐานสามารถคืนค่าผลลัพธ์นั้นได้โดยตรง
วิธีนี้ทำให้การเรียกซ้ำอยู่ในตำแหน่งหาง จึงเปิดให้ Scala ปรับการเรียกฟังก์ชันครั้งสุดท้ายให้มีประสิทธิภาพ และใช้การตรวจสอบความปลอดภัยด้วย @tailrec
กำหนดค่าเริ่มต้นของตัวสะสมให้เป็นค่าเอกลักษณ์ของการดำเนินการ และกลับลำดับเมื่อจบหากลำดับมีความสำคัญ
คำถามที่พบบ่อย
บทเรียน “รูปแบบตัวสะสม” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “รูปแบบตัวสะสม” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Scala for Backend Engineering & Functional Programming ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Scala for Backend Engineering & Functional Programming มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “รูปแบบตัวสะสม”
ส่งต่อสถานะผ่านการเรียกซ้ำ คุณปฏิบัติ Scala for Backend Engineering & Functional Programming ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Scala for Backend Engineering & Functional Programming หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Scala for Backend Engineering & Functional Programming บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “รูปแบบตัวสะสม” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Scala for Backend Engineering & Functional Programming นี้ได้ไหม
ได้ บทเรียน Scala for Backend Engineering & Functional Programming ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ