สอน Functional Programming แบบละเอียด

NSLog0
NSLog0
Feb 15, 2020 · 8 min read

Introduction

บทความนี้สอนการเขียนโปรแกรมในแบบ Functional Programming (FP) และเทคนิคต่างๆ ที่จำเป็นต้องรู้ … Functional Programming นั้นคือการเขียนโปรแกรมโดยการเอาเทคนิคการทำ Composition (การประกอบร่าง) มาใช้ควบคู่กับ Pure function โดยข้อควรจำคือต้องหลีกเลี่ยงการ Share state, การแก้ไขข้อมูล (Mutate Data) และการเขียนโค้ดที่เกิด Side effect. สำหรับการเขียนโค้ดเราจะเขียนแบบ Declarative style แทนการเขียนแบบ Imperative Style

  • Impure function
  • Shared state
  • Declarative vs Imperative style
  • Composition
  • Currying
  • Partial
  • Curry and Partial in Mathematic
  • Point-free programming
  • Avoiding Mutate Data, Immutability
  • Avoiding Side effect

Pure function

การเขียนคอมพิวเตอร์ฟังก์ชันโดยการเอาแนวคิดของฟังก์ชันทางคณิตศาสตร์มาใช้ในการประมวลผลข้อมูล โดยปกติจะมีกฏควรจำให้ขึ้นใจ 2 ข้อคือ:

  • Its evaluation has no side effects กล่าวคือฟังก์ชันใดๆ ที่สร้างขึ้นมาต้องไม่มีการเปลี่ยนแปลง State หรือของด้านนอกฟังก์ชัน หรือพูดง่ายๆ ว่าฟังก์ชันที่สร้างมาจะต้องไม่แก้ไขตัวแปรด้านนอกฟังก์ชัน เพื่อไม่ให้กระทบกับการทำงานของฟังก์ชันอื่น

โจทย์

Given f(x) = 2x - 1, find f(2)f(2) = 2(2) - 1 
= 4 - 1
= 3
∴ f(2) = 3
one-to-one
many-to-one
one-to-many

ลองแปลงโจทย์คณิตศาสตร์ให้เป็น JavaScript

const f = (x) => 2 * x - 1console.log(f(2)) // 3

Impure function

ตรงกันข้ามกับ Pure function หรือพูดง่ายๆ ว่าแหกกฏการเขียนทุกอย่างเลย ตัวอย่างเช่น การนำเอาโค้ดด้านบนมาแก้ให้เป็น Impure function

const f = (x) => { 
init += 2 * x - 1
}
let init = 2f(2)
console.log(init) // 5
f(2)
console.log(init) // 8

Shared state

Shared state เป็นแนวคิดนึงในการเขียนโปรแกรมแบบ OOP ซึ่งคือการสร้าง Properties ใหักับ Object (ผมจะไม่ลงรายละเอียด OOP นะครับ เข้าใจว่าทุกคนน่าจะรู้เรื่อง Properties และ Behaviour แล้ว) ก่อนอื่นมาว่าด้วยเรื่องของ Data type ก่อน ปกติเราจะมี Data type ที่เป็น Primitives/References ในทุกเกือบๆ ภาษาคอมพิวเตอร์อยู่แล้ว ในบทความนี้ผมจะพูดถึง JavaScript เป็นหลัก

  • Number
  • String
  • Null
  • Undefined
  • Symbol
  • Arrays
let greeting = 'Hello'
let saySometing = 'JS very nice lang'saySometing = greeting
console.log(saySometing) // Hello
greeting = 'Hi!!'
console.log(greeting) // Hi!!
saySometing = 'WoW'
console.log(saySometing) // WoW
console.log(greeting) // Hi!!
const person = { age: 20 }person.age = 30console.log(person) // { age: 30 }
const updateAge = (p, newAge) => { 
p.age = newAge
return p
}
const person = { age: 20 }const newPerson = updateAge(person, 30)console.log(newPerson) // { age: 30 }
console.log(person) // { age: 30 }
newPerson.age = 31console.log(newPerson) // { age: 31 }
console.log(person) // { age: 31 }

Declarative vs Imperative style

การเขียน Functional Programming เราจะใช้การเขียนโปรแกรมแบบ Declarative เป็นการเขียนโค้ดที่ไม่มี Control flow ส่วนการเขียนแบบ Imperative จะเขียนเป็นคำสั่งเพื่อบอกว่าระบบของเราทำอะไรบ้างไล่ลำดับการทำงานลงมาและมี Control flow

Imperative

const number = [1, 2, 3, 4, 5]for (let i = 0; i < number.length; i++) {
number[i] = number[i] * 2;
}
console.log(number) // [2, 4, 6, 8, 10]

Declarative

const number = [1, 2, 3, 4, 5]
const sq = number.map(n => n * 2)
console.log(sq) // [2, 4, 6, 8, 10]

Composition

ปกติแล้วตามที่เราเคยเรียนเรื่อง OOP กันมาหรือแม้ก็ทั้ง ถ้าเคยได้ยินเรื่อง SOLID เราจะมีการพูดที่เหมือนกันอย่างนึงคือ Single responsibility คือ 1 Class หรือ 1 Function ต้องทำแค่ 1 อย่าง (การเขียนโปรแกรมที่ถูกต้องจะแบบนี้เสมอ) พอเกิดการกระทำแค่อย่างเดียวเราจึงต้องเอาฟังก์ชันต่างๆ มาประกอบกันให้เป็น Pipeline เพื่อทำให้การทำงานมันครบและจึงนำเอา return value ของฟังก์ชันมาเป็น Parameter ของฟังก์ชันถัดๆ ไปเพื่อให้มันได้ return value จนเราสามารถเอาไปใช้ต่อได้

const number = [1, 3, 4, 6, 7, 8, 10] // [2, 6, 8, 12, 14, 16, 20]numbers.map(n => n * 2).filter(n => n > 10) // [12, 14, 16, 20]

R.pipe(…)

หนึ่งในวิธีการทำ Composition ที่ผมจะแนะนำคือ pipe ซึ่งการทำงานของมันจะเริ่มจากซ้ายไปขวา เหมือนการอ่านหนังสือเลยครับ (ผมชอบใช้ตัวนี้มันอ่านง่ายและเป็นสิ่งที่คุ้นเคยอยู่แล้ว)

import * as R from 'ramda'const upperCase = (o) => o.map(_o => ({ book: R.toUpper(_o.book) }))
const payload = '[{"book":"js"},{"book":"lamda"}]'
const pipeline = R.pipe(
JSON.parse,
upperCase,
console.log
)
pipeline(payload) // [{"book":"JS"},{"book":"LAMDA"}]
const upperCase = (data) => { 
const cloned = JSON.parse(data)
for(let i = 0; i < data.length; i++) {
cloned[i].book = cloned[i].book.toUpperCase()
}

return cloned
}
const payload = '[{"book":"js"},{"book":"lamda"}]'
const result = upperCase(obj)
console.log(result) // [{"book":"JS"},{"book":"LAMDA"}]

R.compose(…)

นอกจากใช้ pipe แล้วเรายังสามารถที่จะใช้ compose ได้อีกด้วยแค่การทำงานของมันจะต่างกันตรงที่ compose จะทำจากขวาไปซ้าย

const composer = R.compose(
console.log,
upperCase,
JSON.parse
)

Currying

เมื่อเราเริ่มใช้ Composition เป็นปัญหาถัดมาคือ แต่ละฟังก์ชันมันรับ Parameter (unary) แค่เพียงตัวเดียว ซึ่งในความเป็นจริงเราไม่สามารถที่จะเขียนโปรแกรมที่มีการทำงานแบบนั้นได้ ในบางกรณีต้องรับอย่างน้อย 2 (none-unary) เช่นถ้าเราจะทำระบบเครื่องคิดเลข อย่างน้อยเราก็ต้องรับเลขตัวตั้งและตัวกระทำเข้ามาในระบบ

const add = (x, y) => x + yconsole.log(add(1,2)) // 3
add(2,y)
const add = x => y => x + yconst addOne = add(1) // return [function]
addOne(2) // 3
------
add(1)(2) // 3
function add(x) {
return function(y) {
return x + y;
}
}
const addOne = add(1)
addOne(2)
------
add(1)(2)

R.curry(…) [docs]

แทนที่เราจะเขียนแยกเองบางครั้งเราอาจจะใช้ ramdajs ช่วยก็ได้ มาดูตัวอย่างกัน

const h = (x, y, z) => x + y * zh(1, 2, 3) // 7
const h = x => y => z => x + y * z
const findX = h(1)
const findXY = findX(2)
const findXYZ = findXY(3)
console.log(findXYZ) // 7
console.log(h(1)(2)(3)) // 7
import * as R from 'ramda'const h = (x, y, z) => x + y * z
const withCurry = R.curry(h)
const findX = withCurry(1)
const findXY = findX(2)
const findXYZ = findXY(3)
------------------------ or -------------------
h(1,2)
h(3)
--------
h(1)(2)
h(3)
--------
h(1)
h(2,3)
--------
h(1)
h(2)(3)
-------

Partial

เป็นการเขียนฟังก์ชันที่สามารถทำให้รับ Paramter ได้มากกว่า 1 และโดยตัว Partial นั้นจะเป็นการเขียนฟังก์ชันเพื่อลดจำนวนของ Arity (เป็นศัทพ์ทางคณิตศาสตร์ ที่พูดถึงจำนวน Parameter ของฟังก์ชัน) ถ้าย้อนกลับไปดูที่ Curry เราจะเห็นว่าการเรียกใช้งานฟังก์ชันจะเรียกเท่าจำนวน Parameter คือ

const h = x => y => z => x + y * z
const partialApplied = (
fn,
...args
) => (
..._args
) => fn.apply(
null, [...args, ..._args]
)
const h = (x, y, z) => x + y * zconst takeAllAtOnce = partialApplied(h, 1, 2, 3) // 7const takeOnlyFn = partialApplied(h) // [function]
takeOnlyFn(1,2,3) // 7
const takeSomePreDefineNumber = partialApplied(h, 1) // [function]
takeSomePreDefineNumber(2,3) // 7
const takeMore3AtOnce = partialApplied(h)
takeMore3AtOnce(1, 2, 3, 4, 5, 6, 7, 8, 9, [1,2,4], 'test') // 7

R.partial(…)

สำหรับใครอยากทำแบบรวดเร็ว ไม่ต้องมานั่งจัดการอะไรมากผมขอแนะนำฟังก์ชัน Partial จาก ramdajs นะ มาดูตัวอย่างกัน

import * as R from 'ramda'const h = (x, y, z) => x + y * z
const takeAllAtOnce = R.partial(h, [1, 2, 3])
takeAllAtOnce()
const takeOnlyFn = R.partial(h)
takeOnlyFn([1, 2, 3])()
const takeSomePreDefineNumber = R.partial(h, [1])
takeSomePreDefineNumber(2, 3)
const takeMore3AtOnce = R.partial(h)
takeMore3AtOnce([1, 2, 3, 4, 5, 6, 7, 8, 9, [1,2,4], 'test'])()

Curry and Partial in Mathematic

ปกติแล้ว Curry และ Partial มันเป็นวิธีการทางคณิคศาสตร์ ถ้าหากเราจะพูดถึงนิยามของแต่ละอันที่ที่เราเขียนมาเป็นโค้ดเราจะสามารถเขียนได้ดังนี้ (ใครที่อ่านนิยามทางคณิตศาตร์ไม่ออกหรือไม่แข็งด้านคณิตศาตร์ ผมแนะนำให้อ่านข้ามไปเลยครับ ไว้มีโอกาสจะมาสอนในบทความอื่น)

Curry

Given ƒ: (X • Y • Z) -> Nthen curry(ƒ): X -> (Y ->(Z -> N))

Partial

Given ƒ: (X • Y • Z) -> N
then ƒ: (Y • Z) -> N
Apply by
partial(f): ((X • Y -> N) • X) -> (Z -> N)

Point-free programming

หรืออีกชื่อคือ Tacit programming คือการเขียนโค้ดแบบไม่ใช้ จุด(.) ไม่สนใจว่าเราต้องเรียกฟังก์ชันอะไรตรงไหนอย่างไร ปกติเราจะเขียนกันเป็นปกติใน OOP เวลาที่ต้องการเรียก Properties ของ Object แต่ใน Functional Programing เราจะเน้นการเขียนแบบ Point free มากกว่า โดยการประกาศฟังก์ชันและประกอบมันให้เป็นฟังก์ชันที่ทำงานกับชุด Data ที่ต้องการแล้วปล่อยให้ระบบทำงานไปเองโดยที่เราไม่ต้องไปสั่งมัน เราลองมาดูตัวอย่างกัน

const payload = '123456'
const _split = R.split('')
const _toNumber = (x) => Number(x)
const _toStr = (x) => String(x)
const _eachToNumber = (x) => x.map(_toNumber)
const _product = (x) => R.product(x)
const _findEven = (x) => x.filter( n => n % 2 === 0)
const _concat = (x) => R.join('', x)
const findEvenFromProductResult = R.pipe(
_split, // ['1','2','3','4','5','6']
_eachToNumber, // [1,2,3,4,5,6]
_product, // 720
_toStr, // '720'
_split, // ['7','2','0']
_eachToNumber, // [7,2,0]
_findEven, // [2,0]
_concat, // 20
_toStr, // '20'
)
const result = pipeline(payload)
console.log(result)

Avoiding Mutate Data, Immutability

การจะทำให้ระบบไม่เกิดบัคได้นั้น อย่างแรกเลยเราต้องทำตัวแปรของเรานั้นไม่ถูกแก้ไขได้ก่อน ทำให้มันเป็น Immutable State ยกเว้นบางตัวเราอาจจะแก้ไขจริงๆ อันนี้ก็เป็นข้อยกเว้นไป แต่ในการเขียน Functional Programming ตัวความถูกต้องของ Data นั้นสำคัญมากถ้าเกิด Data เปลี่ยนจากเดิม ระบบเราอาจจะเกิดข้อผิดพลาดได้ เพราะงั้นเรามาดูวิธีการทำให้ตัวแปรเป็น Immutable

const num = 1
const obj = Object.freeze({ name: 'John' })obj.name = 'Jane'
// Error
const Mr = (obj) => ({...obj, prefix: ‘Mr’ })
const obj = { fname: ‘John’, lname: ‘Doe’, prefix: ‘’ }
const withPrefix = Mr({ …obj }) // here use Spread
const Mr = (obj) => ({...obj, prefix: 'Mr' })
const obj = {
fname: 'John',
lname: 'Doe', prefix: '',
address: { hometown: '', city: },
}
const withPrefix = Mr(obj)
const Mr = (obj) => ({...obj, prefix: 'Mr' })
const obj = {
fname: 'John',
lname: 'Doe', prefix: '',
address: { hometown: '', city: },
}
const newObj = JSON.parse(JSON.stringify(obj))
const withPrefix = Mr(newObj)

Avoiding Side effect

ถ้าอ่านมาถึงตอนนี้ผมจะมีการพูดถึง side effect อยู่บ้าง มันก็คือการที่ค่า return value ของเราถูกแก้ไขหรือเปลี่ยนแปลงโดยฟังก์ชันอื่นที่เราไม่ได้เรียกใช้ ในขณะที่เรากำลังเรียกใช้งานฟังก์ชันอื่นๆ อยู่สาเหตุอาจจะมาจาก:

  • การแชร์ใช้งาน Object ที่ยังไม่ได้ clone ไปยัง ref memory ใหม่

AlgorithmTut

May the force be with you. **Tut stand for Tutorial**

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store