DEV Community

Ta
Ta

Posted on • Edited on • Originally published at tamemo.com

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

บทความจาก https://www.tamemo.com/post/158/fp-02-lambda-and-closure/

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

เนื้อหาในบทความนี้จะมีบางหัวข้อซ้ำซ้อนกับบทความที่เราเคยเขียนไปก่อนหน้านี้ในซีรีส์ JavaScript ฉบับมือใหม่ ตอนที่ 3 Js มองในมุมของ Functional Programming อยู่นิดหน่อยนะ ... แต่จะเอามาขยายความให้ลึกขึ้นหน่อย และไม่ได้โฟัสกับภาษา JavaScript นะ

ก่อนอื่นลองดูโค้ดนี้

function whenClick(){
  ...
}

button.onClick(whenClick)
Enter fullscreen mode Exit fullscreen mode

หากใครเคยเขียนโปรแกรมฝั่ง front-end หรือโปรแกรมที่ต้องมี ui ด้วย น่าจะคุ้นกับการเขียนโปรแกรมแบบนี้คือ Event-Driving

นั่นคือเราจะกำหนดว่าเมื่อปุ่มถูกกด จะให้ทำงานแบบฟังก์ชันที่กำหนดไว้

แต่ถ้า event ของเรามีเยอะมาก เช่นมีปุ่มมากกว่าหนึ่งปุ่ม เราอาจจะต้องเขียนโค้ดแบบนี้

function whenClick1(){
  ...
}

function whenClick2(){
  ...
}

function whenClick3(){
  ...
}

button1.onClick(whenClick1)
button2.onClick(whenClick2)
button3.onClick(whenClick3)
Enter fullscreen mode Exit fullscreen mode

เราจะพบว่า เราจะต้องสร้างฟังก์ชันเยอะมาก อาจจะทำให้อ่านยาก และที่สำคัญคือเราต้องตั้งชื่อฟังก์ชันทุกตัวด้วย (การตั้งชื่อตัวแปรหรือฟังก์ชัน สำหรับ
โปรแกรมเมอร์น่าจะรู้ๆ กันว่าเป็นงานที่เราไม่ค่อยอยากทำกัน ฮา)

Lambda สร้างฟังก์ชันทันใจ

แลมด้าหรือที่เรียกว่า "Anonymous Function" (ฟังก์ชันนิรนาม)

ก่อนจะอธิบายเรื่องแลมด้า ลองดูตัวอย่างโค้ดนี้ก่อน

var x = 10
print(x)
Enter fullscreen mode Exit fullscreen mode

สำหรับตัวแปร x ที่เราสร้างขึ้นมาเพื่อเก็บค่า 10 แล้วนำมาปริ๊นค่าต่อ

จากโค้ดข้างบนนี่ เราสามารถลดรูปให้เหลือแค่นี้ได้

print(10)
Enter fullscreen mode Exit fullscreen mode

นั่นแปลว่าเราสามารถลดการสร้างตัวแปร แล้วหันมาใช้ literal แทนได้ถ้าตัวแปรค่านั้นใช้งานแค่ครั้งเดียว

"literal" คือค่า value ที่ไม่ใช่ตัวแปร เช่น 10, "A" ซึ่งต่างจากตัวแปรหรือ variable เพราะค่าแบบ literal สามารถนำไปโปรเซสค่าได้เลย แต่สำหรับค่าแบบตัวแปรจะต้องมีการเข้าไปดึงค่ามาจาก memory ก่อนนำมาใช้งานได้

ดังนั้นถ้าเราใช้หลักการเดียวกันกับโค้ดฟังก์ชันตอนแรก

function whenClick(){
  ...
}

button.onClick(whenClick)
Enter fullscreen mode Exit fullscreen mode

แทนที่เราจะสร้างฟังก์ชันเตรียมไว้ก่อน (เหมือนประกาศตัวแปร) เราก็เอา function literal ไปใช้เป็น value แทนเลยก็ได้ ... แบบนี้

button.onClick(function whenClick(){
  ...
})
Enter fullscreen mode Exit fullscreen mode

ในเคสนี้ ถ้าเราไม่อยากตั้งชื่อให้ฟังก์ชัน ภาษาส่วนใหญ่ก็มักจะให้ละส่วนนี้ไว้ได้ เป็นแบบนี้

button.onClick(function(){
  ...
})
Enter fullscreen mode Exit fullscreen mode

การใช้ฟังก์ชันแบบนี้ เราเรียกว่าแลมด้า ซึ่งเป็นอักษรกรีกตัว λ โดยเป็นชื่อที่มาจากวิชา 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
}
Enter fullscreen mode Exit fullscreen mode

ตอนนี้เรามีฟังก์ชันอยู่ 2 ตัวคือ inner ที่ถูกสร้างอยู่ใน outer อีกทีนึง

โครงสร้างแบบนี้เราเรียกว่า outer ล้อมตัวแปร x และฟังก์ชัน inner เอาไว้ และรีเทิร์นค่ากลับเป็น high order function

โดยที่ฟังก์ชัน inner นั้นสามารถใช้ตัวแปร x ซึ่งเป็นสโคปของ outer ได้

Quiz?

ทีนี้ เราลองมาเดากันหน่อย ว่าถ้ารันโค้ดข้างล่างนี้ จะได้คำตอบเป็นอะไร

var func = outer()
print(func()) // ได้คำตอบเป็นอะไร?
Enter fullscreen mode Exit fullscreen mode

ถ้าใครอยากลองก็หยุดคิดดูก่อน เมื่อได้แล้วก็ดูคำตอบต่อไปได้เลย

var func = outer()

// ตอนนี้ func ก็คือฟังก์ชัน inner ที่ outer รีเทิร์นกลับมาให้นั่นแหละ

print(func()) // คำตอบคือ 10 นั่นเอง!
Enter fullscreen mode Exit fullscreen mode

อันนี้ไม่ยาก เมื่อเราเรียกใช้ 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 ใช้ไม่ได้ เพราะถือว่าเป็นสโคปของเมธอด
      }
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

สำหรับ 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
Enter fullscreen mode Exit fullscreen mode

เราทำฟังก์ชันสำหรับนับขึ้นมา แต่ข้อเสียของฟังก์ชันนี้คือมีการเรียกใช้ตัวแปร global ทำให้ไม่มีความเซฟเลยในการใช้ฟังก์ชัน

แต่เราสามารถสร้างฟังก์ชันครอบโค้ดชุดนี้เอาไว้ เพื่อกันไม่ให้ภายนอกเข้ามายุ่งกับตัวแปร count

แบบนี้

function createCountIt(){
  var count = 0
  function countIt() {
    count++
    return count
  }
  return countIt
}

var counter = createCountIt()

counter() // 1
counter() // 2
counter() // 3
Enter fullscreen mode Exit fullscreen mode

แล้วก็มีประโยชน์อีกอย่างด้วย นั่นคือเราสามารถสร้างฟังก์ชัน counter นี่ขึ้นมาได้หลายชุด

var firstCounter = createCountIt()
var secondCounter = createCountIt()

firstCounter()  --> 1
secondCounter() --> 1
secondCounter() --> 2
firstCounter()  --> 2
firstCounter()  --> 3
Enter fullscreen mode Exit fullscreen mode

ซึ่งทั้ง 2 ตัวก็จะมี count สำหรับนับเลขแยกกันใช้ ไม่ผสมกัน

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

Top comments (0)