# [第5章] 針對複雜應用的設計模式
## :triangular_flag_on_post: 本章內容
:::success
1.命令是處理異常方式的問題<br>
2.使用容器,以防訪問無效數據<br>
3.用functor的實現來做數據轉換<br>
4.利于組合的Monad數據類型<br>
5.使用Monadic類型來鞏固錯誤處理策略<br>
6.Monadic類型的組合與交錯
:::
### :pencil2: 5.1 命令式錯誤處理的不足
:::success
在許多情況下都會發生javaScript錯誤,特別是在與服務器通信時,或是在試圖訪問一個Null對象屬性時。此外,第三方庫也有可能拋出異常來表示某些特定的錯誤。因此開發者在編程時總是需要做最好最壞的打算
異常是通過try-catch處理
try...catch 語法標記出一整塊需要測試的語句,並指定一個以上的回應方法,萬一有例外拋出時,try...catch 語句就會捕捉。
:::
### :pencil2: 5.1.1 try catch
``` javascript
try {
// code that might throw an exception in here
//需要測試的語句
}
catch (e) {
// 處理錯誤的發生,並做顯示
console.log('ERROR' + e.message);
}
```
#### 這個語法會先將 try 區塊中的程式碼執行一次,如果裡面的程式碼有問題就會立刻中止執行,執行步驟會直接跳到 catch 區塊的程式碼,如果 catch 的程式碼又有問題,那網頁就會跳錯。
``` javascript
// To name a few
// 資料查找 :: DB, String -> Object
//
const findObject = R.curry(function (db, id) {
const result = find(db, id)
if(!result) {
throw new Error('Object with ID [' + id + '] not found');
}
return result;
});
const findStudent = findObject(DB('students'));
```
``` javascript
try {
// 需要測試的語句
var student = findStudent('444-44-4444');
}
catch (e) {
console.log('ERROR' + e.message);
}
//這段程式碼使用了柯里化的概念來實現一個具有錯誤處理的
//創建了一個特定資料庫的查找函數 findStudent。
```
### :pencil2: 5.1.2 函數式程序不應拋出異常
命令式的javaScript代碼結構有很多缺陷,而且也會與函數式的設計有兼容問題。<br>
1.<b>可讀性和可維護性</b>:通常會一系列的指令和狀態改變來描述程式的執行流程,這可能導致程式碼變得難以閱讀、理解和維護。當程式碼的邏輯複雜時,命令式的結構可能導致程式碼變得冗長和難以管理。<br>
2.<b>可變狀態</b>:命令式的 JavaScript 常常使用可變的狀態(mutable state),這使得程式的行為難以預測。當多個部分共享同一個狀態時,容易出現錯誤和副作用,這可能導致難以追蹤程式行為的問題。<br>
3.<b>副作用</b>:命令式的 JavaScript 經常使用副作用(side effects),例如修改全域變數或修改DOM。這種副作用可能導致程式碼的可測試性降低,並增加理解和調整的困難。<br>
4.<b>兼容性問題</b>:函數式設計強調純函數(pure functions)和不可變數據(immutable data),而命令式的 JavaScript 通常偏向於可變狀態和副作用。這可能導致在使用函數式設計技巧時,需要克服一些兼容性問題,或進行額外的轉換和調整。
### :thought_balloon: 5.1.3 空值(null)檢查問題
<!-- 向前面所說的,Try & Cache 會造成無法連貫 -->
除去拋出錯誤外的另外一種錯誤處理,就是回傳 `null`
以下的例子是根據學校去查出學生的住址:point_down:
``` javascript
function getCountry(student) {
let school = student.getSchool();
if(school !== null) {
let addr = school.getAddress();
if(addr !== null) {
var country = addr.getCountry();
return country;
}
// null 回傳最大的保證了單一回傳的特性,雖然沒有比較好。
// 這增加了我們要多花點時間和程式去做棘手的 `null` 判定
return null;
}
throw new Error('Error extracting country info');
}
```
## :triangular_flag_on_post: 5.2 Functor
把值裝進一個容器,而且只能使用對外的 API 處理值,這樣做有什麼好處?換個方式問:讓容器自己去運用函數的好處是什麼?答案就是抽象,也更 陳述性的declarative,我們換個角度看,當我們傳入一個函數給 map() 時,我們請求容器幫我們對值執行這個函數
參考:https://ithelp.ithome.com.tw/articles/10240162
<!-- 雖然說我們可以用一個簡單的過濾去判斷是否為`Null`在做對應的回傳 -->
<!-- 但這和 Try & Cache 都依樣是被動的檢查,如果能夠更有效並且減少這些冗余代碼能夠省下多少時間。 -->
### :thought_balloon: 5.2.1 Wrapping unsafe values
- Containerizing (or wrapping) values(比較實際的例子是封裝)能夠最大程度的保證值的不可變動與安全的存取或修改。
<!-- 這段我們將會用第三章講過的 Map 來做解釋 -->
- Map 就是Functional programing 所說的 pure function
5.1 以下將用 Wrapper 這個範例來做解釋
補充
``` javascript
// 封裝一個值,可以避免使用者去直接的 access 或是 manipulate
class Wrapper {
constructor(value) {
this._value = value;
}
// map :: (A -> B) -> A -> B
map(f) {
// 錯誤 1
return f(this.val);
};
Simple type that stores a single value of any type
Maps a function over this type (just like arrays)
toString() {
// 錯誤 2
return 'Wrapper (' + this.value + ')';
} }
// wrap :: A -> Wrapper(A)
// 注意:這個 wrap 後面會很常使用到
const wrap = (val) => new Wrapper(val);
Helper function that quickly creates wrappers around values
```
例:
``` javascript
const wrappedValue = wrap('Get Functional');
// 用 Ramda 去取得值
// tips: identity 並不做任何事情,只回傳接收到的參數
wrappedValue.map(R.identity); //-> 'Get Functional'
或是
wrappedValue.map(console.log);
// 好處是如果需要 access Wrapper 都必須要經過 .map,而不能直接取得值
// 也最大程度的提升我們整個參數的不可變動性,並且保護我們的變數
```
以上的方法雖然保護到變數,但依然沒辦法很好的處理 null/undefined,接下來我們會根據 null/undefined 來做處理。
#### 如果錯誤能夠被其他方法來做預先的判斷並處理,並保證我們進入 Function 時候的值不會是*錯誤*的
* 這裡錯誤包含 null, 空字串, 負數等非預期的回應資訊
``` javascript
// 以這來當例子,我們建立一個 fmap 靜態 function
// fmap :: (A -> B) -> Wrapper[A] -> Wrapper[B]
Wrapper.prototype.fmap = function (f) {
// 判斷邏輯可以塞這裡,在驗證之後,才送給 wrap 做儲存
return wrap(f(this.val));
};
```
Factor 就是像洋蔥一樣層層的包裹著變數,然後做處理.
- 上面的 Function 先新建一個*容器
- 接著將值拋給由外部傳入的 function
- 最後再將取得到的最終運算值,回傳給*同屬性的容器
後面我們會再進一步的說明這個容器,還有他的相關用法。
### :pencil2: 5.2.2 Functors explained
A functor is simply something that can be mapped over
這句話分成兩部分,一個是 something 一個是 can be mapped over,只要符合這兩個就是 Functor 了
### Something (或稱 Category)<br>
A set of values arranged in some shape

↑ The values are in yellow, and the shape is in blue
``` javascript
// Something 1: Array
[1,2,3,4,5] // 被存放在 Array 裡的一組值
// Something 2: Object
{ age: 22, name: 'Tom'} // 被存在物件裡的一組值
// Something 3: Single value
39 // 某個值,除了 Number 也可以是任何型別
// => [ 5, 7, 9 ]
```
不止 Array 是 Something,任何原始型別 ( string 、 number... ) 或是 Object 甚至 Function 都是 Something
### Can be mapped over<br>
代表你可以把 list 中每一個值做一些事然後輸出
``` javascript
// Something 1: Array
[ 1, 2, 3, 4, 5 ].map(x => x + 3)); // [ 4, 5, 6, 7, 8 ]
```

## :triangular_flag_on_post: 5.3 使用Monad函數式地處理錯誤 <span style="font-size:18px">Functional error handling using monads</span>
``` javascript
// 找到學生
const findStudent = R.curry(function(db, ssn) {
return wrap(find(db, ssn));
});
// 找到學生的地址
const getAddress = function(student) {
return wrap(student.fmap(R.prop('address')));
}
// Compose 兩個功能為一個,目的尋找特定學生地址
const studentAddress = R.compose(
getAddress,
findStudent(DB('student'))
);
// 範例 studentAddress('444-44-4444'); //-> Wrapper(Wrapper(address))
// 第一次 map,应用 R.identity
// const result1 = studentAddress('444-44-4444').map(R.identity);
// 第二次 map,再次应用 R.identity
// const result2 = result1.map(R.identity);
// 為了取得最後的 Result - address
studentAddress('444-44-4444').map(R.identity).map(R.identity);
```
### :pencil2: 5.3.1 Monad 從控制流到數據流
Data control flow
``` javascript
Wrapper(2).fmap(half); //-> Wrapper(1)
Wrapper(3).fmap(half); //-> Wrapper(1.5)
// Functor 被賦予函數並返回結果
const Empty = function(_){};
Empty.prototype.map = function() { return this; };
const empty = () => new Empty();
//(n % 2 == 0) 来判断 n 是否是偶数。如果 n 是有限数字且是偶数,则返回 true,否则返回 false
// half(4), half(7)
const isEven = (n) => Number.isFinite(n) && (n % 2 == 0);
const half = (val) => isEven(val) ? wrap(val / 2) : empty();
half(4).fmap(plus3); //-> Wrapper(5)
//由于 3 不是偶数,所以会返回一个表示空值的 Empty 实例
half(3).fmap(plus3);// Empty
```
有關 Monad
1.或是說 FP,就是一個抽象也沒有任何意義的 Fn<br>
2.簡單來說就是將值包裝起來,不停的轉送,中間會經過拆解、加工(Chain),直到最後解開外包後顯示內容.<br>
3.如上方的範例 Wrapper,但不同的 Monad 有不同的實現原則(例如: map, fmap)。
重構 Wraper
``` javascript
class Wrapper {
constructor(value) {
this._value = value;
}
static of(a) {
return new Wrapper(a);
}
map(f) {
return Wrapper.of(f(this.value));
}
// 遞回自己,用於解構取值
join() {
if(!(this.value instanceof Wrapper)) {
return this;
}
return this.value.join();
}
toString() {
return `Wrapper (${this.value})`;
} }
// 使用 Wrapper 类
const flattenedWrapper = nestedWrapper.join();
console.log(flattenedWrapper.toString()); // Wrapper (7)
```
### :thought_balloon: 5.3.2 Error handling with Maybe and Either monads
- 除了有效的包裝值(如前面所講的例子)外,一元結構(monadic structures)也可以用來模擬不存在(absence of one)的狀況 - 例如 null 或 undefined。
- 所以 FP 具體的將錯誤轉變成一件可以預期的事情,用 Maybe 或 Either 來做處理,而不是直接中斷出錯。
兩者都有以下的優勢
- 隔離不需要的邏輯與變數
- 將 null/undeined 處理併入程式中
- 避免錯誤拋出中斷程式
- 支援功能合併處理的作法
- 集中邏輯並且設置預設值
### Maybe 做空值處理
Maybe 一個空型別和兩個具體的子類型,它幫忙處理了 “nullable” 的值 (null and undefined),所以我們可以專心的處理邏輯相關的事情。
- Just - 用於回傳一個包裹過的值
// 可以想像 nothing 就是一個什麼事情都不做,讓這段程式 pass 過去
- Nothing - 用於回傳兩種包裹過的值 *空值* 或是 *不重要的錯誤資訊*
底下我們來看個範例
``` javascript
// 這宣告一個外框架
class Maybe {
static just(a) {
return new Just(a);
}
static nothing() {
return new Nothing();
}
// 這裡來做判斷使用 Just 或是 nothing
static fromNullable(a) {
return a !== null ? just(a) : nothing();
}
static of(a) {
return just(a);
}
get isNothing() {
return false;
}
get isJust() {
return false;
}
}
// 用來做事情的 Just
class Just extends Maybe {
constructor(value) {
super();
this._value = value;
}
get value() {
return this._value;
}
map(f) {
return of(f(this.value));
}
getOrElse() {
return this.value;
}
filter(f) {
Maybe.fromNullable(f(this.value) ? this.value : null);
}
get isJust() {
return true;
}
toString () {
return `Maybe.Just(${this.value})`;
}
}
// 就真的啥事都不做的 Nothing,最多就是當你要值的時候,給你個錯誤訊息
class Nothing extends Maybe {
map(f) {
return this;
}
get value() {
throw new TypeError('Can't extract the value of a Nothing.');
}
getOrElse(other) {
return other;
}
filter() {
return this.value;
}
get isNothing() {
return true;
}
toString() {
return 'Maybe.Nothing';
}
}
```
從上面程式可以看得出來,Maybe 就是一個骨架,而有兩個子類別(Just, Nothing)去繼承,最終的流向則會由程式的邏輯去處理會觸發哪一個。
<!-- 這邊可以舉個例子 jQuery -->
實例上來說,這裡 `wrap.map` 的結果會決定最後要使用那一個子類別。
如 Students 有取到就會儲存於 Just 內部

讓我們回到 5.4 的例子,要從本地端的 DB 去取得學生地址,我們沒辦法確定是不是一定能取到值,所以我們需要做一些檢查。
``` javascript
// 我們把空值的檢查 Curry 化,這樣其他地方有需要也可以重複的使用
// safeFindObject :: DB -> String -> Maybe
const safeFindObject = R.curry(function(db, id) {
return Maybe.fromNullable(find(db, id));
})
// 指定我要尋找 student 的 table
// safeFindStudent :: String -> Maybe(Student)
const safeFindStudent = safeFindObject(DB('student'));
const address = safeFindStudent('444-44-4444').map(R.prop('address'));
address; //-> Just(Address(...)) or Nothing
// 如果給予不正確的值,當取用變數 (get()) 時 我們會得到對應的錯誤,不取用則不會有任何事發生。
```
<!-- 根據學號取的地址 也就是 Just('444-44-4444') 或是 nothing -->
看完上面之後,可以很直白的由命名知道他要做什麼或用途是什麼。
另外一個有趣得應用是 `getOrElse()`
``` javascript
const userName = findStudent('444-44-4444').map(R.prop('firstname'));
// 如果到了這裡正確,會直接顯示結果,但錯誤的話則會顯示預設的文字作替代
document.querySelector('#student-firstname').value =
username.getOrElse('Enter first name');
```
接下來讓我們再回頭看一下之前那個醜醜的 `getCountry()`
``` javascript
function getCountry(student) {
let school = student.getSchool();
if(school !== null) {
let addr = school.getAddress();
if(addr !== null) {
return addr.country();
}
}
// 當這被回傳,我們很難清楚到底原因是什麼
return 'Country does not exist!';
}
```
<!-- 甚至於可能會被繁雜的 null/undefined 檢查搞得很火大,或是避免型別錯誤造成的意外錯誤。 -->
讓我們用 Maybe 來寫這個實際的運用
``` javascript
const country = R.compose(getCountry, safeFindStudent);
// 如同上面描述,這邊會根據你是 Just 或是 Nothing 來做呈現,如果是 Nothing 那我就會拋出最底下的 else 錯誤。
const getCountry = (student) => student
.map(R.prop('school'))
.map(R.prop('address'))
.map(R.prop('country'))
.getOrElse('Country does not exist!');
```
Function lifting
```javascript
const safeFindObject = R.curry(function(db, id) {
return Maybe.fromNullable(find(db, id));
}
```
> 這裡可以提出一個問題,我真的每個地方都需要去做安全檢查嗎?
接下來如果想要更精確的知道到底錯誤是什麼原因發生的,有另一種 Error handling 叫做 Either.......
------------------------------------- To be continue -------------------------------------
### 5.3.2 Either Monad 來處理異常
#### 使用Either從故障中恢復
一條是 Happy Path (Right),就是運算過程一切順利;<br>
另一條是 Sad Path (Left),只要某一處的運算出現錯誤,就會跳過之後運算直接輸出失敗結果