當我們使用 document.cookie()
讀取 Cookie 時,回傳為 String,我們希望提供 Key 讀取其 Value,這常見需求該如何實現呢 ? 本文分別使用 Imperative、Functional 與 Maybe 三種方式實現。
Version
macOS Mojave 10.14.5
VS Code 1.34.0
Quokka 1.0.216
Ramda 0.26.1
Crocks 0.11.1
Cookie
let readCookie = () => {
let data = document.cookie.split('; ').slice(0, -2);
console.log(getCookie('firstName')(data));
};
實務上要使用 document.cookie
去讀取 cookie,但讀出來為以下格式:
firstName=Sam; lastName=Xiao; _ga=GA1.1.320322304.1546312556; Hm_lvt_47acec2d282c3986f1b600abdc11c7ab=155525178
倒數第二個 ;
之後的資訊並不是我們要的,所以先 split()
成 array 後,用 slice()
取得不含倒數兩個 element 的新 array,最後再傳入 getCookie()
,根據 key 取得 value。
Imperative
let data = [
'firstName=Sam',
'lastName=Xiao'
];
// getCookie :: String -> [String] -> String
let getCookie = key => arr => {
for(let i = 0; i < arr.length; i++) {
let tuple = arr[i].split('=');
if (tuple[0] === key) return tuple[1];
}
return '';
};
getCookie('firstName')(data); // ?
getCookie('lastName')(data); // ?
getCookie('age')(data); // ?
data
為根據 document.cookie()
所整理過的 array。
我們希望當傳入 key 時, getCookie()
回傳 value。
第 8 行
for(let i = 0; i < arr.length; i++) {
Imperative 會使用 for
loop 一個一個找。
第 9 行
let tuple = s.split('=');
Array 中的 string 形式為 firstName=Sam
,可再使用 split()
拆成 array,因為很類似 tuple,變數姑且命名為 tuple
。
tuple
當然是很糟糕的命名,應該取一個有意義的變數名稱
第 10 行
if (tuple[0] === key) return tuple[1];
tuple[0]
即為 key,而 tuple[1]
為 value,因此可直接使用 if
判斷。
Imperative 會充斥著
中繼變數
,然後就開始為變數命名
傷腦筋,要如何使變數可讀性高又有意義,但畢竟我們在乎的是結果,這些中繼變數真的需要嗎 ? 是值得深思的問題
13 行
return '';
若傳入的 key 不存在,則回傳 empty string,避免產生 undefined
。
Functional
import { pipe, compose, split, converge, nth, objOf, map, find, propOr, prop, isNil, complement } from 'ramda';
let data = [
'firstName=Sam',
'lastName=Xiao'
];
// strToObj :: String -> Object
let strToObj = pipe(
split('='),
converge(objOf, [nth(0), nth(1)])
);
// isNotNil :: * -> Boolean
let isNotNil = complement(isNil);
// getCookie :: String -> [String] -> String
let getCookie = key => pipe(
map(strToObj),
find(compose(isNotNil, prop(key))),
propOr('', key)
);
getCookie('firstName')(data); // ?
getCookie('lastName')(data); // ?
getCookie('age')(data); // ?
17 行
// getCookie :: String -> [String] -> String
let getCookie = key => pipe(
map(strToObj),
find(compose(isNotNil, prop(key))),
propOr('', key)
);
FP 不會使用 for
loop 處理,可由 pipe()
清楚看出演算法流程:
- 先使用
map()
將 array 內的 string 轉成 object - 再使用
find()
搜尋 array 中每個 object,找到就傳回 object,否則傳回undefined
- 最後使用
propOr()
根據 key 取得 value
其中將 string 轉成 object,是想借助 Ramda 對 object 有豐富 function 可用。
可以看出 FP 解決問題方式是將問題最小化切割,然後各個擊破,與 imperative 整體思考方式不同
第 8 行
// strToObj :: String -> Object
let strToObj = pipe(
split('='),
converge(objOf, [nth(0), nth(1)])
);
先使用
split()
將字串轉為 array再使用
objOf()
將 array 轉成 object
20 行
find(compose(isNotNil, prop(key)))
使用 find()
根據 key 找尋 value,其 predicate 為 (a -> Boolean)
,而 prop()
回傳為 a | Undefined
,因此組合 isNotNil()
使其轉成 boolean。
14 行
// isNotNil :: * -> Boolean
let isNotNil = complement(isNil);
Ramda 並沒有提供 isNotNil()
,須自行組合,也可使用 compose(not, isNil)
。
21 行
propOr('', key)
由於 find()
可能回傳 undefined
,所以特別使用了 propOr()
處理。
我們可發現 FP 完全
沒有
中繼變數,所以再也不必為了變數命名而傷透腦筋
Maybe
import { pipe, compose, split, converge, nth, objOf, map, find, prop, isNil, complement } from 'ramda';
import { prop as prop_ } from 'crocks';
let data = [
'firstName=Sam',
'lastName=Xiao'
];
// strToObj :: String -> Object
let strToObj = pipe(
split('='),
converge(objOf, [nth(0), nth(1)])
);
// isNotNil :: * -> Boolean
let isNotNil = complement(isNil);
// getCookie :: String -> [String] -> Maybe String
let getCookie = key => pipe(
map(strToObj),
find(compose(isNotNil, prop(key))),
prop_(key),
);
getCookie('firstName')(data).option('N/A'); // ?
getCookie('lastName')(data).option('N/A'); // ?
getCookie('age')(data).option('N/A'); // ?
Ramda 的 find()
唯一缺點就是回傳 undefined
,因此我們必須小心翼翼地使用 propOr()
處理,但這有幾個缺點:
undefined
並非邏輯的一部分,是為了find()
而處理- 若一不小心使用了
prop()
,就可能回傳undefined
- 目前我們使用
propOr('', key)
,也就是undefined
時回傳 empty string,但若使用端想自行決定undefined
的 string 呢 ?
比較好的方式是回傳 Maybe
,由使用端決定 undefined
該如何處理。
21 行
find(compose(isNotNil, prop(key))),
prop_(key),
由於要回傳 Maybe
,改用 Crocks 的 prop()
,而不是 Ramd 的 prop()
,因為 Crocks 很多 function 名稱與 Ramda 一樣,差異只在於回傳 Maybe
,因此 Crocks 所提供的同名 function 一律以 _
postfix 表示。
25 行
getCookie('firstName')(data).option('N/A'); // ?
getCookie('lastName')(data).option('N/A'); // ?
getCookie('age')(data).option('N/A'); // ?
由於 getCookie()
回傳 Maybe
,必須透過 option()
將 Maybe
轉回 string。
若 undefined
時想顯示 N/A
,可一併傳進 option()
。
使用
Maybe
後,undefined
改由使用端處理,getCookie()
可專心處理正常邏輯,不必再為了undefined
分心,程式碼邏輯也更清楚
Conclusion
- Imperative 會使用很多中繼變數,常需為了變數命名傷透腦筋,也必須小心處理
undefined
- Functional 則不必使用中繼變數,可由
pipe()
清楚看出演算法思路,藉由將問題最小化分割,然後各個擊破 - Ramda 有些 function 會回傳
undefined
,如find()
,可藉由Maybe
讓使用端處理,讓主邏輯更為清楚,不用再為undefined
分心 - Crocks 不少 function 與 Ramda 同名,差異在於回傳 ADT,一律以
_
postfix 表示,類似 Haskell 以fn'
的 apostrophe 命名方式
Reference
W3schools.com, JavaScript Cookie
Ramda, pipe()
Ramda, compose()
Ramda, split()
Ramda, converge()
Ramda, nth()
Ramda, objOf()
Ramda, map()
Ramda, find()
Ramda, prop()
Ramda, isNil()
Ramda, complement()
Crocks, Maybe
Crocks, prop()
Crocks, option()