Scala และ Functional Programming
บทนี้ไม่ได้มีเป้าหมายให้ทุกคนกลายเป็น Scala programmer ภายในหนึ่งบท แต่ต้องการให้มองเห็นภาษาความคิดที่อยู่ใต้ Spark: แทนที่จะสั่งเครื่องทีละก้าว เราอธิบายการแปลงข้อมูลเป็นชุดของฟังก์ชันที่ประกอบกันได้ ระบบจึงมีพื้นที่มากขึ้นในการแบ่งงาน ย้ายงาน คำนวณใหม่ และปรับแผนให้เหมาะกับคลัสเตอร์
1Scala คืออะไร และทำไม Spark ถึงเลือกมัน
ชื่อ Scala มาจากแนวคิด scalable language คือภาษาเดียวควรใช้เขียนได้ตั้งแต่ expression สั้น ๆ ไปจนถึงระบบขนาดใหญ่ จุดเด่นไม่ใช่เพียง “เขียน Java ให้สั้นลง” แต่คือการทำให้ object และ function อยู่ร่วมกันได้อย่างเป็นธรรมชาติ เราจึงใช้ class, trait และ encapsulation เมื่อเหมาะ และใช้ immutable value, higher-order function และ pattern matching เมื่อปัญหาเป็นการแปลงข้อมูล
| คุณสมบัติ | ทำไมถึงสำคัญกับ Big Data |
|---|---|
| Object-Oriented + Functional | ทุกค่าคือ object ทุกฟังก์ชันคือ value — เขียนโค้ดแบบ pipeline การแปลงข้อมูล (transformation chain) ได้กระชับ ตรงกับรูปแบบการประมวลผลข้อมูลขนาดใหญ่ |
| Static Typing + Type Inference | ตรวจความไม่เข้ากันของ type ได้ก่อนส่งงานขึ้นคลัสเตอร์ โดยไม่ต้องเขียน type กำกับทุกตำแหน่ง แต่ compile-time checking ไม่ได้ป้องกันความผิดพลาดด้านข้อมูลหรือ logic ทั้งหมด |
| Immutability by convention | นิยมใช้ val และ immutable collection ช่วยลด shared mutable state แต่ Scala ยังอนุญาต var จึงไม่ได้ทำให้โปรแกรมปลอดภัยโดยอัตโนมัติ |
| Functions as values | ส่ง “สิ่งที่ต้องทำกับแต่ละ record” เข้าไปใน map, filter หรือ reduce ได้ Framework จึงควบคุมการกระจายงาน ส่วนผู้ใช้กำหนด logic |
| JVM ecosystem | ใช้ระบบจัดการหน่วยความจำ เครื่องมือ profiling และไลบรารีของ Java ที่พัฒนามานาน รวมถึงทำงานร่วมกับระบบ Hadoop ได้สะดวก |
| Pattern Matching และ Case Class | อธิบายโครงสร้าง record และแยกกรณีของข้อมูลได้ชัด เหมาะกับงาน parse และ transformation แต่ต้องจัดการกรณีข้อมูลไม่ครบอย่างระมัดระวัง |
ทำไม Spark ไม่ได้เลือก Scala เพียงเพราะโค้ดสั้น
ความกระชับมีประโยชน์ แต่เหตุผลที่ลึกกว่าคือ Scala เปิดให้ Spark สร้าง API ที่ดูเหมือน collection ธรรมดา ผู้ใช้เขียน data.map(...).filter(...) แต่ object ที่รับคำสั่งอาจเป็นข้อมูลกระจายหลายร้อย partition Framework เป็นผู้ตัดสินใจว่าฟังก์ชันจะรันที่ใด เมื่อใด และทำซ้ำอย่างไร
กล่าวอีกแบบหนึ่ง Scala ทำให้ “โปรแกรมที่ผู้ใช้เขียน” แยกออกจาก “กลไกที่ระบบใช้รัน” ได้ค่อนข้างสะอาด ผู้ใช้ส่ง closure เข้าไป ส่วน Spark เก็บ lineage สร้าง DAG และจัด task ลง executor นี่เป็นการแบ่งหน้าที่ที่ทำให้ API เดิมใช้ได้ทั้งข้อมูลเล็กใน local mode และข้อมูลใหญ่บนคลัสเตอร์ แม้ต้นทุนจริงจะแตกต่างกันมาก
Scala ไม่ใช่เงื่อนไขบังคับของการเรียน Spark
ปัจจุบันผู้ใช้จำนวนมากเข้าถึง Spark ผ่าน Python หรือ SQL และทำงานจริงได้ดี การเรียน Scala ในบทนี้จึงไม่ใช่การประกาศว่า “ทุกคนต้องเขียน Spark ด้วย Scala” แต่เป็นการใช้ภาษาที่แนวคิด Functional Programming ปรากฏชัด เพื่อมองกลไกใต้ API ให้ทะลุ เมื่อเข้าใจแล้ว แนวคิดเดียวกันย้ายไป PySpark, Java, SQL หรือระบบอื่นได้
Static Type ช่วยอะไร และช่วยไม่ได้ตรงไหน
ถ้าฟังก์ชันต้องการ Int แต่เราเผลอส่ง String compiler อาจตรวจพบก่อนรัน ลดโอกาสเสียเวลาจองคลัสเตอร์แล้วล้มเพราะชนิดข้อมูลไม่ตรง แต่ static type ไม่รู้ว่าอายุเป็น -300 สมเหตุสมผลหรือไม่ ไม่รู้ว่า key กระจายเอียง และไม่รู้ว่าไฟล์จริงมีบรรทัดเสีย ดังนั้น type safety เป็นตาข่ายชั้นหนึ่ง ไม่ใช่ใบรับรองว่าทั้ง pipeline ถูกต้อง
JVM Compatibility มีรายละเอียดที่ต้องตรงกัน
Scala library มี binary version อยู่ในชื่อ artifact เช่น spark-core_2.13 ส่วนท้าย _2.13 ไม่ใช่เลขตกแต่ง แต่บอกว่า library ถูกคอมไพล์สำหรับ Scala binary version ใด แอป Scala ควรใช้ version ที่เข้ากับ Spark distribution นั้น ไม่ควรเห็นคำว่า “Scala” เหมือนกันแล้วถือว่าใช้แทนกันได้ทุกเวอร์ชัน
2ทำไม Immutability ถึงสำคัญกับข้อมูลขนาดใหญ่
เวลาบอกว่า object เป็น immutable เราหมายถึงเมื่อสร้างแล้ว สถานะที่มองเห็นจาก object นั้นไม่ถูกแก้ทับ หากต้องการค่าใหม่ เราสร้าง object ใหม่หรือ collection ใหม่จากของเดิม แนวคิดนี้ดูเหมือนสิ้นเปลือง เพราะทำไมไม่แก้ช่องเดิมไปเลย? แต่ในระบบที่มีหลาย thread หลาย task และหลายสำเนาของข้อมูล ต้นทุนของการแก้ร่วมกันอาจแพงกว่าต้นทุนของการสร้างค่ารุ่นใหม่มาก
class Person(var name: String)
val p = new Person("John")
myStorage.store(p)
p.name = "Jane" // แก้ค่าเดิม
myStorage.store(p)
// ถ้า myStorage เก็บ reference ไว้
// ค่าที่เก็บก่อนหน้าอาจถูกเปลี่ยนตามไปด้วย!
case class Person(name: String)
val p = new Person("John")
val p2 = p.copy(name = "Jane")
// p ยังคงเป็น "John" เสมอ
// p2 เป็นค่ารุ่นใหม่ที่แยกจาก p
เปรียบเหมือนเอกสารฉบับลงนามแล้ว
ระบบกระจายไม่ได้ส่ง memory reference เดียวข้ามเครื่องเหมือนใน process เดียว แต่แนวคิดเรื่อง version ยังสำคัญมาก Dataset ที่ไม่ถูกแก้ทับช่วยให้ lineage อธิบายได้ว่า output รุ่นนี้เกิดจาก input และ transformation ใด เมื่อ partition หาย ระบบจึงสร้างใหม่จากสูตรเดิมได้
Immutability ลด Dependency แบบใด
คำว่า “ลด dependency” ไม่ได้แปลว่างานทุกชิ้นเป็นอิสระทันที แต่ลด write dependency ที่เกิดจากหลายงานต้องแก้ตำแหน่งเดียวกัน หากงาน A และ B อ่าน input เดียวกันโดยไม่เขียนทับ ทั้งคู่ทำพร้อมกันได้ง่ายกว่า แต่ถ้า A ต้องเขียนก่อน B อ่าน หรือทั้งคู่เขียนค่าเดียวกัน เราต้องกำหนดลำดับ ใช้ lock หรือแก้ conflict
| ความสัมพันธ์ | ตัวอย่าง | ผลต่อการทำงานพร้อมกัน |
|---|---|---|
| Read–Read | A และ B อ่าน dataset เดิม | มักทำพร้อมกันได้ เพราะไม่มีใครเปลี่ยนสิ่งที่อีกฝ่ายอ่าน |
| Write–Read | A แก้ค่า แล้ว B ต้องอ่านค่ารุ่นใหม่ | B ต้องรอ A หรือระบบต้องกำหนด version ที่ B ควรเห็น |
| Read–Write | A อ่านค่าเดิม ขณะที่ B จะเขียนทับ | ต้องป้องกันไม่ให้ A เห็นสถานะครึ่งเก่าครึ่งใหม่ |
| Write–Write | A และ B แก้ค่าเดียวกัน | ต้อง lock, serialize, merge หรือกำหนดผู้ชนะ |
Immutability เปลี่ยนปัญหาจาก “ใครมีสิทธิ์เขียนช่องนี้ตอนนี้” เป็น “ค่ารุ่นใหม่เกิดจากค่ารุ่นใด” ปัญหาไม่ได้หายไป แต่เปลี่ยนจากการประสาน write บนตำแหน่งเดิม ไปเป็นการจัดการ dependency ระหว่าง version ซึ่งเหมาะกับ DAG และการคำนวณแบบ dataflow มากกว่า
ไม่แก้ที่เดิม ไม่ได้แปลว่าไม่ใช้หน่วยความจำร่วมกันเลย
Immutable collection ไม่จำเป็นต้องคัดลอกทุก element ทุกครั้ง โครงสร้างข้อมูลแบบ persistent สามารถแบ่งปันส่วนที่ไม่เปลี่ยนร่วมกันได้ เรียกว่า structural sharing เช่นการเพิ่มสมาชิกหน้ารายการอาจสร้าง node ใหม่เพียงหนึ่งตัวแล้วชี้ไปยังหางเดิม
val a = List(2, 3, 4)
val b = 1 :: a
// b มีค่า List(1, 2, 3, 4)
// ส่วนท้าย 2,3,4 สามารถใช้โครงสร้างเดิมร่วมกับ a ได้
ดังนั้น “สร้างค่าใหม่” ไม่ควรถูกตีความว่า “copy ข้อมูลทั้งหมดเสมอ” แต่ก็ไม่ฟรีทุกกรณี Array ขนาดใหญ่ที่ copy จริงย่อมมีต้นทุน และ Spark transformation ก็อาจสร้าง object จำนวนมากได้ การเลือก representation ยังคงสำคัญ
หลีกเลี่ยง Random Write แต่ไม่ใช่หลีกเลี่ยง Random Access ทุกกรณี
Functional data pipeline มักอ่านชุดข้อมูลแล้วสร้างชุดใหม่แบบ append หรือ sequential write แทนการวิ่งกลับไปแก้ record กระจัดกระจายในไฟล์เดิม วิธีนี้เข้ากับ distributed file system และ columnar storage ที่เขียนเป็นก้อน แต่ควรแยก random read ออกจาก random write: immutable array ยังอ่านตำแหน่งใดก็ได้ ส่วนสิ่งที่เราพยายามลดคือการเขียนทับตำแหน่งร่วม ซึ่งสร้าง dependency และ lock
จาก Lock ไปสู่ Version
ถ้าข้อมูลก้อนเดิมถูกแก้ ทุกคนต้องตกลงว่าใครแก้ได้และผู้อ่านเห็นสถานะใด Lock เป็นคำตอบหนึ่ง แต่ทำให้บางงานต้องรอ เมื่อใช้ immutable version ผู้อ่านเก่ายังอ่าน version เดิมได้ ผู้เขียนสร้าง version ใหม่ แล้วระบบค่อยเปลี่ยน pointer หรือ metadata ให้ผู้อ่านรุ่นถัดไปเห็น วิธีนี้พบได้ใน copy-on-write, MVCC, snapshot และ data lake table format หลายชนิด แม้รายละเอียดแต่ละระบบต่างกัน
Immutability ไม่ได้ทำให้ทุกอย่างปลอดภัยเอง
ตัวแปรอาจประกาศด้วย val แต่ object ข้างในยัง mutable ได้ เพราะ val ห้ามเปลี่ยน reference ไม่ได้ห้ามแก้สถานะภายใน object เช่น val buffer = ArrayBuffer(...) ยังเรียก buffer += x ได้ นักศึกษาจึงต้องแยก “ตัวแปรชี้ไปที่เดิม” ออกจาก “ค่าข้างในเปลี่ยนไม่ได้”
val xs = scala.collection.mutable.ArrayBuffer(1, 2)
xs += 3 // ทำได้ แม้ xs เป็น val
val ys = List(1, 2)
val zs = ys :+ 3 // สร้าง List ใหม่; ys ไม่เปลี่ยน
RDD Immutable แต่โค้ดใน Closure ยังสร้างปัญหาได้
แม้ RDD ไม่ถูกแก้ทับ ผู้ใช้ยังอาจปิดล้อม mutable variable ใน closure แล้วคาดว่าทุก executor จะแก้ตัวแปรเดียวกับ driver ตัวอย่างเช่นเพิ่ม counter ธรรมดาใน foreach ผลอาจไม่กลับมาที่ driver เพราะ closure ถูก serialize และส่งสำเนาไปทำงานคนละ process หากต้องนับเพื่อ monitoring ควรใช้ accumulator ตามข้อจำกัดของมัน หรือดีกว่านั้นคือสร้างผลลัพธ์ด้วย transformation และ aggregation ที่ชัดเจน
3ไวยากรณ์พื้นฐานที่ต้องรู้ก่อนเขียน Spark
ส่วนนี้เลือกเฉพาะไวยากรณ์ที่ช่วยให้อ่าน transformation ได้ ไม่ได้พยายามแทนตำรา Scala ทั้งเล่ม เป้าหมายคือเมื่อเห็นโค้ด Spark นักศึกษาควรแยกได้ว่าอะไรคือข้อมูล อะไรคือฟังก์ชัน และอะไรคือขั้นตอนที่ยังไม่ได้ลงมือคำนวณ
Variables: var vs val
var status: String = "draft" // เปลี่ยน reference ได้
status = "published"
val course: String = "Big Data" // เปลี่ยน reference ไม่ได้
// course = "AI" // compile error
val year = 2026 // compiler อนุมานว่าเป็น Int
หลักง่าย ๆ คือเริ่มด้วย val แล้วใช้ var เมื่อมีเหตุผลชัด ไม่ใช่เพราะ var เป็นของต้องห้าม แต่เพราะค่าที่ไม่เปลี่ยนลดจำนวนสถานะที่ต้องตามในหัว เวลา debug เราไม่ต้องถามว่าตัวแปรถูกแก้จากที่ใดมาบ้าง
Functions
def add(a: Int, b: Int): Int = a + b
def normalize(name: String): String =
name.trim.toLowerCase
println(add(5, 3)) // 8
println(normalize(" DATA ")) // data
Scala ใช้ค่าของ expression สุดท้ายเป็นค่าที่คืน จึงไม่จำเป็นต้องสร้างตัวแปรชั่วคราวหรือเขียน return ทุกครั้ง รูปแบบนี้ทำให้ฟังก์ชันเล็กและมองเป็นการจับคู่ input → output ได้ชัด แต่ฟังก์ชันหลายบรรทัดก็ยังใช้วงเล็บปีกกาได้เมื่อช่วยให้อ่านง่าย
Method กับ Function Value
def square(n: Int): Int = n * n // method
val squareFn: Int => Int = n => n*n // function value
List(1, 2, 3).map(square) // ส่ง method เป็นฟังก์ชัน
List(1, 2, 3).map(n => n * n) // anonymous function
ลูกศร => อ่านว่า “รับค่าไปทางซ้าย แล้วให้ expression ทางขวาสร้างผล” ใน Spark closure นี้จะถูก serialize และส่งไป executor จึงไม่ใช่แค่ callback ที่รันใน process เดียว เราควรหลีกเลี่ยงการปิดล้อม object ขนาดใหญ่หรือ resource ที่ serialize ไม่ได้ เช่น database connection
For Loop และ Collection
for (y <- 1 to 4) {
println("Value of y is: " + y)
}
val numbers = Vector(1, 2, 3, 4, 5)
val doubled = numbers.map(n => n * 2)
// numbers ยังเป็น Vector(1,2,3,4,5)
// doubled เป็น Vector(2,4,6,8,10)
Tuple: จับค่าหลายช่องไว้ด้วยกัน
val item = ("Bangkok", 1200.0)
val province = item._1
val amount = item._2
val (p, a) = item // destructuring
val partial: (Double, Long) = (1200.0, 1L)
// ใช้เป็น (sum, count) สำหรับค่าเฉลี่ยแบบกระจาย
Tuple สะดวกสำหรับผลชั่วคราว เช่นคู่ (key,value) หรือสถานะ (sum,count) แต่ถ้ามีหลายช่องจนต้องจำว่า _4 คืออะไร ควรใช้ case class ที่มีชื่อ field ชัดกว่า
Case Class: Record ที่อ่านความหมายออก
case class Sale(
province: String,
amount: Double,
paid: Boolean
)
val s1 = Sale("Bangkok", 500.0, paid = true)
val s2 = s1.copy(amount = 650.0)
// s1 ไม่เปลี่ยน; s2 เป็นค่ารุ่นใหม่
Case class สร้าง constructor, equality, pattern matching และ copy ให้ เหมาะกับการแทน record ที่มี schema ชัด ใน Spark Dataset case class ยังช่วยให้ได้ typed encoder แต่ไม่ได้หมายความว่า Dataset จะเร็วกกว่า DataFrame ทุกกรณี เพราะ optimizer และค่าใช้จ่ายในการแปลง object ยังต้องพิจารณา
Option: ยอมรับว่าบางค่าอาจไม่มี
def parseAge(text: String): Option[Int] =
text.toIntOption
parseAge("42") // Some(42)
parseAge("?") // None
val ages = List("20", "?", "35")
val valid = ages.flatMap(_.toIntOption)
// List(20, 35)
Option[A] บังคับให้เราคิดถึงกรณีมีค่า (Some) และไม่มีค่า (None) แทนการปล่อย null แล้วรอให้โปรแกรมล้มไกลจากต้นเหตุ อย่างไรก็ตามข้อมูล Spark SQL มี null semantics ของตนเอง จึงต้องเข้าใจทั้ง Option ใน Scala และ null ใน DataFrame แยกกัน
Pattern Matching: แยกกรณีอย่างเปิดเผย
def label(result: Option[Int]): String = result match {
case Some(age) if age >= 18 => "adult"
case Some(_) => "minor"
case None => "unknown"
}
Pattern matching ช่วยให้กรณีของข้อมูลปรากฏอยู่ในโค้ด ไม่ซ่อนในชุด if ที่กระจัดกระจาย แต่ pattern ที่ไม่ครอบคลุมทุกกรณีอาจทำให้เกิด MatchError ได้ compiler ช่วยเตือนได้ดีใน sealed hierarchy แต่ข้อมูลจากภายนอกยังต้อง validate เสมอ
Collection แต่ละชนิดมีต้นทุนต่างกัน
| ชนิด | เหมาะกับ | ข้อควรระวัง |
|---|---|---|
List | เพิ่มด้านหน้าและเดินตามลำดับ | เข้าถึงตำแหน่งกลางไม่ใช่ O(1) |
Vector | immutable sequence ที่เข้าถึงตำแหน่งได้ดี | โครงสร้างซับซ้อนกว่า List เล็กน้อย |
Array | ข้อมูลแน่นและเข้าถึงตำแหน่งเร็ว | mutable; ต้องระวังเมื่อนำไปแชร์ |
Map | ค้นหาค่าด้วย key | อย่าสับสนกับฟังก์ชัน map |
Set | สมาชิกไม่ซ้ำและตรวจ membership | ลำดับอาจไม่ใช่ส่วนหนึ่งของความหมาย |
การเรียก map บน collection คือการแปลงสมาชิก ไม่เกี่ยวกับชนิดข้อมูล Map[K,V] ที่เก็บ key/value ชื่อเหมือนกันจึงทำให้นักศึกษาใหม่สับสนได้ง่าย
raw.flatMap(parse).filter(valid).map(toPair) ให้ไล่ถามทีละสถานีว่า input ของสถานีนี้เป็นชนิดอะไร output เป็นชนิดอะไร และจำนวน record เปลี่ยนหรือไม่ วิธีนี้ง่ายกว่าพยายามอ่านทั้งบรรทัดเป็นประโยคยาวในครั้งเดียว4Higher-Order Functions: หัวใจของ Spark API
map, filter, reduce, flatMap) เป็น higher-order function ที่รับ "สิ่งที่จะทำกับแต่ละ record" เป็น parameter| ฟังก์ชัน | ความหมาย | ตัวอย่าง |
|---|---|---|
map | แปลงแต่ละ element เป็นอีกค่าหนึ่ง (1 ต่อ 1) | Array(1,2,3).map(n => n*n) → Array(1,4,9) |
filter | เก็บเฉพาะ element ที่ผ่านเงื่อนไข | Array(1,2,3,4).filter(n => n%2==0) → Array(2,4) |
flatMap | แปลงแต่ละ element เป็น 0 หรือหลายค่า แล้ว "แบน" ผลลัพธ์เป็น collection เดียว | Array("a b","c").flatMap(s => s.split(" ")) → Array("a","b","c") |
reduce | รวม element เป็นค่าเดียว ต้องมีข้อมูลอย่างน้อยหนึ่งค่า และ operation ควรจัดกลุ่มใหม่ได้หากจะขนาน | Vector(1,2,3).reduce(_+_) → 6 |
foldLeft | พับจากซ้ายพร้อมค่าเริ่มต้น ลำดับเป็นส่วนหนึ่งของ semantics | Vector(1,2,3).foldLeft(0)(_+_) → 6 |
map และ reduce สะท้อนรากเดียวกับ Functional Programming แต่ MapReduce ของ Google ไม่ได้เท่ากับการเรียก collection map แล้ว reduce บนเครื่องเดียว Framework เพิ่ม key grouping, partition, Shuffle, failure recovery และ distributed scheduling เข้ามา ชื่อเดียวกันจึงไม่ควรทำให้เรามองข้ามกลไกที่เพิ่มขึ้นFunction เป็นข้อมูลชนิดหนึ่ง
val clean: String => String = _.trim.toLowerCase
val nonEmpty: String => Boolean = _.nonEmpty
val result = List(" A ", "", " B ")
.map(clean)
.filter(nonEmpty)
// List("a", "b")
เมื่อฟังก์ชันเป็น value เราเก็บในตัวแปร ส่งเป็น argument และประกอบกับฟังก์ชันอื่นได้ Spark ใช้คุณสมบัตินี้รับ logic จากผู้ใช้ แล้วนำ logic เดียวกันไปรันกับ partition ต่าง ๆ ผู้ใช้จึงไม่ต้องสร้าง thread หรือส่ง message ระหว่าง worker เอง
map, flatMap และ filter เปลี่ยน Cardinality ต่างกัน
| ตัวดำเนินการ | หนึ่ง input ให้กี่ output | ภาพที่ควรนึก |
|---|---|---|
map | หนึ่งต่อหนึ่ง | เปลี่ยนรูปซองจดหมายทุกซอง แต่จำนวนซองเท่าเดิม |
filter | ศูนย์หรือหนึ่ง | ด่านตรวจปล่อยผ่านหรือคัดออก |
flatMap | ศูนย์ หนึ่ง หรือหลาย | เปิดกล่องหนึ่งใบแล้วเทของข้างในออกมาเป็นสายเดียว |
val lines = List("big data", "scala")
lines.map(_.split(" ").toList)
// List(List("big", "data"), List("scala"))
lines.flatMap(_.split(" "))
// List("big", "data", "scala")
mapValues, groupByKey และ reduceByKey ไม่ได้มีต้นทุนเท่ากัน
บน collection ในเครื่อง ฟังก์ชันเหล่านี้อาจดูเป็นเพียง API ต่างชื่อ แต่บนข้อมูลกระจาย groupByKey มักต้องส่ง values ทุกตัวผ่าน Shuffle แล้วเก็บกลุ่มไว้ ส่วน reduceByKey สามารถรวมผลภายใน partition ก่อนส่ง หาก operation รวมย่อยได้ ปริมาณข้อมูลข้ามเครือข่ายจึงอาจต่างกันมาก
foldLeft ไม่ใช่ Parallel Fold โดยอัตโนมัติ
foldLeft บน collection มีลำดับชัด: เริ่มจาก seed แล้วรวมสมาชิกจากซ้ายไปขวา จึงใช้กับ operation ที่ไม่ associative ได้ เช่นการลบ แต่ลำดับนี้ทำให้แบ่งเป็นต้นไม้อย่างอิสระไม่ได้ ส่วน distributed aggregate ต้องแยกการรวมภายใน partition กับการรวมผลระหว่าง partition และกำหนด operation ที่ปลอดภัยต่อการจัดกลุ่มใหม่
List(10, 3, 2).foldLeft(0)(_ - _)
// ((0 - 10) - 3) - 2 = -15
// ถ้าแบ่ง (10-3) และ (2) แล้วค่อยรวม
// ความหมายอาจไม่เหมือนเดิม การลบจึงไม่ใช่ reduction ที่จัดต้นไม้ได้ตรง ๆ
reduce ต้องการ Associativity; Commutativity ช่วยเมื่อ order ไม่แน่นอน
การบวกจำนวนเต็ม, max, min และ set union เหมาะกับ parallel reduction เพราะเปลี่ยนวงเล็บได้ ส่วนการต่อ string เปลี่ยนวงเล็บได้แต่สลับลำดับไม่ได้ หาก framework ไม่รับประกัน order ผลลัพธ์อาจต่างกัน นักศึกษาจึงต้องแยกสองคำถาม: จัดกลุ่มใหม่ได้หรือไม่ และ สลับตำแหน่งได้หรือไม่
Function Composition: สร้างชิ้นเล็กแล้วต่อกัน
val trim: String => String = _.trim
val lower: String => String = _.toLowerCase
val normalize = trim.andThen(lower)
normalize(" Spark ") // "spark"
ฟังก์ชันเล็กช่วยทดสอบง่ายและประกอบใหม่ได้ แต่การแตกทุก expression เป็นฟังก์ชันจิ๋วจนตามชื่อไม่ทันก็ไม่ได้ทำให้โค้ดดีขึ้น หลักคือแยกตามความหมายและขอบเขตการทดสอบ ไม่ใช่แยกเพื่อให้ดูเป็น functional
Currying และ Partial Application: เติมข้อมูลทีละชุด
def above(min: Double)(value: Double): Boolean =
value > min
val abovePassing = above(50.0)
List(30.0, 65.0, 80.0).filter(abovePassing)
// List(65.0, 80.0)
แนวคิดนี้ช่วยสร้างฟังก์ชันเฉพาะจากฟังก์ชันทั่วไป เช่นเติม configuration ก่อน แล้วส่งฟังก์ชันที่เหลือให้ transformation แต่ไม่จำเป็นต้องใช้ currying ทุกแห่ง ไวยากรณ์ที่ฉลาดเกินไปอาจเพิ่มภาระผู้อ่านมากกว่าลดโค้ด
5จาก Collection สู่ Spark: โค้ดสั้นลง แต่ระบบข้างใต้ใหญ่ขึ้น
ตัวอย่างเดิมมักนำ Java MapReduce มาเทียบกับ Scala Spark แล้วสรุปว่า Scala ดีกว่าเพราะจำนวนบรรทัดน้อยกว่า การเปรียบเทียบนี้ให้ภาพเพียงครึ่งเดียว เพราะเราเปลี่ยนทั้งภาษาและระดับ abstraction พร้อมกัน Java ตัวอย่างแรกต้องจัดการ Hadoop API ส่วน Scala ตัวอย่างหลังใช้ Spark API ที่สูงกว่า ความสั้นจึงไม่ได้มาจาก syntax ของ Scala เพียงอย่างเดียว
val lines = List(
"big data big",
"scala data"
)
val counts = lines
.flatMap(_.split("\\s+"))
.map(word => (word, 1))
.groupMapReduce(_._1)(_._2)(_ + _)
// Map(big -> 2, data -> 2, scala -> 1)
val lines = sc.textFile("input/*.txt")
val counts = lines
.flatMap(_.split("\\s+"))
.filter(_.nonEmpty)
.map(word => (word.toLowerCase, 1L))
.reduceByKey(_ + _)
counts.saveAsTextFile("output/word-count")
สองตัวอย่างดูคล้ายกัน แต่ความหมายทางกายภาพต่างกัน Collection อยู่ใน process เดียว ส่วน Spark RDD แบ่งเป็น partition และ reduceByKey อาจสร้าง Shuffle ข้ามเครื่อง บรรทัดที่เพิ่มเพียงจุดเดียวจึงอาจใช้เวลาหลายนาทีและย้ายข้อมูลหลายกิกะไบต์
ตัวอย่างค่าเฉลี่ยที่ถูกต้อง: ส่ง State ไม่ส่งค่าเฉลี่ยย่อย
// ข้อมูล: (ชื่อ, อายุ)
val ages = sc.parallelize(Seq(
("Ann", 20.0),
("Ann", 40.0),
("Bob", 30.0)
))
val averageByName = ages
.map { case (name, age) =>
(name, (age, 1L)) // (sum, count)
}
.reduceByKey {
case ((sumA, countA), (sumB, countB)) =>
(sumA + sumB, countA + countB)
}
.mapValues { case (sum, count) =>
sum / count
}
// Ann -> 30.0, Bob -> 30.0
จุดสำคัญไม่ใช่ความสั้น แต่คือ state (sum,count) มีโครงสร้างที่รวมผลย่อยได้ นี่คือ monoidification จากบทก่อน Spark จึงรวมใน partition ก่อน แล้วค่อยรวมข้าม partition โดยไม่ต้องส่งข้อมูลอายุทุกค่ามายังเครื่องเดียว
อย่าใช้ collect เป็นเครื่องหมายวรรคตอนท้ายทุกตัวอย่าง
collect() ดึงข้อมูลทั้งหมดกลับ driver เหมาะกับผลขนาดเล็กเพื่อดูหรือทดสอบ แต่ถ้าผลใหญ่กว่าหน่วยความจำ driver โปรแกรมอาจล้ม การเขียนตัวอย่างที่ลงท้ายด้วย collect ทุกครั้งทำให้นักศึกษาติดนิสัยนำข้อมูลกลับเครื่องตนเอง ซึ่งสวนทางกับหลัก “ส่ง code ไปหา data”
| Action | ใช้เมื่อ | ข้อควรระวัง |
|---|---|---|
take(n) | ดูตัวอย่างจำนวนเล็ก | ตัวอย่างไม่แทน distribution ทั้งหมด |
count() | ต้องการจำนวน record | ยังต้อง scan ข้อมูลตาม lineage |
collect() | มั่นใจว่าผลเล็กพอสำหรับ driver | เสี่ยง OutOfMemory และ network burst |
save... | ผลใหญ่และต้องใช้ต่อ | ต้องเลือก partition และ format ให้เหมาะ |
Closure ถูกส่งไป Executor
class Cleaner {
val hugeDictionary = loadHugeDictionary()
def run(lines: RDD[String]) =
lines.map(line => hugeDictionary.normalize(line))
}
closure ใน map อ้างถึง hugeDictionary และอาจดึง object รอบข้างติดไป serialize ด้วย หาก dictionary ใหญ่และเหมือนกันทุก task ควรพิจารณา broadcast variable หรือจัดโครงสร้างให้ closure จับเฉพาะสิ่งที่จำเป็น หาก object serialize ไม่ได้ job จะล้มก่อนหรือระหว่างส่ง task
Serialization คือค่าขนส่งที่ซ่อนอยู่
Built-in Expression มักเปิดทางให้ระบบ Optimize มากกว่า UDF
เมื่อใช้ DataFrame expression เช่น lower, sum, when หรือ SQL operator ระบบเห็นความหมาย ชนิดข้อมูล และ null behavior จึงวางแผนได้ ส่วน UDF เป็นกล่องดำมากกว่า อาจขวาง code generation หรือ optimization บางชนิด ดังนั้นหลักปฏิบัติที่ดีคือใช้ built-in function ก่อน แล้วใช้ UDF เมื่อ logic ไม่มีตัวแทนที่เหมาะ
ความกระชับไม่เท่ากับความชัดเจนเสมอไป
validSales, dailyTotals และตรวจ schema/plan ตามจุดที่สำคัญ ความเป็น functional ไม่ได้บังคับให้เขียนทุกอย่างเป็นบรรทัดเดียวLocal Collection API คล้าย Spark API แต่ไม่เหมือนกัน
การฝึกด้วย List หรือ Vector ช่วยเข้าใจชนิดและการประกอบฟังก์ชัน แต่ไม่แสดง partition, Shuffle, retry หรือ serialization จึงควรใช้ local collection เพื่อทดสอบ logic ขนาดเล็ก แล้วทดสอบ Spark local mode เพื่อดู execution semantics ก่อนขึ้นคลัสเตอร์จริง
6Functional Programming ช่วยลดการรอได้อย่างไร
Functional Programming ในวิชานี้ไม่ได้มีไว้เพื่อให้โค้ดดูสั้นเท่านั้น ประโยชน์สำคัญคือช่วยบอกระบบว่า “ต้องการคำนวณอะไร” โดยลดการผูกติดกับลำดับคำสั่ง เมื่องานไม่มีการแก้ตัวแปรร่วมกันโดยไม่จำเป็น ระบบจึงแบ่งงาน สลับลำดับ และนำไปรันหลายเครื่องได้ง่ายขึ้น
| แนวคิด | ผลต่อการคำนวณแบบกระจาย |
|---|---|
| Pure function | input เดิมให้ output เดิม จึงย้ายงานไปเครื่องอื่นหรือคำนวณใหม่หลังเครื่องล้มได้ง่าย |
| Immutability | แต่ละงานไม่แย่งกันแก้ข้อมูลก้อนเดียว ลดปัญหาการล็อกและผลลัพธ์ที่ขึ้นกับจังหวะเวลา |
| Map | แปลงข้อมูลแต่ละชิ้นอย่างอิสระ จึงกระจายให้หลาย worker ทำพร้อมกันได้ |
| Reduce | รวมผลย่อยกลับมา หาก operation จัดกลุ่มใหม่ได้ ระบบจะรวมเป็นต้นไม้แทนการรอต่อแถว |
การลดเวลารอมีอย่างน้อยสี่แบบ
| วิธีลดการรอ | Functional idea ที่ช่วย | สิ่งที่ยังขวางได้ |
|---|---|---|
| ทำหลาย record พร้อมกัน | Pure map ไม่มี dependency ข้าม record | ข้อมูลเอียง งานต่อ record ไม่เท่ากัน หรือ resource จำกัด |
| รวมผลเป็นต้นไม้ | Associative operation และ Monoid | operation ขึ้นกับลำดับ หรือ state ผลย่อยใหญ่ |
| เลื่อนงานที่ยังไม่จำเป็น | Lazy evaluation | เมื่อ action ต้องการผลจริง งานยังต้องถูกทำ |
| ไม่ทำงานเดิมซ้ำ | Cache/Persist ผลที่ใช้หลายรอบ | หน่วยความจำไม่พอ หรือค่าจัดเก็บแพงกว่าคำนวณใหม่ |
Pure Function: สูตรเดิม Input เดิม ควรได้ผลเดิม
Pure function มีสองใจความ: ผลลัพธ์ขึ้นกับ argument ที่รับ และการเรียกฟังก์ชันไม่สร้าง side effect ที่มองเห็นภายนอก เช่นไม่เขียนไฟล์ ไม่แก้ global variable และไม่ส่งข้อความ หาก function pure ระบบสามารถย้าย task ไปเครื่องอื่น รันซ้ำ หรือจำผลไว้ได้ง่ายกว่า
// ใกล้เคียง pure
def tax(amount: Double, rate: Double): Double =
amount * rate
// impure: ผลขึ้นกับเวลา และยังเขียนออกหน้าจอ
def stamp(message: String): String = {
println(message)
s"${System.currentTimeMillis}: $message"
}
คำว่า pure เป็นแบบจำลองเชิงเหตุผล ในระบบจริงฟังก์ชันอาจใช้ CPU และ memory ซึ่งเป็นผลทางกายภาพอยู่แล้ว เราสนใจว่าในระดับ semantics ของโปรแกรม การแทนการเรียกฟังก์ชันด้วยค่าผลลัพธ์จะไม่เปลี่ยนพฤติกรรมส่วนอื่น คุณสมบัตินี้เรียกว่า referential transparency
Pure ไม่ได้แปลว่า Deterministic เสมอ หาก Input ที่แท้จริงถูกซ่อน
ฟังก์ชันที่อ่านเวลาปัจจุบัน random number หรือ configuration ภายนอก แม้หน้าตารับ argument เดียว แต่จริง ๆ มี input ซ่อนอยู่ หากต้องการทำให้เหตุผลง่ายขึ้น เราสามารถส่ง clock, seed หรือ configuration เข้ามาเป็น argument อย่างเปิดเผย การทดสอบจึงกำหนด input ได้และการ retry ไม่เปลี่ยนผลอย่างคาดไม่ถึง
Immutability, Lazy Evaluation และ Cache เป็นคนละแกน
สามแนวคิดนี้เชื่อมกันแน่นจนมักถูกพูดเหมือนเป็นเรื่องเดียวกัน แต่จริง ๆ ตอบคนละคำถาม:
| แนวคิด | ตอบคำถามอะไร | ถ้าไม่มีจะเกิดอะไร |
|---|---|---|
| Immutability | ค่าที่สร้างแล้วถูกแก้ทับหรือไม่ | lineage และ cache อาจอ้างค่าที่เปลี่ยนไปแล้ว ต้องติดตาม write dependency |
| Lazy Evaluation | ลงมือคำนวณเมื่อใด | แต่ละคำสั่งอาจทำงานทันทีจนระบบมองไม่เห็นแผนรวม |
| Cache | ผลที่คำนวณแล้วจะเก็บไว้ใช้ซ้ำหรือไม่ | action รอบถัดไปอาจคำนวณ lineage เดิมซ้ำ |
ทำไม Immutability จึงทำให้ Laziness ปลอดภัยขึ้น
สมมุติเราบันทึกแผนว่า “นำราคาทุกชิ้นคูณ 1.07” แต่ก่อนลงมือ มีคนแก้ชุดราคาเดิมบางค่า หาก source mutable ผลขึ้นกับจังหวะว่า action เริ่มเมื่อใด นักพัฒนาตามเหตุผลยากขึ้น เมื่อ dataset เป็น immutable แผนที่สร้างไว้ยังอ้างค่ารุ่นเดิมอย่างชัดเจน การเลื่อนคำนวณจึงไม่เปลี่ยนความหมายเพราะข้อมูลถูกแก้กลางทาง
ทำไม Immutability จึงทำให้ Cache เชื่อถือได้ขึ้น
Cache มีความหมายก็ต่อเมื่อผลที่เก็บยังตรงกับ input หาก input ถูกแก้ทับ ระบบต้อง invalidate cache และตอบคำถามว่า cache ใดพึ่งพาค่าเดิมบ้าง แต่ถ้า input เป็น version ที่ไม่เปลี่ยน ผล cache ของ version นั้นยังถูกต้องเสมอ เมื่อมีข้อมูลใหม่ เราสร้าง version ใหม่และ cache แยกได้
Lazy ไม่ได้แปลว่าไม่คำนวณ
Transformation ที่ไม่ถูกใช้ต่ออาจไม่ต้องคำนวณเลย และหลาย transformation อาจถูก pipeline ใน stage เดียว แต่เมื่อ action ต้องการผล งานที่จำเป็นยังต้องทำ Laziness ลดงานที่ไม่จำเป็นและเปิดทางให้ optimize ไม่ได้เสกให้ต้นทุนของงานจำเป็นหายไป
val raw = sc.textFile("sales.csv") // กำหนดแหล่งข้อมูล
val paid = raw.filter(isPaid) // ยังอาจไม่อ่านไฟล์
val amounts = paid.map(parseAmount) // ยังสร้าง lineage
val n = amounts.count() // action: เริ่มคำนวณ
val total = amounts.reduce(_ + _) // หากไม่ cache อาจคำนวณซ้ำ
Cache ตรงไหนดี
ในตัวอย่างข้างบน amounts ถูกใช้สอง actions ถ้าการอ่านและ parse แพง เราอาจ persist ก่อน action แรก แต่ถ้า dataset ใหญ่เกิน memory หรือใช้เพียงครั้งเดียว cache อาจเพิ่ม overhead ควรดู reuse count, cost of recomputation, ขนาดข้อมูล และ storage level ไม่ใช่ cache เพราะเห็นว่า “น่าจะเร็ว”
จาก Random Write ไปสู่ Append + Transform
Data pipeline มักเก็บ input รุ่นเดิมแล้วเขียน output ไปตำแหน่งใหม่ ช่วยหลีกเลี่ยงการแก้กระจัดกระจายและ write lock บนก้อนเดิม อีกทั้งย้อนตรวจ lineage ได้ แต่ข้อแลกคือใช้พื้นที่มากขึ้น ต้องจัดการ version และลบข้อมูลเก่าตามนโยบาย ความคิดแบบ functional จึงไม่ได้ลบต้นทุน เพียงเปลี่ยนต้นทุนจาก synchronization ไปสู่ storage และ metadata management
ลด Dependency ไม่เท่ากับไม่มี Dependency
map แต่ละ partition อาจอิสระ แต่ reduceByKey ต้องรอข้อมูลจากหลาย partition ผ่าน Shuffle DAG จึงยังมีขอบ dependency และ stage barrier สิ่งที่ Functional Programming ช่วยคือทำให้ dependency ปรากฏในโครงสร้างการคำนวณแทนที่จะซ่อนอยู่ในการแก้ global state
Side Effect ควรย้ายไปขอบระบบ
โปรแกรมที่มีประโยชน์ต้องอ่านและเขียนอะไรสักอย่าง จึงไม่มีทาง pure ทุกส่วน หลักปฏิบัติคือให้แกน transformation เป็น pure เท่าที่ทำได้ แล้วรวม I/O ไว้ที่ขอบ เช่นอ่าน input ตอนต้นและ commit output ตอนท้าย ทำให้ส่วนใหญ่ของ pipeline ทดสอบด้วยข้อมูลใน memory ได้ และจุดที่ต้องจัดการ retry/idempotency มีจำนวนน้อยลง
Monoid เชื่อม Functional Programming กับ Distributed Systems
เมื่อ operation มี identity และ associative เราสามารถรวมผลย่อยภายใน partition แล้วรวมข้าม partition เป็นต้นไม้ได้ โครงสร้างทาง Algebra จึงไม่ได้อยู่เพื่อความสวยงาม แต่บอก scheduler ว่างานแบบใดเปลี่ยนวงเล็บได้โดยคำตอบไม่เปลี่ยน Sum, count, max, histogram และ top-k state เป็นตัวอย่าง ส่วนค่าเฉลี่ยต้องแปลงเป็น (sum,count) ก่อน
Functional Style ช่วย Optimizer เมื่อความหมายไม่เป็นกล่องดำ
Pure function ช่วยให้มนุษย์และระบบ reasoning ได้ แต่ Spark optimizer ไม่สามารถอ่านความหมาย Scala closure ทั่วไปได้ทั้งหมด DataFrame expression และ SQL operator จึงให้ข้อมูลแก่ optimizer มากกว่า UDF เพราะบอก schema, operator และ null semantics อย่างเปิดเผย ลำดับวิวัฒนาการจึงไม่ใช่เพียง imperative → functional แต่ยังเป็น opaque function → algebraic expression ที่ระบบวิเคราะห์ได้
7เมื่อยุคเปลี่ยน: Scala ในปี 2026 เทียบกับช่วงที่เริ่มใช้ Spark
ควรเรียน Scala 2 หรือ Scala 3
ถ้าเป้าหมายคือเขียนแอปกับ Spark ให้เริ่มจาก Scala binary version ที่ Spark รุ่นนั้นรองรับ ปัจจุบัน Spark 4.x distribution หลักใช้ Scala 2.13 ส่วนถ้าเป้าหมายคือเรียนภาษา Scala และ Functional Programming ระยะยาว Scala 3 มี syntax และคุณสมบัติรุ่นใหม่ที่น่าสนใจ ทั้งสองเส้นทางไม่ขัดกัน แต่ควรแยก “ภาษาที่อยากเรียน” ออกจาก “binary compatibility ที่ระบบงานกำหนด”
Scala, PySpark หรือ SQL: เลือกจากงาน ไม่ใช่เลือกทีม
| ทางเลือก | จุดแข็ง | สิ่งที่ต้องระวัง |
|---|---|---|
| Scala | เข้าถึง JVM API โดยตรง, static type, เหมาะกับ library และ typed Dataset บางงาน | learning curve และ binary compatibility |
| PySpark | เชื่อม Python data/ML ecosystem และทดลองได้สะดวก | Python boundary, serialization และชนิด UDF ที่เลือก |
| SQL/DataFrame | declarative, optimizer เห็น semantics และผู้ใช้หลายกลุ่มเข้าถึงได้ | logic เฉพาะทางบางอย่างเขียนยาก และ SQL สั้นไม่ได้แปลว่า plan เบา |
ในระบบจริงหนึ่งโครงการอาจใช้ทั้งสามแบบ เช่นใช้ SQL สำหรับ transformation หลัก, PySpark สำหรับ workflow และ ML, และ Scala สำหรับ extension ที่ต้องอยู่ใกล้ engine การเลือกภาษาจึงไม่ควรกลายเป็นสงครามศาสนา เพราะคอขวดมักอยู่ที่ Shuffle, skew, I/O และ plan มากกว่าจำนวนตัวอักษรใน syntax
อย่าใช้ UDF ถ้า Built-in Function ทำได้
ข้อแนะนำนี้สำคัญกว่าคำถามว่า UDF ภาษาใดเร็วกว่า Built-in expression เปิดให้ Spark วิเคราะห์และ optimize ได้เต็มกว่า อีกทั้งมักรองรับ columnar execution และ code generation ได้ดีกว่า เมื่อจำเป็นต้องใช้ Python UDF จึงค่อยพิจารณา Arrow หรือ vectorized API ตามชนิดงานและทดสอบด้วยข้อมูลจริง
ภาษาเปลี่ยน แต่ Algebra ยังอยู่
จาก Scala collection ไป RDD, จาก RDD ไป DataFrame และจาก DataFrame ไป SQL ชื่อ API อาจเปลี่ยน แต่คำถามเดิมยังอยู่: transformation นี้เปลี่ยนจำนวน record อย่างไร? แบ่งตาม key ใด? ต้อง Shuffle หรือไม่? aggregate รวมย่อยได้หรือไม่? cache แล้วคุ้มหรือไม่? คนที่ตอบคำถามเหล่านี้ได้จะย้าย framework ได้เร็วกว่าคนที่จำ syntax ได้เพียงชุดเดียว
8สรุปและขั้นตอนถัดไป
Scala ช่วยให้แนวคิด Functional Programming ปรากฏเป็นภาษาที่อ่านได้ชัด แต่สาระของบทนี้ไม่ได้จบที่เครื่องหมาย => หรือการเลือก val แทน var สิ่งที่ต้องติดตัวไปคือความสามารถในการมองโปรแกรมเป็น transformation และ dependency graph
เมื่อข้อมูลไม่ถูกแก้ทับ เราลด write dependency และอ้าง version ได้ชัด เมื่อฟังก์ชันมี input/output ที่เปิดเผย เรารันซ้ำและย้ายงานได้ง่าย เมื่อการคำนวณเป็น lazy ระบบเห็นแผนก่อนลงมือ และเมื่อผลบางก้อนถูกใช้ซ้ำ เราเลือก cache ได้ แต่ไม่มีแนวคิดใดให้ประโยชน์ฟรี: Immutability แลกด้วย version/storage, Laziness อาจทำให้งงว่า action ใดเริ่มงาน และ Cache อาจแย่งหน่วยความจำ
ดังนั้นความสัมพันธ์ที่ควรจำคือ:
- Immutability ทำให้ความหมายของค่าคงที่และ lineage เชื่อถือได้
- Pure Function ทำให้ task ย้าย รันซ้ำ และทดสอบได้ง่ายขึ้น
- Higher-Order Function แยก logic ของผู้ใช้ออกจากกลไกกระจายงาน
- Associativity และ Monoid เปิดทางให้รวมผลเป็นต้นไม้และทำ partial aggregation
- Lazy Evaluation เปิดพื้นที่ให้ระบบรวมและปรับแผนก่อน action
- Cache เลี่ยงการคำนวณ lineage เดิมซ้ำเมื่อคุ้มกับพื้นที่ที่ใช้
- Relational Algebra ทำให้ optimizer เข้าใจความหมายได้มากกว่า closure ที่เป็นกล่องดำ
บทถัดไปจะพูดถึง Superlinear Speedup ซึ่งดูเหมือนขัดกับสามัญสำนึกว่าใช้ CPU แปดตัวควรเร็วได้ไม่เกินแปดเท่า จุดเชื่อมจากบทนี้คือการเปลี่ยนรูปการคำนวณและการจัดข้อมูลอาจลด cache overhead หรือปริมาณงานที่เครื่องต้องทำจริงได้ การขนานจึงไม่ได้มีเพียง “แบ่งงานเดิมเป็นหลายส่วน” แต่บางครั้งเปลี่ยนพฤติกรรมของระบบทั้งชุด
- อธิบายว่าทำไม Scala ถึงเหมาะกับ Spark มากกว่าภาษาที่เน้น mutable state แบบ imperative ล้วน ๆ
- เขียนตัวอย่างโค้ด Scala ที่แสดงปัญหาของ mutable object เมื่อถูกแชร์ระหว่าง thread และแก้ปัญหาด้วย immutable design
- อธิบายความหมายของ
map,filter,flatMap,reduceพร้อมยกตัวอย่างผลลัพธ์ - อธิบายว่าทำไม
foldLeftจึงไม่ใช่ parallel reduction โดยอัตโนมัติ และยกตัวอย่าง operation ที่เปลี่ยนวงเล็บไม่ได้ - เปรียบเทียบ Word Count บน Scala collection กับ Spark RDD แม้โค้ดหน้าตาคล้ายกัน แต่ execution ต่างกันอย่างไร
- เชื่อมโยง pure function และ immutability กับการแบ่งงาน การคำนวณใหม่ และการลดการใช้ shared mutable state
- อธิบายว่า Immutability, Lazy Evaluation และ Cache เป็นคนละแนวคิด แต่ช่วยกันสร้าง data pipeline ที่คำนวณซ้ำและปรับแผนได้อย่างไร
- ออกแบบสถานะผลย่อยสำหรับหาค่าเฉลี่ยและ variance โดยไม่ส่งข้อมูลดิบทั้งหมด แล้วอธิบายว่าทำไมสถานะนั้นรวมแบบขนานได้
- ยกตัวอย่าง closure ที่จับ object ใหญ่เกินจำเป็น และเสนอวิธีลด serialization หรือใช้ broadcast อย่างเหมาะสม
- อธิบายว่าทำไม built-in DataFrame expression มักเปิดให้ optimizer ทำงานได้มากกว่า UDF
- เปรียบเทียบ Scala, PySpark และ SQL โดยพิจารณา semantics, ecosystem, optimizer และค่าใช้จ่ายข้าม runtime แทนการสรุปเพียงว่าภาษาใดเร็วกว่า