Function ทำงานยังไง?, ในมุมมองของโปรแกรมเมอร์สาย Imperative

ถ้าพูดถึงคำว่า Function แล้ว ความคิดแรกทุกคนน่าจะนึกถึงวิชาคณิตศาสตร์ แต่ไหนๆ บล็อกนี้ก็เป็นบล็อก Programming แล้ว เราจะขอพูดถึงเรื่องฟังก์ชันในมุมมองของโปรแกรมเมอร์แทนละกัน

Function คืออะไร

นิยามของฟังก์ชันคือ "Black Box ที่รับค่าบางอย่างเข้าไป แล้วทำการคำนวณแล้วตอบผลลัพธ์กลับมา"

ในวิชาคณิตศาสตร์จะเทียบได้กับกราฟที่ไม่มีจุด x อยู่ในแนวตั้งเดียวกัน หรือก็คือสำหรับค่า x ทุกๆ x จะต้องมี y คู่กับมันแค่ค่าเดียวเท่านั้น (ถ้าไม่เข้าใจ ดูรูปประกอบข้างล่าง)

ถ้าเทียบกับภาษาโปรแกรม

  • x จะเทียบได้กับ input
  • y จะเทียบได้กับ output

เหตุผลที่มี output ได้แค่ค่าเดียวก็เพราะฟังก์ชันคือการส่งค่าไปคำนวณหรือประมวลผลอะไรบางอย่างแล้วส่งคำตอบกลับมา แล้วการส่งคำตอบกลับมาเนี่ย มันก็มีได้คำตอบเดียวยังไงล่ะ ... การคำนวณไม่สามารถให้คำตอบ 2 ค่าด้วยคำถามเดียวกันได้นะ (แม้ว่าจะคำนวณผิด ก็ต้องตอบมาค่าเดียวอยู่ดี)

เช่น ถามว่า

1 + 1 = ?

การตอบก็จะต้องตอบว่า 2 (ในกรณีที่ตอบถูก) หรือจะตอบ 3, 4, 10 อะไรก็ว่าไป (แน่นอนว่าตอบผิด)

แต่มันไม่สามารถมี output ออกมาสองค่าพร้อมกันได้นะ เช่นบอกว่า 1 + 1 = 2, 3 จะเป็นทั้ง 2 และ 3 ในเวลาเดียวกันเหรอ? แบบนี้ไม่ได้! ... หรือถึงจะออกมาค่าเดียว เช่นเรียก 1 + 1 = 2 แต่ลองเรียกอีกครั้งดันได้ 1 + 1 = 3 แบบนี้ก็ถือว่ามีหลาย output ไม่ได้เหมือนกัน

(คือใส่ค่าเข้าไปเหมือนกัน ต้องได้ค่าคำตอบเดิมออกมา จะเปลี่ยนไปเรื่อยๆ ไม่ได้)

หลักการใช้งานฟังก์ชันคือ

  1. Declaration: การประกาศฟังก์ชัน
  2. Call: การเรียกใช้งานฟังก์ชัน

เราจะต้อง declare ฟังก์ชันก่อนเรียกใช้งานเสมอ ไม่งั้นคอมพิวเตอร์ก็จะไม่รู้ว่าฟังก์ชันนี้ต้องทำงานยังไง

f(x) = x + 10
│ │    └─┬──┘
│ │      └─ body
│ └─── parameter(s)
└── function name

สำหรับภาษาโปรแกรมจะต้องเขียนละเอียดขึ้น ต้องมีการกำหนด type ว่า input, output เป็นอะไรด้วย (ฟังก์ชันในคณิตศาสตร์ไม่ต้องกำหนด เพราะใช้ number type เป็นหลักเท่านั้น)

            ┌─────────────────── function name
            │    ┌────────────── parameter(s)
            │    │         ┌──── return type
function plusTen(x: Int): Int {
    return x + 10
}          └─┬──┘
             └─ body

หน้าที่หลักของฟังก์ชัน ถ้าพูดในเชิงการเขียนโปรแกรม มันคือการรวมกลุ่ม code ที่ใช้งานบ่อยๆ เข้าไว้ด้วยกันเป็นก้อนเดียว ทำให้เวลาเขียนโปรแกรมขนาดใหญ่ เราไม่จำเป็นต้องเขียนโค้ดซ้ำๆ กันหลายๆ รอบ

หน้าที่ของฟังก์ชันอีกอย่างคือการสร้าง Encapsulation หรือการห่อหุ้ม code กลุ่มหนึ่งแล้วสร้างชื่อเรียก code กลุ่มนั้นแทน เช่นการสร้างฟังก์ชัน sin(), cos(), tan() หรือ print() ขึ้นมา เวลาเรียกใช้งานก็จะง่ายขึ้น เพราะเราไม่จำเป็นต้องรู้การทำงานภายในของฟังก์ชันเลย รู้แค่ฟังก์ชันจะทำอะไรออกมาให้ก็พอ

ฟังก์ชันทำงานอย่างไร, ในมุมมองของคอมพิวเตอร์

เพื่อให้เข้าใจฟังก์ชันจริงๆ เราต้องรู้ก่อนว่าเวลาฟังก์ชันทำงาน เกิดอะไรขึ้นภายในคอมพิวเตอร์บ้าง

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)
}

ตัวอย่างโค้ดข้างบนนี้...

  1. โค้ดเริ่มต้นทำงานที่ฟังก์ชัน main (ในขณะนี้ฟังก์ชัน mul ยังไม่ถูกเรียกให้ทำงาน ดังนั้นตัวแปรทั้งหมดจะยังไม่ถูกจองพื้นที่ในเมโมรี่) --> ฟังก์ชันเริ่มทำงาน จะเกิดการจองพื้นที่ในเมโมรี่ เรียกว่า frame ของ main
  2. การประมวลผลจะทำงานเรียงตามบรรทัด เริ่มจากการกำหนดค่า a, b ใน 2 บรรทัดแรก --> variable และ value ก็จะถูกจองลงไปในเมโมรี่ (ดูรูปประกอบข้างล่าง)
  3. ต่อมา, ที่บรรทัดที่ 3 ของ main มีการ call ฟังก์ชัน mul เกิดขึ้น --> มีการเปิดเฟรมใหม่ของฟังก์ชัน mul ขึ้นมาข้างบนเฟรมของ main อีกที
  4. โครงสร้างเฟรม

นี่เลยเป็นเหตุผลว่าทำไมเราไม่สามารถเรียกใช้ตัวแปรข้ามฟังก์ชันกันได้ เพราะมันอยู่คนละ stack frame กันไงล่ะ

และการที่มันชื่อว่า Stack นั่นก็แปลว่าการซ้อนกันของเฟรมไม่ได้จำกัดว่าต้องมีแค่ชั้นเดียวเท่านั้น จะมีกี่ชั้นก็ได้ (จนกว่าเมมจะเต็ม)

ลองมาดูอีกตัวอย่างที่ซับซ้อนมากขึ้นกัน
คราวนี้จะเป็นการเรียกฟังก์ชันแบบ 2 ชั้น โดยเราจะเพิ่มฟังก์ชันที่ชื่อว่า pow2 ซึ่งเรียกใช้ฟังก์ชัน mul ต่ออีกทีหนึ่ง

function mul(x, y){
    var z = x * y
    return z
}

function pow2(x){
    var result = mul(x, x)
    return result
}

main(){
    var a = 5
    var b = pow2(a)
}

การทำงานของโปรแกรมจะเริ่มที่ main เหมือนเดิมจากนั้น --> pow2 --> mul การทำงานของเมมโมรี่ก็จะเป็นแบบรูปข้างล่างนี่

ข้อสังเกตคือจำนวนเฟรมจะเพิ่มขึ้นหนึ่งชั้นเมื่อเรียกใช้งาน mul ในขณะที่เฟรมของฟังก์ชัน pow2 ก็ยังคงค้างอยู่ในเมมโมรี่
นั่นเพราะฟังก์ชัน pow2 นั้นยังทำงานไม่เสร็จและต้องรอค่าจากฟังก์ชัน mul ซะก่อน

สรุปว่าการเรียกฟังก์ชันซ้อนๆ กัน (Nested) นั้น โปรแกรมจะโปรเซสเฉพาะฟังก์ชันที่ทำงานอยู่ในตอนนั้นเท่านั้น (เป็นเฟรมบนสุดใน Stack Frame) ส่วนเฟรมอื่นๆ จะโดน pause เอาไว้ก่อนจนกว่าเฟรมบนจะทำงานเสร็จ

289 Total Views 27 Views Today
Ta

Ta

สิ่งมีชีวิตตัวอ้วนๆ กลมๆ เคลื่อนที่ไปไหนโดยการกลิ้ง .. ถนัดการดำรงชีวิตโดยไม่โดนแสงแดด
ปัจจุบันเป็น Senior Software Engineer อยู่ที่ Centrillion Technology
งานอดิเรกคือ เขียนโปรแกรม อ่านหนังสือ เขียนบทความ วาดรูป และ เล่นแบดมินตัน

You may also like...

2 Responses

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องที่ต้องการถูกทำเครื่องหมาย *