---
title: Functional Programming 第五章 針對複雜應用的設計模式
tags: F/E 分享, Functional Programming
###### tags: `F/E 分享`, `Functional Programming`
---
# Function programing 第五章 針對複雜應用的設計模式 下半章
## :triangular_flag_on_post: 本章內容
:::success
1. Either 命令式處理異常方式的問題
2. 使用容器,以防訪問無效數據
3. 用functor的實現來做數據轉換
4. 利于組合的Monad數據類型
5. 使用Monadic類型來鞏固錯誤處理策略
6. Monadic類型的組合與交錯
:::
### :pencil2: 5.3.2 Either Monad 來處理異常
#### 使用Either從故障中恢復
Either 代表2個邏輯分離的值a b
一條是 Happy Path (Right),就是運算過程一切順利;
另一條是 Sad Path (Left),只要某一處的運算出現錯誤,就會跳過之後運算直接輸出失敗結果
但他的運作方式跟前面說的Maybe 相似,只是多了個用於表示錯誤的型別,但在型別處理上要複雜許多。
Either Monad
``` javascript
//Either有2個子類別:Left 和 Right,分别用表示左值和右值。
class Either {
constructor(value) {
this._value = value;
}
get value() {
return this._value;
}
//左值表示錯誤、異常或失敗等情況。
static left(a) {
return new Left(a);
}
//表示正常的结果或成功等情況
static right(a) {
return new Right(a);
}
//根據给定的值val建一个Either。
//如果值不為 null,則建一个 Right;
//否則,創建一个 Left 对象。这是一种處理可能為空的值的常見模式。
static fromNullable(val) {
return val !== null ? right(val): left(val);
}
//建一个 Right 對象,并將值 a 儲在其中。它是 right 方法的别名,用于表示正常的结果或成功的情况。
static of(a){
return right(a);
}
}
// left(error) block
//map(_): 跳過,直接返回自身。
//value: 無法提取值,抛出 TypeError。
//getOrElse(other): 返回 other。
//orElse(f): 返回 f(this.value)。
//chain(f): 返回自身。
//getOrElseThrow(a): 抛出錯誤 a。
//filter(f): 返回自身。
//toString(): 返回包含值的字符串表示形式,形如 Either.Left(${this.value})。
class Left extends Either {
map(_) {
// silently skip
return this;
}
//左值無法提取值,會抛出 TypeError。
get value() {
throw new TypeError('Can't extract the value of a Left(a).');
}
getOrElse(other) {
return other;
}
orElse(f) {
return f(this.value);
}
chain(f) {
return this;
}
getOrElseThrow(a) {
throw new Error(a);
}
//用于根据谓词函数过滤值,在 Left 中,無法提取值,因此会直接返回自身。
filter(f) {
return this;
}
//返回包含值的字符串表示形式,Either.Left。
toString() {
return `Either.Left(${this.value})`;
}
}
// right(value) block
//map(f): 應用函数 f 到值并返回新的 Either 对象。
//getOrElse(other): 返回值本身。
//orElse(): 静默跳过,直接返回自身。
//chain(f): 返回 f(this.value)。
//getOrElseThrow(_): 返回值本身。
//filter(f): 根据谓词函数 f 过滤值并返回新的 Either 对象。
//toString(): 返回包含值的字符串表示形式,形如 Either.Right(${this.value})。
class Right extends Either {
map(f) {
return Either.of(f(this.value));
}
getOrElse(other) {
return this.value;
}
orElse() {
// silently skip
return this;
}
chain(f) {
return f(this.value);
}
getOrElseThrow(_) {
return this.value;
}
filter(f) {
return Either.fromNullable(f(this.value) ? this.value : null);
}
toString() {
return `Either.Right(${this.value})`;
}
}
```
重構 safeFindObject
safeFindObject 函数,它是一个柯里化函数,用於安全地在數據庫中查找对象。
``` javascript
//如果 obj 存在,則使用 Either.of 方法将其包装成一个 Either.Right 案例,并返回。
//如果 obj 不存在,則使用 Either.Left 方法创建一个包含错误消息的 Either.Left 实例,并返回该实例。错误消息指明未找到具有指定 id 的对象。
const safeFindObject = R.curry(function (db, id) {
const obj = find(db, id);
if(obj) {
return Either.of(obj);
}
// If is error, we call Left directly
return Either.Left(`Object not found with ID: ${id}`);
}
```
``` javascript
//這樣設計的目的是通過 Either 對象来表示解碼的结果,無論是成功還是失敗。都可以根據返回的Either對象来判断是否成功,並進一步處理成功和失敗的情况。
//使用 Either 對象可以避免直接抛出异常,而是將错誤包装進行傳遞,讓整個程式更具可讀性和可控性。
function decode(url) {
try {
const result = decodeURIComponent(url);
return Either.of(result);
}
catch (uriError) {
return Either.Left(uriError);
}
}
```
Either的結構可以存儲對象到右側或帶有錯誤訊息到左側
這樣可以返回單一的值,或者再發生故障的情況下返回錯誤訊息

Either 和 Try 是不同的概念,但它们都为函数式编程中的错誤處理提供了一种機制。Either 更加通用,可以用于任何類型的錯誤訊息,而 Try 更專注于操作可能抛出異常的情况。
### :thought_balloon: 5.3.3 使用IO Monad 與外部資源交互
``` javascript
// Example code
// 先大概看一下,之後我們會做詳細解釋.
IO.of('An unsafe operation').map(alert);
// 來個比較熟悉的範例吧
const read = function(document, id) {
// 非純函數:innerHTML 會因為外部的影響改變結果(例如 write 被呼叫)
// 因此無法保證每次的參數都相同。
return document.querySelector(`\#${id}`).innerHTML;
}
const write = function(document, id, val) {
// 改變值,並影響 read function 的結果
document.querySelector(`\#${id}`).innerHTML = value;
};
```
* 提醒:如第四章的 showStudent 所提的,為什麼要隔離 `impure` 功能出來是為了保證能夠最大程度的持續一至的輸出
#### IO monad
``` javascript
class IO {
constructor(effect) {
// Lodash here
if (!_.isFunction(effect)) {
throw 'IO Usage: function required';
}
// IO mornads 是改為包裝 function 來達成緩載入的功用
this.effect = effect;
}
static of(a) {
return new IO( () => a );
}
static from(fn) {
return new IO(fn);
}
map(fn) {
var self = this;
return new IO(function () {
return fn(self.effect());
});
}
chain(fn) {
return fn(this.effect());
}
run() {
// 緩載入 function 的手法 (lazy value)
return this.effect();
}
}
// 讓我們來重構一下 getter/write
const read = function (document, id) {
return function () {
return document.querySelector(`\#${id}`).innerHTML;
};
};
const write = function(document, id) {
return function(val) {
return document.querySelector(`\#${id}`).innerHTML = val;
};
}
// 鏈!!起來
// JS document
const readDom = _.partial(read, document);
const writeDom = _.partial(write, document);
// 這裡才去引入 document 避免污染純函式
// 完整的範例
<div id="student-name">alonzo church</div>
const changeToStartCase =
IO.from(readDom('student-name')).
// 中間可以穿插一些效果或是動畫
map(_.startCase).
map(writeDom('student-name'));
// 這裡可以做延緩載入,延緩到任何當你需要的時候。
changeToStartCase.run();
```
- IO monads 的優勢就是可以拆解出 pure 跟 impure 程式的
- 中繼傳送的方法可以獨立於 W/R (主要邏輯)外
- 我們將啟動放在最後面,因此不用擔心這中間會有任何的變化,保證最終值的一致性
- Monads 是可鏈(整合)起來的表達式或是運算式
- 可以想像成一個輸送帶,流水線方式運算著所需要的邏輯流程
- 這個容器可以用來創造一致的、安全的型別的值或是保留引用的可見性。
### 5.4 Monad 鏈式調用及組合
Either chain map
函數findStudent和append的組合
如果沒有適當的檢查前,前者如果產生null的返回值
,後者失敗則拋出TypeError

1.輸入
2.查學生紀錄
3.將學生資訊添加到HTML頁面
首先確保執行第一個函數把結果包裹一個適合的Monad(Maybe,Either)處理錯誤

#### 用Either重構函數
``` javascript
// validLength :: Number, String -> Boolean
const validLength = (len, str) => str.length === len;
//直接使用 monad 並根據錯誤提供特定的錯誤消息,而不是將這些函數提升到 Either 中。
// checkLengthSsn :: String -> Either(String)
const checkLengthSsn = function (ssn) {
return Either.of(ssn).filter(_.bind(validLength, undefined, 9))
.getOrElseThrow(`Input: ${ssn} is not a valid SSN number`);
};
// safeFindObject :: Store, string -> Either(Object)
//safeFindObject 是一个柯里化函数,用于在给定的db 中查找指定 id。
//它使用 Either.fromNullable 將 find 方法的返回结果包装成 Either 对象,並通過 getOrElseThrow 抛出錯誤。
const safeFindObject = R.curry(function (db, id) {
return Either.fromNullable(find(db, id))
.getOrElseThrow(`Object not found with ID: ${id}`);
});
// finStudent :: String -> Either(Student)
const findStudent = safeFindObject(DB('students'));
// csv :: Array => String
const csv = arr => arr.join(',');
//重構的 csv 函數從值數組返回字符串
```
#### :thought_balloon: showStudent 改用 monads 做錯誤自動處理
``` javascript
const showStudent = (ssn) =>
// WARNING: 不是 Maybe
Either.fromNullable(ssn)
.map(cleanInput)
// Chain 在這邊用於攤平值
.chain(checkLengthSsn)
.chain(findStudent)
.map(R.props(['ssn', 'firstname', 'lastname']))
.map(csv)
.map(append('#student-info'));
/** How Chain works start */
chain(f) {
return f(this.value);
}
/** How Chain works end */
// Lets call it again
showStudent('444-44-4444').orElse(errorLog);
// Either successfully
Monad Example [INFO] Either.Right('444-44-4444, Alonzo,Church')
// Or failed
Monad Example [ERROR] Student not found with ID: 444444444
//- Chaining isnt the only pattern
//- Compose can handle it well as well
```
#### :thought_balloon: 通用的 map 和 chain 函數
map 和 chain 可用於轉換 monad 中的值
``` javascript
// Map and Chain functions that work on any container
// map :: (ObjectA -> ObjectB), Monad -> Monad[ObjectB]
const map = R.curry(function (f, container) {
return container.map(f);
});
// chain :: (ObjectA -> ObjectB), M -> ObjectB
const chain = R.curry(function (f, container) {
return container.chain(f);
});
// This can be say as programmable commas
// Becasue Monads control data flow from one expression to next, we will descussing later on 5-11
```
#### :pencil2: Monad用作可編程逗號
``` javascript
const showStudent = R.compose(
//使用 trace 函數打印訊息,表示學生資訊已成功添加到 HTML 页面。
R.tap(trace('Student added to HTML page'))
//將前一步驟得到的 CSV 格式的學生信息附加到 HTML 頁面上的 #student-info 元素中。
map(append('#student-info')),
//將前一步驟得到的學生信息轉換成CSV格式
R.tap(trace('Student info converted to CSV')),
map(csv),
map(R.props(['ssn', 'firstname', 'lastname'])),
//使用 trace 函數打印訊息,表示學生紀錄成功。
R.tap(trace('Record fetched successfully!')),
chain(findStudent),
R.tap(trace('Input was valid')),
chain(checkLengthSsn),
lift(cleanInput));
```
在findstudent成功的找到SSN的學生對象情況下,showStudent函數的數據流

在findstudent不成功的情況下,對其於部分的影響
管道中任何組件的故障都會被跳過

liftIO 是一个函數,用於將一個值包装在IO實例中。
這樣可以將普通的值轉換為具有延遲執行特性的操作,從而與其他的 IO 操作一起使用。
``` javascript
const liftIO = function (val) {
return IO.of(val);
};
```
#### :thought_balloon: 完成 showStudent 程式改寫
``` javascript
// BEWARE: Compose 如同之前所說是由右到左來執行
const showStudent = R.compose(
map(append('#student-info')),
liftIO,
map(csv),
map(R.props(['ssn', 'firstname', 'lastname'])),
chain(findStudent),
chain(checkLengthSsn),
lift(cleanInput));
// 當值行 showStudent(ssn), 我們會跑一遍所有邏輯驗證,接著在執行資料取得。
// 執行成功後,我們會等待被手動觸發(lazy function)來寫到頁面上
showStudent(studentId).run(); //-> 444-44-4444, Alonzo, Church
// or
let student = showStudent(studentId)
student.run(); //-> 444-44-4444, Alonzo, Church
```
- 提醒:通常我們習慣將 Impure 的程式放到最後面來做執行,避免中間因為未知的影響導致錯誤發生。
``` javascript
// 最後我們來看一下如果沒有使用 Monads 那結果會是怎樣呢?
function showStudent(ssn) {
if(ssn != null) {
ssn = ssn.replace(/^\s*|\-|\s*$/g, '');
if(ssn.length !== 9) {
throw new Error('Invalid Input');
}
let student = db.get(ssn);
if (student) {
document.querySelector(`#${elementId}`).innerHTML =
`${student.ssn},
${student.firstname},
${student.lastname}`;
}
else {
throw new Error('Student not found!');
}
}
else {
throw new Error('Invalid SSN!');
}
}
// 整個程式會充滿 side effects,缺少模組化,還有命令式的錯誤處理,對於不管是 unit testing 或是維護都會有相當高的成本。
// 更詳細的介紹會等下一張由其他人來協助進一步說明。
```
### :thought_balloon: 5.5 總結
1.面向對象的代碼中,抛出異常的机制会導致函数變得不纯,同時也對調用者施加了很大的責任,需要提供足夠的try-catch邏輯来處理異常。
2.容器化模式被用来創建無副作用的代碼,通過將可能的變異封装在一個单一的引用透明过程下。
3.使用Functor將函数映射到容器中,以便以無副作用和不可變的方式訪問和修改对象。
4.Monad 是一種函数式编程的設計模式,通過在函数之間编排數據的安全流動来降低應用程序的复雜性。
5.弹性和健壮的函數组合使用了 Maybe、Either 和 IO 等 Monad 類型的交錯使用。