---
# System prepended metadata

title: Functional Programming 第五章 針對複雜應用的設計模式
tags: [F/E 分享, ' Functional Programming']

---

---
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的結構可以存儲對象到右側或帶有錯誤訊息到左側
這樣可以返回單一的值,或者再發生故障的情況下返回錯誤訊息
![](https://hackmd.io/_uploads/rkyUAfoL3.png)

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
![](https://hackmd.io/_uploads/Bk_WL0cUn.png)


1.輸入
2.查學生紀錄
3.將學生資訊添加到HTML頁面

首先確保執行第一個函數把結果包裹一個適合的Monad(Maybe,Either)處理錯誤
![](https://hackmd.io/_uploads/HJ9S80cIn.png)


#### 用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函數的數據流
![](https://hackmd.io/_uploads/HkuqumiUh.png)

在findstudent不成功的情況下,對其於部分的影響
管道中任何組件的故障都會被跳過
![](https://hackmd.io/_uploads/B17iuXs82.png)

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 類型的交錯使用。
