Programming ကို လေ့လာကြသူအများစုဟာ Procedural Programming (သို့မဟုတ်) Object-Oriented Programming (OOP) ရေးနည်းတွေနဲ့ စတင်လေ့လာခဲ့ကြတာ များပါတယ်။ System Level Language အများစုဟာ Procedural Language တွေဖြစ်ကြပါတယ်။ C, Go, Rust, Zig စသည်ဖြင့်။ Application Level Language အများစုကတော့ OOP Language တွေများပါတယ်။ Java, C#, Python, Ruby, PHP, JavaScript စသည်ဖြင့်။
Functional Programming ကိုတော့ လေ့လာသူတွေ ထိတွေ့မှုနည်းကြပါတယ်။ ဒါပေမယ့် ကနေ့ခေတ်မှာ Functional Programming ဟာ အလွန်အရေးပါတဲ့ ရေးနည်းတစ်ခုဖြစ်နေသလို စောစောက ပြောခဲ့တဲ့ Language အများစုကလည်း Functional Programming ရေးနည်းကို အပြည့်မရပေမယ့် နည်းနည်းတော့ Support လုပ်ကြပါတယ်။
Functional Programming ရေးနည်းကိုပဲ အဓိကထားတဲ့ Language တွေလည်း ရှိကြပါတယ်။ Lisp, Haskell, Erlang, Elixir, Scala, F# စသည်ဖြင့်။
ကနေ့ခေတ်မှာ Functional Programming ရဲ့ အခန်းကဏ္ဍဟာ တစ်နေ့တစ်ခြား ပိုအရေးပါလာတဲ့အတွက် မူလက Procedural Language တွေ OOP Language တွေနဲ့ လေ့လာခဲ့ကြသူတွေဟာ Functional Programming ကိုလည်း အသုံးချနိုင်ဖို့ လိုအပ်လာကြပါတယ်။
အဲ့ဒီမှာ ပြဿနာစတာပါပဲ။ ရေးနည်းသာမက စဉ်းစားပုံစဉ်းစားနည်းပါ ပြောင်းလဲသွားတဲ့အတွက် Functional Programming က အရမ်းခက်သလို ဖြစ်နေကြပါတယ်။ စကတည်းက Functional Language တွေနဲ့ လေ့လာခဲ့ကြရင်တော့ ဒီလောက်ခက်မယ် မထင်ပါဘူး။ ဒါပေမယ့် အရင်သိထားတဲ့ စဉ်းစားပုံစဉ်းစားနည်းတွေကနေ လုံးဝပြောင်းပြီး စဉ်းစားအလုပ်လုပ်ရတဲ့အခါ ခက်သင့်တာထက် ခက်သွားတော့တာပါပဲ။
ဒီသင်ခန်းစာမှာ JavaScript ကိုအသုံးပြုပြီး Functional Programming ရဲ့ အခြေခံသဘောသဘာဝတွေကို ကုဒ်နမူနာလေးတွေနဲ့ ရှင်းပြမှာဖြစ်ပါတယ်။ ဆက်လက်ဖတ်ရှုဖို့အတွက် JavaScript အခြေခံရှိပြီး ဖြစ်ဖို့လိုပါတယ်။ လိုအပ်ရင် JavaScript - လိုတိုရှင်း စာအုပ်ကို ဖတ်ရှုပြီး အခြေခံကနေ စတင်လေ့လာနိုင်ပါတယ်။
Functional Programming ကို အကျဉ်းချုပ်အားဖြင့်
Pure Function တွေကို အသုံးပြုပြီး ပရိုဂရမ်ရေးသားနည်း
လို့ မှတ်နိုင်ပါတယ်။ ဒါကြောင့် Pure Function ဆိုတာ Functional Programming ရဲ့ အဓိက အသက်ပါပဲ။ Pure Function ဆိုတာ ဒီလိုပါ။
-
ပေးလိုက်တဲ့ Input (Parameter) တူရင် ပြန်ရမယ့် Output (Return Value) အမြဲတမ်းတူတဲ့ Function ဖြစ်ပါတယ်။
-
အခြား Function တွေ လုပ်ဆောင်ချက်တွေပေါ်မှာ Side-Effect ခေါ် သက်ရောက်မှုတွေမရှိဘဲ သီးခြားအလုပ်လုပ်နိုင်တဲ့ Function ဖြစ်ပါတယ်။
ဒီကုဒ်နမူနာကိုကြည့်ပါ။
function add(a, b) {
return a + b
}
add(1, 2) // => 3
add(1, 2) // => 3ဒါဟာ Pure Function ဖြစ်ပါတယ်။ ပေးလိုက်တဲ့ Parameter တူနေသ၍ ပြန်ရတဲ့ ရလဒ်အမြဲတမ်းတူညီမှာ ဖြစ်တဲ့အတွက်ကြောင့်ပါ။
let value = 0
function increase() {
++value
return value
}
increase() // => 1
increase() // => 2ဒီ Function ကတော့ Pure Function မဟုတ်တော့ပါဘူး။ ပြန်ရမယ့် ရလဒ်က ပြောင်းနေတဲ့အတွက်ကြောင့် ဖြစ်သလို Function ကို ခေါ်ယူလိုက်တိုင်းမှာ Global Variable ဖြစ်တဲ့ value ပေါ်မှာ သက်ရောက်ပြောင်းလဲတဲ့ Side-Effect ရှိနေမှာမို့လို့ပါ။
Functional Programming ဆိုတာဟာ Side-Effect တွေ နည်းနိုင်သမျှ အနည်းဆုံးနဲ့ Pure Function တွေကို အသုံးပြုပြီး ရေးသားရတဲ့ ရေးနည်းပဲဖြစ်ပါတယ်။
Functional Programming ရဲ့ သဘောသဘာဝတွေထဲက အဓိကအကျဆုံးနဲ့ နေရာတိုင်းမှာ အသုံးပြုရမှာကတော့ Higher-Order Function (HOF) ဖြစ်ပါတယ်။ HOF ဆိုတာ Function ကို လက်ခံနိုင်တဲ့ Function (သို့မဟုတ်) Function ကို ပြန်ပေးနိုင်တဲ့ Function ဖြစ်ပါတယ်။ ဒီလိုပါ -
function times(m) {
return function(v) {
return v * m
}
}
const double = times(2)
const triple = times(3)
double(2) // => 4
triple(2) // => 6နမူနာအရ times() Function က Function တစ်ခုကို Return ပြန်ပေးပါတယ်။ const double = times(2) ဆိုတဲ့ Statement က ဘာလုပ်သွားမှာလဲ စဉ်းစားကြည့်ပါ။ times() က Function ကို ပြန်ပေးတဲ့အတွက် double ကလည်း Function ဖြစ်သွားမှာပါ။ အလားတူပဲ triple ကလည်း Function ပဲ ဖြစ်နေမှာပါ။
ဒါပေမယ့် double ရဲ့ m Value က 2 ဖြစ်ပြီး triple ရဲ့ m Value ကတော့ 3 ဖြစ်နေမှာပါ။ စမ်းကြည့်လိုက်ရင် double() က ပေးလိုက်တဲ့ တန်ဖိုးကို နှစ်ဆတိုးပေးပြီး triple() က ပေးလိုက်တဲ့တန်ဖိုးကို သုံးဆတိုးပေးသွားမှာပါ။
ဒီနည်းနဲ့ Function ဆိုတာ ရေးထားတဲ့အတိုင်း အမြဲတမ်း ပုံသေအလုပ်လုပ်တယ် ဆိုတာမျိုး မဟုတ်တော့ဘဲ၊ လိုအပ်သလို ပြန်လည်အသုံးပြုလို့ရတဲ့၊ ပြောင်းလဲအသုံးပြုလို့ရတဲ့ Function တွေကို ရသွားပါတယ်။
Object-Oriented Programming မှာဆိုရင် Object တွေမှာ ကိုယ်ပိုင် Property ရှိကြသလိုပဲ၊ Functional Programming မှာလည်း Function တွေမှာ ကိုယ်ပိုင် State Data ရှိကြပါတယ်။ အထက်က HOF မှာလည်း ဒီသဘောကို တွေ့ခဲ့ပြီးဖြစ်ပါတယ်။
စောစောက နမူနာရေးပြလိုက်တဲ့ increase() Function ကို Side-Effect မရှိအောင် ပြောင်းရေးကြည့်ပါမယ်။
const increase = () => {
let count = 0
return () => ++count
};
const counter = increase()
counter() // => 1
counter() // => 2နမူနာအရ increase() Function ဟာ Global Variable Value တွေကို သက်ရောက် ပြောင်းလဲစေတဲ့ Side-Effect မရှိတဲ့ Pure Function ဖြစ်ပါတယ်။ ဒါပေမယ့် နည်းနည်းဂရုစိုက်ပါ။ သူ့မှာ သူ့ကိုယ်ပိုင် count Variable ရှိနေပါတယ်။ Function State လို့ ဆိုနိုင်ပါတယ်။ ပြီးတော့သူက Function တစ်ခုကို ပြန်ပေးထားပါတယ်။ ဒါကြောင့် const counter = increase() ဆိုတဲ့ Statement အရ counter ဟာ increase() က ပြန်ပေးတဲ့ Function ဖြစ်သွားပါတယ်။
ဒါပေမယ့် counter() ကို ခေါ်ယူလိုက်တိုင်းမှာ တန်ဖိုးကို (၁) တိုးပြီး ပြန်ရနေတာကို တွေ့ရပါလိမ့်မယ်။ ဒီသဘောကို Closure လို့ခေါ်ပါတယ်။
ကိုယ်ပိုင်တန်ဖိုးကို ကိုယ့် Scope ထဲမှာပဲ State အနေနဲ့ မှတ်သားထားပြီး၊ ကိုယ့် Scope ရဲ့ ပြင်ပမှာ ခေါ်ယူအသုံးပြုရင်တောင် အဲ့ဒီလို မှတ်သားထားတဲ့ State တန်ဖိုးနဲ့ ဆက်လက် အလုပ်လုပ်ပေးနိုင်တဲ့သဘော ဖြစ်ပါတယ်။
ဒီနည်းနဲ့ Global Side-Effect မရှိဘဲ ရေရှည်မှာ အမှားပိုနည်းပြီး ပိုကောင်းတဲ့ ရေးနည်းရေးဟန်ကို ရရှိသွားပါတယ်။
ဂဏန်းနှစ်လုံးကို ပေါင်းပေးတဲ့ ဒီ Function ရိုးရိုးလေးကို နောက်တစ်ကြိမ် ပြန်ကြည့်ပါ။
const add = (a, b) => a + b
add(1, 2) // => 3add ဟာ Function တစ်ခုဖြစ်ပြီး a နဲ့ b ဆိုတဲ့ Parameter (၂) ခုကို လက်ခံပါတယ်။ ပြီးတဲ့အခါ a နဲ့ b ကိုပေါင်းပြီး ပြန်ပေးပါတယ်။ ဒီလို Parameter (၂) ခုလက်ခံတဲ့ Function ကို တစ်ကြိမ်မှာ တစ်ခုပဲလက်ခံတဲ့ Function ဖြစ်သွားအောင် ပြောင်းရေးကြည့်ပါမယ်။
const add = (a) => (b) => a + b
const addFive = add(5)
addFive(10) // => 15
addFive(20) // => 25add ဟာ Parameter a တစ်ခုထဲကိုပဲ လက်ခံတဲ့ Function ဖြစ်သွားတာပါ။ ဒါပေမယ့် သူက Parameter b ကိုလက်ခံတဲ့ Function တစ်ခု ပြန်ပေးပါတယ်။ ဒါကြောင့် const addFive = add(5) လို့ရေးသားလိုက်တဲ့အခါ addFive ဟာ Parameter b ကို လက်ခံအလုပ်လုပ်မယ့် Function ဖြစ်သွားပါတယ်။ လက်ရှိ a ရဲ့ တန်ဖိုးကတော့ 5 ဖြစ်နေမှာပါ။
နည်းနည်းမျက်စိလည်သွားနိုင်ပါတယ်။ ရှင်းလင်းချက်ထက် ကုဒ်ကိုပဲ နှစ်ခါသုံးခါ ပြန်ဖတ်ကြည့်ပါ။
addFive(10) လို့ ခေါ်ယူလိုက်တဲ့အခါ b တန်ဖိုးက 10 ဖြစ်သွားလို့ မူလ a တန်ဖိုး 5 နဲ့ပေါင်းလိုက်တဲ့အခါ အဖြေရလဒ် 15 ကိုပြန်ရပါတယ်။ addFive(20) လို့ခေါ်ယူလိုက်တဲ့အခါ အဖြေရလဒ် 25 ကို ပြန်ရသွားပါတယ်။
ဒီနည်းနဲ့ Parameter အများအပြားလက်ခံတဲ့ Function ကို တစ်ကြိမ်မှာ Parameter တစ်ခုပဲ လက်ခံတဲ့ Function တွေဖြစ်အောင် ပြောင်းရေးတဲ့နည်းကို Currying လို့ ခေါ်တာပါ။ ရေးနည်းပဲ ပြောချင်ပါတယ်။ ဘယ်လိုနေရာမျိုးတွေမှာ အသုံးဝင်သလဲဆိုတာ အရမ်းစိတ်ဝင်စားဖို့ ကောင်းပေမယ့် ဒီနေရာမှာ ထည့်မပြောနိုင်တော့ပါဘူး။
Functional Programming မှာ အစဉ်အလာအသုံးပြုနေကြ If-Condition တွေ Loop တွေကို သုံးကြလေ့မရှိပါဘူး။ If-Condition အစား Ternary Operator တို့ Logical And Operator တို့ Conditional Function တို့ကို အသုံးပြုကြပါတယ်။ ဥပမာ -
const isOdd = v => v % 2
const u18 = users =>
users.filter(u => u.age < 18)
const rgb = code => {
const colors = { R: 'Red', G: 'Green' }
return colors[code] || 'Unknown'
}ဒါက ပုံမှန် If-Condition သုံးပြီးရေးရလေ့ရှိတဲ့ ကုဒ်တွေကို If-Else တွေမပါဘဲ အလားတူရလဒ်ရအောင် Function တွေနဲ့ပဲ ရေးလိုက်တာပါ။
Loop တွေအစား Recursion ကို အသုံးပြုကြပါတယ်။ Recursion ဆိုတာ ကိုယ့်ကိုယ်ကို ပြန်ခေါ်တဲ့ Function တစ်မျိုးပါပဲ။
const sum = arr =>
arr.length && arr[0] + sum(arr.slice(1))
const nums = [1, 2, 3, 4, 5]
sum(nums) // => 15ဒီ Function က For-While-Loop တွေမပါဘဲ Recursion ကိုသုံးပြီး ပေးလာတဲ့ Number Array ကို ပေါင်းပေးလိုက်တာပါ။ လက်တွေ့မှာ ဒီလိုကိုယ့်ဘာသာ ရေးစရာမလိုပါဘူး။ arr.reduce() လို လုပ်ဆောင်ချက်မျိုးကို သုံးနိုင်ပါတယ်။ နမူနာရအောင်ရေးပြလိုက်တာပါ။
နမူနာအရ && And Operator နဲ့ arr.length ကိုစစ်ပြီး Array ရှိတယ်ဆိုမှ အလုပ်လုပ်ပါတယ်။ မရှိရင် undefined ကို Return ပြန်ပြီး ထပ်ခါထပ်ခါ အလုပ်လုပ်နေတဲ့ Recursion က ရပ်သွားမှာပါ။ Function Name က sum ဖြစ်ပြီး သူ့ထဲမှာ သူ့ကိုယ်သူ ပြန်ခေါ်ထားတာကို တွေ့ရနိုင်ပါတယ်။
အဓိကလုပ်ဆောင်ချက်ဖြစ်တဲ့ arr[0] + sum(arr.slice(1)) ရဲ့ ပြန်ခေါ်တိုင်းမှာ အဆင့်လိုက် အလုပ်လုပ်သွားပုံက ဒီလိုပါ -
// arr = [1, 2, 3, 4, 5]
arr[0] + sum(arr.slice(1))
// => 1 + sum([2, 3, 4, 5])
// => 3 + sum([3, 4, 5])
// => 6 + sum([4, 5])
// => 10 + sum([5])
// => 15နောက်ထပ်အရေးပါတဲ့ လုပ်ဆောင်ချက်ကတော့ Function တွေကိုပေါင်းစပ်ပြီး Function တစ်ခုကို ဖန်တီးယူတဲ့ Composition ဖြစ်ပါတယ်။ ဒီလိုပါ -
const lower = str => str.toLowerCase()
const trim = str => str.trim()
const compose = (a, b) => str => a(b(str))
const clean = compose(lower, trim)
clean(' Hello') // => helloနမူနာအရ lower Function က ပေးလာတဲ့ String ကို Lower Case ပြောင်းပေးပြီး trim Function က ပေးလာတဲ့ String ကနေ မလိုအပ်တဲ့ Whitespace တွေကို ဖယ်ရှားပေးပါတယ်။ ပြီးတဲ့အခါ compose Function က Function A နဲ့ B ကို လက်ခံပြီး တွဲသုံးပေးလိုက်တာပါ။
ဆက်လက်ပြီး compose() ရဲ့အကူအညီနဲ့ clean Function ကို တည်ဆောက်ပါတယ်။ တနည်းအားဖြင့် lower() နဲ့ trim() ကို ပေါင်းစပ်ပြီး အလုပ်လုပ်ပေးနိုင်တဲ့ Function ပါ။
Functional Language စစ်စစ်တွေမှာ Currying လုပ်တာတွေ Compose လုပ်တာတွေကို ကိုယ်ဘာသာ လုပ်စရာမလိုပါဘူး။ နမူနာမှာလို compose Function ကို ကိုယ်ဘာသာ ရေးစရာမလိုဘူးလို့ ပြောတာပါ။ JavaScript မှာ Functional Programming Feature တွေ အတော်လေး ကောင်းမွန်ပေမယ့် Functional Language စစ်စစ်တော့ မဟုတ်ပါဘူး။ ဒါကြောင့် ကိုယ့်ဘာသာ ရေးလိုက်တာပါ။ ကိုယ့်ဘာသာ မရေးချင်ရင် UnderscoreJS လို နည်းပညာမျိုးကို သုံးလို့လည်း ရနိုင်ပါတယ်။
Function တစ်ခုပေါ်မှာ အခြေခံပြီး နောက်ထပ် Function တစ်ခုကို တည်ဆောက်တဲ့နည်းဖြစ်ပါတယ်။ ဒီလိုပါ -
function add(a, b, c) {
return a + b + c
}
const sum = _.partial(add, 1, 2)
sum(10) // => 13နမူနာအရ add() Function က a, b, c ဆိုတဲ့ Parameter (၃) ခုကို လက်ခံပါတယ်။ sum Function ကတော့ partial() Function ရဲ့အကူအညီနဲ့ add ကို ကူးယူလိုက်တာပါ။ အဲ့ဒီလို ကူးယူစဉ်မှာ a အတွက် 1 နဲ့ b အတွက် 2 ကို တခါထဲ အသေထည့်ပေးလိုက်ပါတယ်။ ဒါကြောင့် ပြန်ခေါ်တဲ့အခါ c ကိုပဲ ပေးဖို့လိုပါတော့တယ်။
partial() Function က JavaScript မှာ မရှိပါဘူး။ ကိုယ့်ဘာသာထည့်မရေးချင်တော့လို့ UnderscoreJS က Function ကိုသုံးပြီး နမူနာပေးလိုက်တာပါ။
နောက်ဆုံးတစ်ခုအနေနဲ့ Lazy Evaluation ခေါ် ချက်ချင်းအလုပ်မလုပ်ဘဲ လိုအပ်တဲ့အချိန်ရောက်တော့မှ အလုပ်လုပ်တဲ့ သဘောသဘာဝတစ်ခုကို ထည့်ပြောပါမယ်။
const lazyMap = (arr, fn) => {
return () => arr.map(fn)
}
const nums = [1, 2, 3]
const double = lazyMap(nums, n => n * 2)
double() // => [2, 4, 6]ပုံမှန်အားဖြင့် Array တွေ map လုပ်ချင်ရင် အခုလို တိုက်ရိုက်လုပ်နိုင်ပါတယ် - arr.map(fn)။ ဒါဆိုရင် map() ကပေးလိုက်တဲ့ fn Function ကို arr Item တွေပေါ်မှာ ချက်ချင်း အလုပ်လုပ်ပေးလိုက်မှာပါ။
နမူနာအရ lazyMap ခေါ် Function တစ်ခုကြားခံထားပါတယ်။ ဒါကြောင့် အဲ့ဒီ lazyMap ကို အသုံးပြုပြီး double Function ကို တည်ဆောက်စဉ်မှာ ချက်ချင်းအလုပ်မလုပ်တော့ပါဘူး။ double Function ကို ခေါ်လိုက်တော့မှသာ စတင်အလုပ်လုပ်ပါတယ်။
ဒီနည်းနဲ့ မလိုသေးရင် အလုပ်မလုပ်သေးဘဲ လိုတော့မှ ထအလုပ်လုပ်တဲ့ ပိုမြန်တဲ့ကုဒ်ကို ရရှိသွားပါတယ်။ ဒီထက်ပိုမြန်ချင်သေးရင် ရလဒ်ကို Cache လုပ်ထားတဲ့နည်းကို သုံးနိုင်ပါသေးတယ်။ ဒီလိုပါ -
const lazyMap = (arr, fn) => {
let evaluated = false
let result = []
return () => {
return (
evaluated || (
(result = arr.map(fn)),
(evaluated = true)
), result
)
}
}
const nums = [1, 2, 3]
const double = lazyMap(nums, n => n * 2)
double() // => [2, 4, 6]
double() // => [2, 4 ,6] from cache နမူနာအရ arr.map() ဟာချက်ခြင်းအလုပ်မလုပ်ဘဲ လိုတဲ့အချိန်မှ ထအလုပ်လုပ်ယုံသာမက၊ တစ်ကြိမ်အလုပ်လုပ်ပြီးရင် ရလဒ်ကို result အနေနဲ့ မှတ်ထားပါသေးတယ်။ ဒါကြောင့် နောက်ထပ်ခေါ်တဲ့အခါ ထပ်အလုပ်မလုပ်တော့ဘဲ မှတ်ထားတဲ့ result ကို ပြန်ပေးလိုက်မှာဖြစ်ပါတယ်။ ဒါကြောင့် မလိုအပ်ဘဲ ထပ်ခါထပ်ခါ အလုပ်လုပ်စရာမလိုတော့ပါဘူး။
ဒီလိုနည်းတွေကို အသုံးချရတာ Function Programming ရေးနည်းနဲ့ဆိုရင် ပိုသဘာဝကျနေတဲ့အတွက် နောက်ပိုင်းမှာ အသုံးပြုမှု တွင်ကျယ်သထက် တွင်ကျယ်လာကြတာပဲဖြစ်ပါတယ်။
နောက်ထပ် ပို Advanced ဖြစ်တဲ့ Funtor တို့ Monad တို့လို သဘောသဘာဝတွေ ကျန်ပါသေးတယ်။ Immutable Data အကြောင်းလည်း ကြည့်ဖို့လိုပါလိမ့်ဦးမယ်။ ဒါပေမယ့် အခုဖော်ပြခဲ့သလောက် သိပြီဆိုရင်ကိုပဲ၊ စတင်အသုံးပြုလို့ ရနေပါပြီ။
အထက်မှာ ပြောခဲ့သလိုပဲ JavaScript မှာ Functional Programming Feature တွေ အတော်လေး ကောင်းမွန်ပေမယ့် Functional Language စစ်စစ်တော့ မဟုတ်ပါဘူး။ ဒါက လောလောဆယ် ပိုတောင်အဆင်ပြေသေးတယ်လို့ ဆိုချင်ပါတယ်။ Functional Language စစ်စစ်တွေကို တိုက်ရိုက်သွားလေ့လာရင် ရေးနည်းတွေ လုံးဝကွဲပြားသွားလို့ ဒီထက်ပိုပြီး နားလည်ရခက်နေပါလိမ့်မယ်။ ဒီလိုကြားခံလေးနဲ့ အရင်လေ့လာလိုက်တဲ့အခါ၊ လိုအပ်လို့ Functional Language စစ်စစ်တွေကို ထပ်လေ့လာရရင်လည်း အများကြီး အဆင်ပြေသွားပါလိမ့်မယ်။
အခုတော့ 100% Functional Programming ဖြစ်စရာမလိုသေးဘဲ ကိုယ်နားလည်လက်စ ရေးနည်းတွေနဲ့ အရမ်းမကွာတဲ့ Functional Programmin ကုဒ်တွေကို ရေးလို့ရသွားပြီပဲ ဖြစ်ပါတယ်။