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

developer

บทความชุด: Functional Programming

รวมเนื้อหาเกี่ยวกับการเขียนโปรแกรมแนว Functional และหัวข้ออื่นๆ ที่เกี่ยวข้อง

- FP 00: บทนำ, ก้าวแรกสู่โลกแห่ง Functional Programming
- FP 01: Pure, First-Class, High-Order, พื้นฐานแห่ง Functional Programming
- FP 02: map, filter, reduce และผองเพื่อน, อาวุธหนักแห่ง Functional Programming
- FP 03: Recursive Function, เขียนลูปไม่ได้ทำยังไง มาเขียนฟังก์ชันเรียกตัวเองแทนกันเถอะ
- FP 04: โครสร้างแบบ Pair และ Either กับ List Comprehension การสร้างลิสต์ฉบับฟังก์ชันนอล
- FP 05: Immutable, Curry, Partial Function, Lazy Evaluation
- FP 06: Functor and Monad
- [บทความปี 2015] มารู้จักกับ Functional Programming สิ่งที่คุณต้องรู้ในตอนนี้กันเถอะ
- Function ทำงานยังไง?, ในมุมมองของโปรแกรมเมอร์สาย Imperative

Category Theory


Lambda Calculus

ถ้าพูดถึงคำว่า 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 เอาไว้ก่อนจนกว่าเฟรมบนจะทำงานเสร็จ

156 Total Views 3 Views Today
Ta

Ta

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

You may also like...

1 Response

ใส่ความเห็น

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