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