FP - Pure functions, side effects

FP - Pure functions, side effects

紀錄一下上課時學習到的函式導向的一些基礎觀念。

Concepts

  1. Avoid side effects 避免副作用

  2. Avoid mutations 避免修改變數

  3. avoid shared state 避免共享狀態

  4. use pure function 使用純函式

  5. use function composition 使用函式組合

  6. use declarative code instead of imperative code 使用聲明式而不是命令式的編碼風格

JavaScript中可以使用不同的編碼風格, 函式導向只是其中一種。所以在寫code時不妨大膽一點, 不要局限自己在特定一種風格。

What are Pure Functions ? 什麼是純函式?

  1. 純函式只依賴它接收到的參數, 而不會依賴一個外部的變數
  2. 純函式不會有副作用, 他不會去變更不屬於他作用域內的變數
  3. 輸入與輸出之間的關係是一定的, 同樣的輸入必定產生同樣的輸出

其他兩項都很好懂, 可是第二項提到的副作用是什麼意思?

What are Side effects ? 什麼是副作用?

side effect: 在函式外部可觀察到的改變, 這些改變難以追蹤, 難以重現,這會導致程式碼難以維護,也難以理解,有時甚至難以預料它的成果。遇到帶有side effect的函式,我們可能會需要四處追蹤一段函式中的所有變數來源,才能理解它的功能。

  1. 變更一個全域的值(這個值可能是變數, 可能是某個物件內的屬性, 可能是資料結構)

  2. 變更一個參數的原始值

  3. throw an exeption
    [JS] 談談 JavaScript 中的錯誤處理 Error Handling

  4. 印出東西(console.log)或是在畫面上顯示
    這兩樣也被算是函式與外部互動, 因此是side effect

  5. 觸發外部程序

  6. 呼叫其他有副作用的函式

side effect是不可能被完全避免的, 甚至有時候我們就是需要side effect來達成目的。functional programming的目標並不是要我們完全掃除side effect, 而是要更好地控制他們。通常我們會將有side effect的函式聚集起來, 這樣一來其他的函式就更好做測試, 更好理解

如何避免副作用?

1
2
3
4
5
6
let count = 0;

let increment = function() {
count ++;
return count;
}

在上面的範例中, 可以看到有一個變數count, 還有一個函式increment, 在increment中修改了count, 可是問題是, count是一個全域變數, 它有可能在任何地方被修改

一個理想的函式應該是:知道輸入,就能獲得預期輸出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

let count = 0;

// 改寫之前
let increment = function() {
count ++;
return count;
}


// 改寫之後
let increment = function (num) {
return num + 1;
}

在第二個函式中, 我們使用一個參數來代替原本使用的全域變數, 這個函式會處理這個參數, 他不會去觸碰任何不屬於它作用域內的東西, 這就是pure function

1
2
3
4
5
6
7
8
9
10

let average = function (scores) {
let total;
for (let i = 0; i < scores.length; i++) {
total += scores[i];
}
return total / scores.length;
}

average([10, 20, 30, 40]);

這個取平均值的函式也不會變動到傳入的陣列, 而是返回一個新的值

一些反面例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var currentUser = 0,
users = [
{name: "James", score: 30, tries: 1},
{name: "Mary", score: 110, tries: 4},
{name: "Henry", score: 80, tries: 3}
];



// A. unpure: mutate property of a global variable
var updateScore = function(amount) {
users[currentUser].score += amount;
}

// B. unpure: mutate a global variable
var updateUser = function (newUser) {
currentUser = newUser;
}

A, B, C三個函式都不符合函式導向的概念, 為什麼?
A函式中使用了一個外部變數-users, 然後改變了這個users陣列中的值。他改變了一個不屬於自己作用域內的東西,因此是不純的。
B函式也改變了一個外部變數currentUser的值, 這個變數並不屬於他自己的作用域, 也因此這個函式也是不純的

改寫

要注意, 函式導向中的函式, 他們的行為都是非常單純的 - 接受資料, 回傳結果。一個函式只應該根據他收到的東西做動作!而不應該去觸碰任何不屬於他作用域內的東西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

// A. 不去改變原陣列users中的值, 而是使用一個函式寫出分數更新的邏輯, 然後回傳出去
var updateScore = function (originalAmount, newAmount) {
return originalAmount + newAmount;
}

// B. 不去改變外部currentUser的值, 而是寫一個函式找到新的user後回傳出去
var updateUser = function(newUser) {
return users[newUser];
}

users[1].score = updateScore(users[1].score, 10);
users[2].tries = updateTries(users[2].tries);
currentUser = updateUser[users[1]];

參考

Functional Programming in JavaScript: A Practical Guide
這位講師解說的觀念都很清楚好懂, 也有很豐富的練習, 如果對函式導向有興趣不妨在特價時購買!

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×