iT邦幫忙

2021 iThome 鐵人賽

DAY 2
0
Software Development

Functional Programming For Everyone系列 第 2

Day02 - Pure Function

yo! what's up!

本篇文章會簡單地介紹基本的 Functional Programming 概念,這些概念不僅重要,更是貫穿了之後的主題。

Pure Function

給定相同的輸入,一定會得到相同的輸出。並且不會產生任何 Side Effect.

那我們就來探討一下這段純函式(pure function)的定義,

  • 什麼是給定相同輸入,一定會得到相同的輸出?
  • 什麼是 Side Effect?

給定相同輸入,一定會得到相同的輸出

舉例來說, Math.random

Math.random() // 0.06243529219776711
Math.random() // 0.9436551613058293
Math.random() // 0.29082104009990295

Math.random 有什麼問題呢?可以看到給定同樣的輸入,卻是不同的輸出,這就是不純的函式(impure 函式)。

滿足純函式的要點之一,就是必須是在相同輸入下,無論重複呼叫多少次,輸出的結果永遠是一樣的,舉 square square為例,只要給定特定的值,就會回傳該值的平方。

const square = num => Math.pow(num, 2);

square(2) // 4
square(2) // 4
square(2) // 4

ex1

數學中的函式就像是兩個集合的對應關係,且集合內的數皆可以在另外一個集合找到對應的唯一值。而純函式就是數學中的函式,必須是一對一或是多對一的關係。

Side Effect

Side Effect 就是當呼叫函式時也改變外部物件的狀態,我們就會稱這個函式有Side Effect.

而什麼是改變外部狀態,舉以下幾個例子

let state = 0;

const counter = () => {
    state += 1;
    return state;
} 

counter(); // 1
counter(); // 2
counter(); // 3
counter(); // 4

上面範例的counter 就是一個有side effect 的函式,因為我們因為呼叫了 counter 時也改變外部的狀態。當一個有程式四處散落著 side effect 的函式時,那程式變得非常不可控。

又或是在函式內進行console.log

const doWhat = (x) => {
    console.log(`hahaha, ${x}`) 
    return x;
}

doWhat()

這個範例則是我們在呼叫 doWhat 時,同時也在 console 寫入了一些資訊,這也是一個有 Side Effect 的函式。

而會導致Side Effect 的操作,筆者列了幾項,

  • 寫入 / 讀取 / 刪除 LocalStorage 的狀態
  • DOM 操作
  • 寫入 / 讀取檔案
  • 改變外部的資料結構
  • Http請求

Wait..., 讀者們可能心想,在現實開發中,不使用上述這些會產生 Side Effect 的功能,還能進行開發嗎? 對,基本上是開發不了任何東西的。所以FP 該不會等於 no code!

不不不

FP 不是不能有Side Effect,而是將這些Side Effect用一些方式包覆起來,將 Effectful program 跟 Pure function 有個明確的界線進行劃分。在之後章節我們也會提到FP是如何解決。

回到純函式的定義

為什麼純函數這個概念這麼重要? 因為它直接確保 referential transparency 的概念!

首先我們先來看看 referential transparency 的定義

An expression is called referentially transparent if it can be replaced with its corresponding value (and vice-versa) without changing the program's behavior. - wiki

舉例來說,

const x = square(2);

上述提到的範例square ,因為它是有著 referential transparency 的特性,所以我們可以將直接用4 取代

const x = 4;

此概念雖然簡單,筆者們看了這個範例或許心想 So What?

但 referential transparency 確保了

  • 開發者可以在理解該段程式時,不需要去擔心外部狀態會如何改變。
  • 可以更有信心地進行重構。

為什麼這些概念這麼重要呢?

提升程式碼的可預期性

就如同剛剛上述提到的特性,當程式是由眾多純函數組成時,不用擔心我們影響外部的任何狀態,也可以確保程式執行過程中,不會產生非預期地結果。

提升可測試性

純函式也確保了我們在測試時,不用去建造一個真實的環境去測試整個 program, 只需要給定 input 並預期其結果是正確的就好。

可快取

由於純函式的關係,確保了每個輸入對應唯一的輸出值,這就代表我們可以進行快取,舉最知名的 fibonacci number cache 過後的 fib函式與原本的 fib 的差別就是可以減少重複計算。

const memo = f => {
    const cache = {};
    const memoized = n => {
        if(!cache[n]) {
             cache[n] = f(n);
        }
        return cache[n]
    }
    return memoized;
}

const fib = n => {
    if(n === 0) return 0;
    if(n === 1) return 1;
    return fib(n-2) + fib(n-1)
}

const memoFib = memo(fib)

memoFib(10) // 55
memoFib(10) // cache 後的 55

小結

儘管這些概念都是基本的概念,但其概念卻是貫穿著函數式編程,明天將介紹 Currying vs. Partial Application.


上一篇
Day 01 - Hi, Functional Programming
下一篇
Day 03 - Curry
系列文
Functional Programming For Everyone30

尚未有邦友留言

立即登入留言