บทความจาก https://www.tamemo.com/post/158/fp-02-lambda-and-closure/
ในบทที่แล้ว เราพูดถึง Pure Function ว่ามันคือการที่เซ็ตฟังก์ชันเป็นตัวแปรได้ ในบทนี้เราจะเราพูดเสริมในหัวข้อนั้นต่อ
เนื้อหาในบทความนี้จะมีบางหัวข้อซ้ำซ้อนกับบทความที่เราเคยเขียนไปก่อนหน้านี้ในซีรีส์ JavaScript ฉบับมือใหม่ ตอนที่ 3 Js มองในมุมของ Functional Programming อยู่นิดหน่อยนะ ... แต่จะเอามาขยายความให้ลึกขึ้นหน่อย และไม่ได้โฟัสกับภาษา JavaScript นะ
ก่อนอื่นลองดูโค้ดนี้
function whenClick(){
...
}
button.onClick(whenClick)
หากใครเคยเขียนโปรแกรมฝั่ง front-end หรือโปรแกรมที่ต้องมี ui ด้วย น่าจะคุ้นกับการเขียนโปรแกรมแบบนี้คือ Event-Driving
นั่นคือเราจะกำหนดว่าเมื่อปุ่มถูกกด จะให้ทำงานแบบฟังก์ชันที่กำหนดไว้
แต่ถ้า event ของเรามีเยอะมาก เช่นมีปุ่มมากกว่าหนึ่งปุ่ม เราอาจจะต้องเขียนโค้ดแบบนี้
function whenClick1(){
...
}
function whenClick2(){
...
}
function whenClick3(){
...
}
button1.onClick(whenClick1)
button2.onClick(whenClick2)
button3.onClick(whenClick3)
เราจะพบว่า เราจะต้องสร้างฟังก์ชันเยอะมาก อาจจะทำให้อ่านยาก และที่สำคัญคือเราต้องตั้งชื่อฟังก์ชันทุกตัวด้วย (การตั้งชื่อตัวแปรหรือฟังก์ชัน สำหรับ
โปรแกรมเมอร์น่าจะรู้ๆ กันว่าเป็นงานที่เราไม่ค่อยอยากทำกัน ฮา)
Lambda สร้างฟังก์ชันทันใจ
แลมด้าหรือที่เรียกว่า "Anonymous Function" (ฟังก์ชันนิรนาม)
ก่อนจะอธิบายเรื่องแลมด้า ลองดูตัวอย่างโค้ดนี้ก่อน
var x = 10
print(x)
สำหรับตัวแปร x
ที่เราสร้างขึ้นมาเพื่อเก็บค่า 10
แล้วนำมาปริ๊นค่าต่อ
จากโค้ดข้างบนนี่ เราสามารถลดรูปให้เหลือแค่นี้ได้
print(10)
นั่นแปลว่าเราสามารถลดการสร้างตัวแปร แล้วหันมาใช้ literal แทนได้ถ้าตัวแปรค่านั้นใช้งานแค่ครั้งเดียว
"literal" คือค่า value ที่ไม่ใช่ตัวแปร เช่น
10
,"A"
ซึ่งต่างจากตัวแปรหรือ variable เพราะค่าแบบ literal สามารถนำไปโปรเซสค่าได้เลย แต่สำหรับค่าแบบตัวแปรจะต้องมีการเข้าไปดึงค่ามาจาก memory ก่อนนำมาใช้งานได้
ดังนั้นถ้าเราใช้หลักการเดียวกันกับโค้ดฟังก์ชันตอนแรก
function whenClick(){
...
}
button.onClick(whenClick)
แทนที่เราจะสร้างฟังก์ชันเตรียมไว้ก่อน (เหมือนประกาศตัวแปร) เราก็เอา function literal ไปใช้เป็น value แทนเลยก็ได้ ... แบบนี้
button.onClick(function whenClick(){
...
})
ในเคสนี้ ถ้าเราไม่อยากตั้งชื่อให้ฟังก์ชัน ภาษาส่วนใหญ่ก็มักจะให้ละส่วนนี้ไว้ได้ เป็นแบบนี้
button.onClick(function(){
...
})
การใช้ฟังก์ชันแบบนี้ เราเรียกว่าแลมด้า ซึ่งเป็นอักษรกรีกตัว λ โดยเป็นชื่อที่มาจากวิชา Lambda Calculus ในจักรวาล FP ของ Alonzo Church
สังเกตว่าเวลาใช้แลมด้านั้น เราสร้างเวลาใช้งานเลย สร้างทีเดียวทิ้ง ไม่จำเป็นต้องประกาศชื่อให้มัน เพราะไม่มีการอ้างอิงที่ใดอีก
Note: ในแต่ละภาษามีวิธีสร้าง lambda ที่ต่างกัน
// แบบมาตราฐาน button.onClick(function(){ ... }) // แบบยาวสุดๆ ในภาษา OOP เช่น Java button.onClick(new OnClickListener(){ public void click(Event e){ ... } }) // แบบย่อหรือ "arrow function" เช่นในภาษา JavaScript button.onClick(() => { ... }) // แบบย่อกว่าแบบที่แล้ว เช่นในภาษา Dart button.onClick((){ ... }) // แบบย่อสุดๆ จนไม่เหลืออะไรแล้ว ในภาษา Kotlin button.onClick { ... } // แบบใช้คำว่า lambda เลยในภาษา Python button.on_click(lambda _: ...)
Closure
หรือแปลว่า "การปิดล้อม" หรือ "พื้นที่ปิดล้อม" เป็นคุณสมบัติพิเศษอีกอย่างใน FP ซึ่งใช้ได้กับฟังก์ชันธรรมดาหรือแลมด้าก็ตาม
Scope
ภาษาโปรแกรมส่วนใหญ่ สามารถสร้างฟังก์ชันซ้อนๆ กันได้ (nested) ในแต่ละชั้นของฟังก์ชันที่สร้างซ้อนๆ กันอาจจะมีการสร้างตัวแปรเอาไว้ แต่การจะเรียกใช้ตัวแปรพวกนั้น ไม่ใช่ว่าฟังก์ชันทุกชั้นจะเรียกใช้ได้ เราเรียกว่า Scope ในการเรียกใช้ตัวแปร
ลองดูโค้ดข้างล่างประกอบ
function a(){
var x = 10
function b(){
var y = 20
function c(){
var z = 30
x, y, z // Ok! ฟังก์ชั c สามารถเรียกใช้ตัวแปรได้หมดเลย
}
x, y // Ok! ฟังก์ชั c สามารถเรียกใช้ตัวแปรได้หมดเลย
z // ในฐานะ b เราไม่รู้จักตัวแปร z เพราะมันถูกสร้างใน c
}
x // Ok! ฟังก์ชัน b สามารถเรียกใช้ x ได้
y, z // ในฐานะ a เราไม่รู้จักทั้งตัวแปร y และ z เพราะมันถูกสร้างภายในฟังก์ชันข้างในทั้งคู่เลย
}
ให้จำไว้ว่าฟังก์ชันจะมีคุณสมบัติการปิดล้อม หรือก็คือ "Closure" ในการปกปิดตัวแปรภายในเอาไว้ ถ้าสโคปที่เราอยู่ อยู่ข้างนอกฟังก์ชัน เราจะไม่มีสิทธิในการใช้ตัวแปรในฟังก์ชันนั้นเลย
เช่น...
ถ้าเราอยู่ในสโคปของฟังก์ชัน b()
เราสามารถใช้งานตัวแปร y
ซึ่งเป็นสโคปของตัวมันเองได้อยู่แล้ว ... แต่เพิ่มเติมคือมันใช้ตัวแปร x
ซึ่งเป็นสโคปของ a()
แต่เพราะฟังก์ชัน b
อยู่ใน a
ก็เลยใช้งานได้ ไม่มีปัญหา
แต่สำหรับตัวแปร z
ที่อยู่ในฟังก์ชัน c()
จะไม่สามารถเรียกได้เลย เพราะโดน Closure ของฟังก์ชัน c()
ปิดล้อมเอาไว้นั่นเอง
─────
แต่นอกจากคุณสมบัติ scope แล้ว การใช้งาน closure ยังมีเรื่องที่น่าสนใจ
function outer() {
var x = 10
function inner() {
return x
}
return inner
}
ตอนนี้เรามีฟังก์ชันอยู่ 2 ตัวคือ inner
ที่ถูกสร้างอยู่ใน outer
อีกทีนึง
โครงสร้างแบบนี้เราเรียกว่า outer
ล้อมตัวแปร x
และฟังก์ชัน inner
เอาไว้ และรีเทิร์นค่ากลับเป็น high order function
โดยที่ฟังก์ชัน inner
นั้นสามารถใช้ตัวแปร x
ซึ่งเป็นสโคปของ outer
ได้
Quiz?
ทีนี้ เราลองมาเดากันหน่อย ว่าถ้ารันโค้ดข้างล่างนี้ จะได้คำตอบเป็นอะไร
var func = outer()
print(func()) // ได้คำตอบเป็นอะไร?
ถ้าใครอยากลองก็หยุดคิดดูก่อน เมื่อได้แล้วก็ดูคำตอบต่อไปได้เลย
var func = outer()
// ตอนนี้ func ก็คือฟังก์ชัน inner ที่ outer รีเทิร์นกลับมาให้นั่นแหละ
print(func()) // คำตอบคือ 10 นั่นเอง!
อันนี้ไม่ยาก เมื่อเราเรียกใช้ outer
มันก็จะรีเทิร์นค่ากลับมาเป็น ฟังก์ชัน inner ซึ่งถ้าเราเรียกใช้มันต่อ มันก็จะรีเทิร์นค่า 10 กลับมานั่นเอง
การทำงานก็ตรงไปตรงมานี่นา? แล้วมีอะไรน่าแปลกเหรอ?
แต่ตามหลักการทำงานของฟังก์ชัน การที่ inner
ยังเรียกใช้งานตัวแปร x
ได้อยู่นั่นแหละ คือเรื่องแปลก!!
ถ้าใครยังไม่รู้ว่า เมื่อเราเรียกใช้ฟังก์ชัน มันทำงานในเชิงเมโมรี่ยังไง อ่านเพิ่มได้ก่อนที่ Function ทำงานยังไง?, ในมุมมองของโปรแกรมเมอร์สาย Imperative
ลองดูรูปข้างล่างนี่เพื่ออธิบายว่าทำไมเหตุการณ์แบบนี้ถึงแปลก
- เรียกใช้ฟังก์ชัน
outer
เกิดสโคปของตัวแปรx
และinner
ขึ้น - ฟังก์ชัน
outer
รีเทิร์นค่ากลับเป็นฟังก์ชันinner
--> ตามหลักการของฟังก์ชัน ถ้ามันรีเทิร์นค่ากลับแล้ว มันจะหยุดการทำงานทันที นั่นแปลว่าตัวแปรx
ก็จะหายไปด้วยในจังหวะนี้! - พอเราเรียกใช้
inner
ซึ่งต้องรีเทิร์นค่าx
กลับ ... นั่นแหละ แล้วจะไปหยิบค่า x มาจากไหนในเมื่อฟังก์ชันคืนเมโมรี่กลับไปแล้ว?
สาเหตุที่ x
ยังสามารถเรียกใช้ได้อยู่ เพราะคุณสมบัติของ Closure นั่นเอง
ในเชิงการจัดการเมโมรี่เมื่อเราสร้างฟังก์ชันซ้อนๆ กัน เมื่อฟังก์ชันทำงานเสร็จแล้วจะมีการเช็กว่าตัวแปรในสโคปของมันมีการเรียกใช้งานจากฟังก์ชันที่อยู่ใน closure ส่วนผิดล้อมของมันหรือไม่ ถ้ายังมีฟังก์ชันจะถูกย้ายไปเก็บไว้ใน [[memory]] พิเศษอีกส่วนหนึ่ง (แยกเป็นคนละส่วนกับ stackframe)
สำหรับภาษา OOP
ถึงภาษา OOP จะไม่มีการสร้างฟังก์ชัน แต่ก็มีเมธอดแทน เพียงแต่กฎการเรียกใช้ตัวแปรจะต่างจาก FP นิดหน่อย
class Outer {
int x;
public void outerMethod(){
final int y;
int z;
new Inner(){
public void innerMethod(){
// x, y สามารถเรียกใช้ได้
// z ใช้ไม่ได้ เพราะถือว่าเป็นสโคปของเมธอด
}
};
}
}
สำหรับ inner method จะสามารถเรียกใช้ตัวแปร x ที่เป็น properties ของคลาสได้ แต่ถ้าตัวแปรนั้นเป็น local ในเมธอดละก็ จะเรียกใช้งานไม่ได้เลย ตามเหตุผลที่อธิบายไปข้างบน (OOP ไม่มีคุณสมบัติของ Closure) ถ้าจะใช้จริงๆ จะต้องประกาศให้ตัวแปรนั้นเป็น final
ซะก่อน ระบบของOOPถึงจะอนุญาตให้เรียกใช้ได้
การประยุกต์ใช้งาน Closure
เราสามารถใช้ประโยชน์จาก Closure ได้หลักๆ คือการปิดล้อมตัวแปร (ก็ตามชื่อมันนั่นแหละ)
เช่น
var count = 0
function countIt() {
count++
return count
}
print(countIt()) // 1
print(countIt()) // 2
print(countIt()) // 3
เราทำฟังก์ชันสำหรับนับขึ้นมา แต่ข้อเสียของฟังก์ชันนี้คือมีการเรียกใช้ตัวแปร global ทำให้ไม่มีความเซฟเลยในการใช้ฟังก์ชัน
แต่เราสามารถสร้างฟังก์ชันครอบโค้ดชุดนี้เอาไว้ เพื่อกันไม่ให้ภายนอกเข้ามายุ่งกับตัวแปร count
แบบนี้
function createCountIt(){
var count = 0
function countIt() {
count++
return count
}
return countIt
}
var counter = createCountIt()
counter() // 1
counter() // 2
counter() // 3
แล้วก็มีประโยชน์อีกอย่างด้วย นั่นคือเราสามารถสร้างฟังก์ชัน counter นี่ขึ้นมาได้หลายชุด
var firstCounter = createCountIt()
var secondCounter = createCountIt()
firstCounter() --> 1
secondCounter() --> 1
secondCounter() --> 2
firstCounter() --> 2
firstCounter() --> 3
ซึ่งทั้ง 2 ตัวก็จะมี count
สำหรับนับเลขแยกกันใช้ ไม่ผสมกัน
ในบทต่อไป เราจะกลับไปถูกถึงเรื่องที่ได้ใช้งานกับโลก imperative ก็บ้าง นั่นคือการใช้ map
, filter
, reduce
และตัวอื่นๆ ที่น่าสนใจกัน
Top comments (0)