這篇要來講 Effect 內的一些資料型態,不過有些你可能在我們之前的一些範例中都見到了,比如 Array
,我們之前就用過 Array.map
,不過在那之前,我們先來聊聊不可變的資料有什麼好處
你可能在之前就有聽過不可變的資料的概念,例如你是 React 的開發者,你在更新 React 的 state 時就會需要確保你回傳一個全新的 object
function Component() {
const [state, setState] = useState({ count: 0, message: 'Current count is:' })
useEffect(() => {
setInterval(() => {
// 使用 object spread 來產生新的 object
setState((current) => ({ ...current, count: current.count + 1 }))
}, 1000)
}, [])
return <div>{state.message} {state.count}</div>
}
另外還有像 immer 或是 mutative 這種套件在幫助你讓你更好的在不變動原本的 object 的情況下產生更新的 object ,但說了這麼多,好像是因為套件的要求才讓你需要使用不可變的 object ,那我們實際上為什麼要使用呢?
我們來看個例子,這個其實在
interface State {
count: number
}
const state = {
count: 0
}
function increaseCount(currentState: State) {
currentState.count += 1
return currentState
}
const nextState = increaseCount(state)
// oh no, 原本的 state 也被更改了
console.log(state.count)
這樣看好像還不是什麼問題,如果你的程式的資料操作不只有一個步驟呢?如果你的程式中間還需要其它的 async 的操作呢?還記得我們在 「23. Effect 應用 2 :用 orpc 與 Effect 打造強韌的 API 介面」中展示的 data race 問題嗎?如果你的操作沒辦法一步完成,中間還有可能被中斷,那你就要小心最後執行的結果不符合預期
而不可變的資料因為不會去改寫原本的資料,而是產生一組新的,這基本上就解決了同時寫入同一組資料時產生的 data race 的問題
Effect 裡面其實有很多的資料型態與 helper function 來幫助你操作資料,而 Effect 的 helper function 的特色是:
pipe
串連這邊這只有介紹一部份,先從 helper function 系列的開始吧
Effect 裡的 Array 裡包含了大量的操作 Array 的 helper function ,這邊介紹幾個我個人覺得常用的
可以用來建立一個指定長度的陣列,並且呼叫你提供的 function 來產生值
const array = Array.makeBy(5, (i) => i)
console.log(array) // => [0, 1, 2, 3, 4]
可以根據你指定的 function 來取得 key ,並使用 key 來將資料分組
const data = [
{
type: "animal",
name: "Dog"
},
{
type: "animal",
name: "Cat"
},
{
type: "fruit",
name: "Apple"
},
{
type: "fruit",
name: "Banana"
}
]
console.log(Array.groupBy(data, (item) => item.type))
上面會輸出
{
animal: [ { type: 'animal', name: 'Dog' }, { type: 'animal', name: 'Cat' } ],
fruit: [
{ type: 'fruit', name: 'Apple' },
{ type: 'fruit', name: 'Banana' }
]
}
可以根據你給的 function 判斷兩個元素是不是重覆的,並去掉重覆的只留下一個,例如我們把上面的 groupBy 的資料用 type
去重覆看看
console.log(Array.dedupeWith(data, (a, b) => a.type === b.type))
我們會得到
[{ type: 'animal', name: 'Dog' }, { type: 'fruit', name: 'Apple' }]
與 pipe 一起使用
上面提到了大部份的 function 都可以跟 pipe 一起使用,我們來看個範例吧,這邊是我們把陣列中的數字都乘二,並且找出大於 5 的數字
const numbers = [1, 2, 3, 4, 5]
const processedArray = pipe(
numbers,
Array.map(n => n * 2),
Array.filter(n => n > 5)
)
console.log(processedArray) // => [6, 8, 10]
像這樣你可以用 pipe 串連多個操作
Record 主要用來操作 object 的,不過你在 Effect 裡還會看到另一個類似的模組叫 Struct,兩個的差別在於 Struct
會保存比較多的型態,而 Record
則比較通用,適合用來處理一般的 key value pair 的資料,大概就是當你發現 Record
處理完的 type 不合你的預期時可以試看看 Struct
這可以從 object 裡面移除掉一個特定的 key ,一般想要這麼做可能會用 object spread ,不過這樣就會多一個沒有用到的變數,用這個 function 就可以簡單解決了
const data = {
a: 1,
b: 2
}
// 不用 Record.remove 的方法
const {a: _, ...other} = data
console.log(other) // => { b: 2 }
console.log(Record.remove(data, 'a')) // => { b: 2 }
再來要介紹的是 Effect 內提供的類似於原生 ES6 的 Set 與 Map 的 HashSet
還有 HashMap
,雖說如此,但其實它們的用法都大同小異,最主要的差別在於這兩個資料型態本身是不可變的,所有改動的 function 都會回傳一個新的版本
HashMap 是內建用來代替 Map 的型態,它有著跟內建的 Map 類似的操作
從 key value pair 建立一個 HashMap
// 這會建立 HashMap { 'a' => 1, 'b' => '2' }
const map = HashMap.fromIterable([['a', 1], ['b', 2]])
從 HashMap 取得一個元素回來,要注意的是它回傳的是一個 Option ,代表它有可能找不到元素
console.log(HashMap.get(map, 'a'))
對已經有的 HashMap 設定一個值,它不會改動既有的 HashMap ,而是會回傳一個新的
const newMap = HashMap.set(map, "c", 3)
Effect 內建的資料型態大多支援使用這個 function 比較兩個是否相等,要注意的是它比較的是值是否相等 (deep equal) ,而非單純的 reference equal
console.log(Equal.equals(map, HashMap.fromIterable([["a", 1], ["b", 2]]))) // => true
HashSet 則是用來代替 Set 的型態,同樣的也有類似的操作
可以從任何的 iterable ,例如陣列建立一個 HashSet
const set = HashSet.fromIterable(['Apple', 'Banana'])
增加元素到 set 中,這會回傳一個新的 set
const newSet = HashSet.add(set, "Pineapple")
檢查元素是否在 set 中
console.log(HashSet.has(set, "Apple"))
上面我們介紹了 Effect 內建的資料操作的 function ,還有一些內建的資料型態,其實我們可以看到 Effect 在這方面是非常完整的,可以取代大部份你會在 lodash 或是 remeda 這種 utilties library 中用到的 function ,Effect 內建的資料型態也比起 js 內建的要來的提供了更多的操作方法,因為篇幅有限,剩下的就留給你自己探索了
話說明天就是算是鐵人完賽了,不過照我的預定還會有兩篇才會完結,實際上我覺得還有好多東西可以分享的,如果你看到這邊有什麼感想歡迎留言讓我知道,下一篇要來介紹 Effect 的 platform 這個將不同平台的 API 統一的套件