0Pricing
Scala for Backend Engineering & Functional Programming · บทเรียน

รูปแบบตัวสะสม

ส่งต่อสถานะผ่านการเรียกซ้ำ

รูปแบบตัวสะสม เป็นบทเรียน 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. คิดแบบเรียกซ้ำ
  2. รูปแบบตัวสะสม
  3. foldLeft และ foldRight
  4. reduce และ aggregate
← กลับไปที่ Scala for Backend Engineering & Functional Programming