iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 3
2

本章重點

  • Shared mutable state is the root of all evil.
  • Immutable Data 是非常重要的概念, 變數一經賦值後不能被修改。
  • Immutable Data 降低 Mutable Data 的複雜度,並保證無副作用,更簡單搭配 Pure Function

來寫個 Todo List 吧!

昨天大致介紹過會用到的 JS 語法,那我們今天可以正式開始來寫點 code 了!

不管什麼程式寫完 Hello World 之後,大概就是寫 Todo List 吧!

// 以_(underscore)開頭的變數,表示它是 private 的,它不該被 Class 外的人碰到,
// 像是 TodoList._list ,表示這是一個 private 變數。
class TodoList {
	constructor() {
		this._list = []
	}
	get length() {
		return this._list.length
	}
	add(todo) {
		let finished = false
		this._list.push({ todo, finished })
		return this
	}
	toggleFinish(index) {
		this._list[index].finished = !this._list[index].finished
		return this
	}
	remove(index) {
		this._list.splice(i, 1)
		return this
	}
	get(index) {
		return this._list[index]
	}
	show() {
		console.table(this._list)
	}
}

很好!現在把這段 code 貼到 Console (如果你是用 Google Chrome ,按下 ctrl + shift + J 或是 Command + Option + J),開始創造一個 Todo List 吧!

let list = new TodoList()

list.add("找實習")
	.add("買洗碗精")
	.add("幫狗狗洗澡")
	.add("去遛狗")
	.show()

Output:

(index) todo finished
0 "找實習" false
1 "買洗碗精" false
2 "幫狗狗洗澡" false
3 "去遛狗" false

我做了點小技巧,在 TodoList.add 最後 return this ,這樣就可以把所有操作串起來,就不用一直寫 list
看來我們的 TodoList 運作得很正常,是時候來 finished 一些事情了!
TodoList.toggleFinish 相當簡單,就是把傳入 index 的 finished 反轉過來而已,今天就先完成前三項吧,明天早上起來再去遛狗吧。

list.toggleFinish(0)
	.toggleFinish(1)
	.toggleFinish(2)
	.show()

Output:

(index) todo finished
0 "找實習" true
1 "買洗碗精" true
2 "幫狗狗洗澡" true
3 "去遛狗" false

不不不,身為程式設計師怎麼能做這麼笨的事呢,為什麼不用迴圈呢?

for (let i = 0; i < 3; i++) {
	list.toggleFinish(i)
}
list.show()

Output:

(index) todo finished
0 "找實習" false
1 "買洗碗精" false
2 "幫狗狗洗澡" false
3 "去遛狗" false

太完美了,不過又把它們反轉回來了,既然都完成了,那乾脆把它們從 list 上移除掉吧,這麼一來乾淨一點。
TodoList.remove 是我剛剛從 Stack Overflow 上抄的,從陣列中移除元素可以呼叫 array.splice(要移除的index, 1)

for (let i = 0; i < 3; i++) {
	list.remove(i)
}
list.show()

Output:

(index) todo finished
0 "買洗碗精" false
1 "去遛狗" false

不不不不不不不!結果不是應該是 去遛狗 嗎?

What's wrong?

怎麼可能,這 Bug 一定不是我寫的!你檢查一下 commit 吧,是不是有誰改到了我的 code 了!
好啦,我承認是我,但這沒道理呀,問題會發生在哪?

如果你沒有馬上猜出問題在哪,先去喝杯咖啡 ☕ 吧,我們等等再聊。

有東西被改變了

猜到了嗎?如果沒猜到,我們來把迴圈親手執行一次。
把迴圈展開後,可以得到等價的程式碼。

list.remove(0)
list.remove(1)
list.remove(2)
list.show()

當你執行完 list.remove(0)list._list 被改變了。

Output arter list.remove(0)

(index) todo finished
0 "買洗碗精" false
1 "幫狗狗洗澡" false
2 "去遛狗" false

此時如果你執行 list.remove(1)"幫狗狗洗澡" 就會被移除掉,繼續執行 list.remove(2) 則會移除不到任何東西。
現在知道問題所在了,那該如何解決呢? Hmm...輪到我去泡杯咖啡 ☕ 了。

不要改變它,這點子怎麼樣?

解法當然不只一種,但我想先聽看看你的。

全部改成 list.remove(0) 怎麼樣?

目前的問題解決了,但如果目標是要移除 0, 1, 5, 6, 8 怎麼辦?
就得改成移除 0, 0, 3, 3, 4 , 註解都快比程式碼多了。

那不要使用 index ?
賦予每個 todo 一個不重複的 id ,搭配一個 TodoList._autoIncrement,
為了加快搜尋,再配上一個 Binary Search Tree

當然可行,這個解法相當完美,您是 SQL 系的嗎?
那換一個比較簡單的構想,在跑迴圈的時候不要改變 list 如何?

let tempList = new TodoList()
for (let i = 0; i < list.length; i++) {
	if (!(i < 3)) {
		tempList.add(list.get(i))
	}
} 
let undoStack = []
undoStack.push(list)
list = tempList
tempList = undefined

這個點子是不是輕鬆簡單呢?
而且還能將舊的 list 存進 undoStack,這樣當我們需要 undo 時,就可以隨時都可以做回朔。

Immutable Data

TodoList.remove 會改變 TodoList._list,但 TodoList._list 並不好追蹤,所以沒能在第一時間察覺錯誤。
在現實案例中,更不可能像 TodoList 如此單純, bug 可能產生在更難發現的地方‵,只要變數不能改寫,就能保證沒有副作用,這樣的概念稱之為 Immutable Data ,這在是 FP 中是很重要的觀念,超級無敵重要。

如果變數不能改寫 能帶來什麼優點:

  • 降低 Mutable Data 的複雜度,並保證無副作用。
  • 能輕易 undo,緩存舊變數即可。
  • 更適合 Concurrency Programming ,因為能無副作用,所以不需要數據鎖。
  • 能快速比對兩個變數,因為 reference 不同,使用 === 即可 (Shallow equality checking) ,不需要深度比對,效能較佳。
  • 更簡單搭配 Pure Function

另外,在 JS 為了保護資料,會使用 const 宣告變數,但 const 卻不能保證一定 Immutable,至於為什麼,還有什麼是 Pure Function ,這個我們留到明天再說吧。

後記

這篇好長我頭好痛,你不是答應我寫短一點嗎?
今天總算是進入正題了,其實這並不是 Immutable Data 的經典範例,通常範例是 Concurrency Programming ,不過應該有把 Immutable 的觀念解釋清楚吧,不知道大家是不是也有類似的經驗,是不是開始有點共鳴了呢?在最上頭有一句 Shared mutable state is the root of all evil. ,現在應該也懂什麼是萬惡之源了。
如果你喜歡這個系列的話,別忘了幫我按個讚,加上訂閱系列文章,然後把這篇文章分享給你所~~有的朋友。
明天繼續聊聊 Immutable Data 在 JS 中的實作,並且介紹 Immutable.js。

參考資料


上一篇
JavaScript (ES6) Syntax 大集合
下一篇
最詳細 Immutable Data 入門,看完秒懂
系列文
30天快樂學習 Functional Programming14

1 則留言

0
imakou
iT邦新手 5 級 ‧ 2018-01-05 06:10:23

您好,

請問實際應用上

get length() {
		return this._list.length
	}

為何命名上要有空白,而目的又是為何呢?

謝謝

阿志 iT邦新手 5 級‧ 2018-01-05 15:49:37 檢舉

這是 getter
目的是把語法模擬的像 Array.length
而不是 Array.length()

我要留言

立即登入留言