FP(02): Lambda Function and Closure ฟังก์ชันทันใจและพื้นที่ปิดล้อม!

developer

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

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


Category Theory & Lambda Calculus


ในบทที่แล้ว เราพูดถึง 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

ลองดูรูปข้างล่างนี่เพื่ออธิบายว่าทำไมเหตุการณ์แบบนี้ถึงแปลก

  1. เรียกใช้ฟังก์ชัน outer เกิดสโคปของตัวแปร x และ inner ขึ้น
  2. ฟังก์ชัน outer รีเทิร์นค่ากลับเป็นฟังก์ชัน inner --> ตามหลักการของฟังก์ชัน ถ้ามันรีเทิร์นค่ากลับแล้ว มันจะหยุดการทำงานทันที นั่นแปลว่าตัวแปร x ก็จะหายไปด้วยในจังหวะนี้!
  3. พอเราเรียกใช้ 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 สำหรับนับเลขแยกกันใช้ ไม่ผสมกัน

Next~

ในบทต่อไป เราจะกลับไปถูกถึงเรื่องที่ได้ใช้งานกับโลก imperative ก็บ้าง นั่นคือการใช้ map, filter, reduce และตัวอื่นๆ ที่น่าสนใจกัน

117 Total Views 3 Views Today
Ta

Ta

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

You may also like...