เป็นโปรแกรมย่อย (subprogram) ชนิดหนึ่ง ที่มีหน้าที่ คำนวณหาค่า เมื่อได้ค่าแล้ว ต้องส่งค่านั้นกลับไปยัง โปรแกรม หลัก (main program) การส่งค่ามาคำนวณใน โปรแกรม ย่อยนั้นมี 2 แบบคือ แบบแรกเรียกว่า แบบฟังก์ชัน ซึ่งจะส่งค่ากลับไปยัง โปรแกรมหลักได้ทีละค่า ( อีกแบบหนึ่งเรียกว่า แบบ procedures ซึ่งจะส่งค่าที่คำนวณ ได้กลับไปโปรแกรมหลักได้ทีละหลายค่า )
ถ้าพูดถึงคำว่า Function แล้ว ความคิดแรกทุกคนน่าจะนึกถึงวิชาคณิตศาสตร์ แต่ไหนๆ บล็อกนี้ก็เป็นบล็อก Programming แล้ว เราจะขอพูดถึงเรื่องฟังก์ชันในมุมมองของโปรแกรมเมอร์แทนละกัน
Function คืออะไร
นิยามของฟังก์ชันคือ "Black Box ที่รับค่าบางอย่างเข้าไป แล้วทำการคำนวณแล้วตอบผลลัพธ์กลับมา"
ในวิชาคณิตศาสตร์จะเทียบได้กับกราฟที่ไม่มีจุด x อยู่ในแนวตั้งเดียวกัน หรือก็คือสำหรับค่า x ทุกๆ x จะต้องมี y คู่กับมันแค่ค่าเดียวเท่านั้น (ถ้าไม่เข้าใจ ดูรูปประกอบข้างล่าง)
ถ้าเทียบกับภาษาโปรแกรม
- x จะเทียบได้กับ input
- y จะเทียบได้กับ output
เหตุผลที่มี output ได้แค่ค่าเดียวก็เพราะฟังก์ชันคือการส่งค่าไปคำนวณหรือประมวลผลอะไรบางอย่างแล้วส่งคำตอบกลับมา แล้วการส่งคำตอบกลับมาเนี่ย มันก็มีได้คำตอบเดียวยังไงล่ะ ... การคำนวณไม่สามารถให้คำตอบ 2 ค่าด้วยคำถามเดียวกันได้นะ (แม้ว่าจะคำนวณผิด ก็ต้องตอบมาค่าเดียวอยู่ดี)
เช่น ถามว่า
1 + 1 = ?การตอบก็จะต้องตอบว่า f(x) = x + 10 │ │ └─┬──┘ │ │ └─ body │ └─── parameter(s) └── function name1 (ในกรณีที่ตอบถูก) หรือจะตอบ f(x) = x + 10 │ │ └─┬──┘ │ │ └─ body │ └─── parameter(s) └── function name2, f(x) = x + 10 │ │ └─┬──┘ │ │ └─ body │ └─── parameter(s) └── function name3, f(x) = x + 10 │ │ └─┬──┘ │ │ └─ body │ └─── parameter(s) └── function name4 อะไรก็ว่าไป (แน่นอนว่าตอบผิด)
แต่มันไม่สามารถมี output ออกมาสองค่าพร้อมกันได้นะ เช่นบอกว่า f(x) = x + 10 │ │ └─┬──┘ │ │ └─ body │ └─── parameter(s) └── function name5 จะเป็นทั้ง 2 และ 3 ในเวลาเดียวกันเหรอ? แบบนี้ไม่ได้! ... หรือถึงจะออกมาค่าเดียว เช่นเรียก f(x) = x + 10 │ │ └─┬──┘ │ │ └─ body │ └─── parameter(s) └── function name6 แต่ลองเรียกอีกครั้งดันได้ f(x) = x + 10 │ │ └─┬──┘ │ │ └─ body │ └─── parameter(s) └── function name7 แบบนี้ก็ถือว่ามีหลาย output ไม่ได้เหมือนกัน
(คือใส่ค่าเข้าไปเหมือนกัน ต้องได้ค่าคำตอบเดิมออกมา จะเปลี่ยนไปเรื่อยๆ ไม่ได้)
หลักการใช้งานฟังก์ชันคือ
- Declaration: การประกาศฟังก์ชัน
- Call: การเรียกใช้งานฟังก์ชัน
เราจะต้อง declare ฟังก์ชันก่อนเรียกใช้งานเสมอ ไม่งั้นคอมพิวเตอร์ก็จะไม่รู้ว่าฟังก์ชันนี้ต้องทำงานยังไง
สำหรับภาษาโปรแกรมจะต้องเขียนละเอียดขึ้น ต้องมีการกำหนด type ว่า input, output เป็นอะไรด้วย (ฟังก์ชันในคณิตศาสตร์ไม่ต้องกำหนด เพราะใช้ number type เป็นหลักเท่านั้น)
┌─────────────────── function name │ ┌────────────── parameter(s) │ │ ┌──── return type function plusTen(x: Int): Int { return x + 10 } └─┬──┘ └─ bodyหน้าที่หลักของฟังก์ชัน ถ้าพูดในเชิงการเขียนโปรแกรม มันคือการรวมกลุ่ม code ที่ใช้งานบ่อยๆ เข้าไว้ด้วยกันเป็นก้อนเดียว ทำให้เวลาเขียนโปรแกรมขนาดใหญ่ เราไม่จำเป็นต้องเขียนโค้ดซ้ำๆ กันหลายๆ รอบ
หน้าที่ของฟังก์ชันอีกอย่างคือการสร้าง Encapsulation หรือการห่อหุ้ม code กลุ่มหนึ่งแล้วสร้างชื่อเรียก code กลุ่มนั้นแทน เช่นการสร้างฟังก์ชัน f(x) = x + 10 │ │ └─┬──┘ │ │ └─ body │ └─── parameter(s) └── function name8, f(x) = x + 10 │ │ └─┬──┘ │ │ └─ body │ └─── parameter(s) └── function name9, ┌─────────────────── function name │ ┌────────────── parameter(s) │ │ ┌──── return type function plusTen(x: Int): Int { return x + 10 } └─┬──┘ └─ body0 หรือ ┌─────────────────── function name │ ┌────────────── parameter(s) │ │ ┌──── return type function plusTen(x: Int): Int { return x + 10 } └─┬──┘ └─ body1 ขึ้นมา เวลาเรียกใช้งานก็จะง่ายขึ้น เพราะเราไม่จำเป็นต้องรู้การทำงานภายในของฟังก์ชันเลย รู้แค่ฟังก์ชันจะทำอะไรออกมาให้ก็พอ
ฟังก์ชันทำงานอย่างไร, ในมุมมองของคอมพิวเตอร์
เพื่อให้เข้าใจฟังก์ชันจริงๆ เราต้องรู้ก่อนว่าเวลาฟังก์ชันทำงาน เกิดอะไรขึ้นภายในคอมพิวเตอร์บ้าง
Stack Frame
อย่างที่เรารู้กันว่าตัวแปรทุกตัวที่เราเขียนขึ้นมาในโค้ด ต้องการที่อยู่เพื่อเก็บ value ของมัน (เก็บอยู่ใน RAM ซึ่งเป็น main memory ไง)
แต่ใช่ว่าตัวแปรทั้งหมดจะกองกันอยู่ในพื้นที่เดียวกัน โดยส่วนใหญ่แล้วพื้นที่ในเมโมรี่จะถูกแบ่งออกเป็นส่วนๆ คือ Heap และ Stack โดยในบทความนี้เราจะโฟกัสในส่วนของสแต็ก หรือที่เรียกว่า "Stack Frame" ซึ่งใช้เก็บค่าของตัวแปรแยกกันตาม function ...
function mul(x, y){ var z = x * y return z } main(){ var a = 2 var b = 5 var c = mul(a, b) }ตัวอย่างโค้ดข้างบนนี้...
- โค้ดเริ่มต้นทำงานที่ฟังก์ชัน ┌─────────────────── function name │ ┌────────────── parameter(s) │ │ ┌──── return type function plusTen(x: Int): Int { return x + 10 } └─┬──┘ └─ body2 (ในขณะนี้ฟังก์ชัน ┌─────────────────── function name │ ┌────────────── parameter(s) │ │ ┌──── return type function plusTen(x: Int): Int { return x + 10 } └─┬──┘ └─ body3 ยังไม่ถูกเรียกให้ทำงาน ดังนั้นตัวแปรทั้งหมดจะยังไม่ถูกจองพื้นที่ในเมโมรี่) --> ฟังก์ชันเริ่มทำงาน จะเกิดการจองพื้นที่ในเมโมรี่ เรียกว่า frame ของ ┌─────────────────── function name │ ┌────────────── parameter(s) │ │ ┌──── return type function plusTen(x: Int): Int { return x + 10 } └─┬──┘ └─ body2
- การประมวลผลจะทำงานเรียงตามบรรทัด เริ่มจากการกำหนดค่า ┌─────────────────── function name │ ┌────────────── parameter(s) │ │ ┌──── return type function plusTen(x: Int): Int { return x + 10 } └─┬──┘ └─ body5, ┌─────────────────── function name │ ┌────────────── parameter(s) │ │ ┌──── return type function plusTen(x: Int): Int { return x + 10 } └─┬──┘ └─ body6 ใน 2 บรรทัดแรก --> variable และ value ก็จะถูกจองลงไปในเมโมรี่ (ดูรูปประกอบข้างล่าง)
- ต่อมา, ที่บรรทัดที่ 3 ของ ┌─────────────────── function name │ ┌────────────── parameter(s) │ │ ┌──── return type function plusTen(x: Int): Int { return x + 10 } └─┬──┘ └─ body2 มีการ call ฟังก์ชัน ┌─────────────────── function name │ ┌────────────── parameter(s) │ │ ┌──── return type function plusTen(x: Int): Int { return x + 10 } └─┬──┘ └─ body3 เกิดขึ้น --> มีการเปิดเฟรมใหม่ของฟังก์ชัน ┌─────────────────── function name │ ┌────────────── parameter(s) │ │ ┌──── return type function plusTen(x: Int): Int { return x + 10 } └─┬──┘ └─ body3 ขึ้นมาข้างบนเฟรมของ ┌─────────────────── function name │ ┌────────────── parameter(s) │ │ ┌──── return type function plusTen(x: Int): Int { return x + 10 } └─┬──┘ └─ body2 อีกที
- โครงสร้างเฟรม
นี่เลยเป็นเหตุผลว่าทำไมเราไม่สามารถเรียกใช้ตัวแปรข้ามฟังก์ชันกันได้ เพราะมันอยู่คนละ stack frame กันไงล่ะ
และการที่มันชื่อว่า Stack นั่นก็แปลว่าการซ้อนกันของเฟรมไม่ได้จำกัดว่าต้องมีแค่ชั้นเดียวเท่านั้น จะมีกี่ชั้นก็ได้ (จนกว่าเมมจะเต็ม)
ลองมาดูอีกตัวอย่างที่ซับซ้อนมากขึ้นกัน
คราวนี้จะเป็นการเรียกฟังก์ชันแบบ 2 ชั้น โดยเราจะเพิ่มฟังก์ชันที่ชื่อว่า function mul(x, y){
var z = x * y
return z
}
main(){
var a = 2
var b = 5
var c = mul(a, b)
}1 ซึ่งเรียกใช้ฟังก์ชัน ┌─────────────────── function name
│ ┌────────────── parameter(s)
│ │ ┌──── return type
function plusTen(x: Int): Int {
return x + 10
} └─┬──┘
└─ body3 ต่ออีกทีหนึ่ง
การทำงานของโปรแกรมจะเริ่มที่ ┌─────────────────── function name
│ ┌────────────── parameter(s)
│ │ ┌──── return type
function plusTen(x: Int): Int {
return x + 10
} └─┬──┘
└─ body2 เหมือนเดิมจากนั้น --> function mul(x, y){
var z = x * y
return z
}
main(){
var a = 2
var b = 5
var c = mul(a, b)
}1 --> ┌─────────────────── function name
│ ┌────────────── parameter(s)
│ │ ┌──── return type
function plusTen(x: Int): Int {
return x + 10
} └─┬──┘
└─ body3 การทำงานของเมมโมรี่ก็จะเป็นแบบรูปข้างล่างนี่
ข้อสังเกตคือจำนวนเฟรมจะเพิ่มขึ้นหนึ่งชั้นเมื่อเรียกใช้งาน ┌─────────────────── function name
│ ┌────────────── parameter(s)
│ │ ┌──── return type
function plusTen(x: Int): Int {
return x + 10
} └─┬──┘
└─ body3 ในขณะที่เฟรมของฟังก์ชัน function mul(x, y){
var z = x * y
return z
}
main(){
var a = 2
var b = 5
var c = mul(a, b)
}1 ก็ยังคงค้างอยู่ในเมมโมรี่
นั่นเพราะฟังก์ชัน function mul(x, y){
var z = x * y
return z
}
main(){
var a = 2
var b = 5
var c = mul(a, b)
}1 นั้นยังทำงานไม่เสร็จและต้องรอค่าจากฟังก์ชัน ┌─────────────────── function name
│ ┌────────────── parameter(s)
│ │ ┌──── return type
function plusTen(x: Int): Int {
return x + 10
} └─┬──┘
└─ body3 ซะก่อน
สรุปว่าการเรียกฟังก์ชันซ้อนๆ กัน (Nested) นั้น โปรแกรมจะโปรเซสเฉพาะฟังก์ชันที่ทำงานอยู่ในตอนนั้นเท่านั้น (เป็นเฟรมบนสุดใน Stack Frame) ส่วนเฟรมอื่นๆ จะโดน pause เอาไว้ก่อนจนกว่าเฟรมบนจะทำงานเสร็จ