--- title: Functional Programming 第八章 By Daniel tags: F/E 分享, Functional Programming ###### tags: `F/E 分享`, `Functional Programming` --- ## Challenges of asynchronous code 書上提到非同步程式主要會遇到幾個問題 - The creation of temporal dependencies among your functions\ 在函式之間創建時間依賴關係。 - The inevitable fall into a callback pyramid\ 不可避免地陷入 callback 金字塔的情况。 - An incompatible mix of synchronous and asynchronous code\ 不兼容的同步和非同步程式混合。 ### temporal dependency Temporal coupling, temporal cohesion 我想要做的事必須先等某件事完成,在同步的狀態下只要調動執行順序即可,在非同步的情況變得複雜,因為我們不確定到底何時完成。 ```javascript let students = null; // 無法得知 getJSON 何時完成,導致該流程會有問題 getJSON('/students', function(studentObjs) { students = studentObjs; }, function (errorObj){ console.log(errorObj.message); } ) showStudents(students); ``` ### Callback callback 是非同步的一個解決方案之一(用途不只有處理非同步),白話文解釋 callback 在做的事情就是「我先跟妳說我拿到值後要做什麼(就是 Task 的概念),你準備好了幫我執行」,所以前一個範例應該改成 ```javascript getJSON('/students', function(studentObjs) { // 拿到值幫我做這件事 showStudents(studentObjs); }, function (errorObj){ // 失敗了幫我做這件事 console.log(errorObj.message); } ) ``` ### Callback hell, Callback pyramid Callback 似乎解決了非同步問題,但衍生出另一個問題,如果依賴鏈過長會有 Callback hell, Callback pyramid 問題。 \ 假設我有 a, b, c 三件非同步的事,依賴順序是 a → b → c,那程式大概會長這樣 ```javascript a(params, (data) => b(data, (data2)=> c(data2, (data3) => console.log(data3)))) ``` ![callback hell](https://hackmd.io/_uploads/S1TlQVIsn.jpg) ### Nested control flow 上述都是一條鏈的流程,但實際程式更複雜,我們還有控制流(control flow) ```javascript a(function (dataA){ if (dataA) { b(dataA, function (dataB){ if (dataB) { c(dataB, function (dataC){ // nested control flow }) } else { // ... } }) } else { // ... } }) ``` ### CPS (continuation-passing style) 為了解決 Nested control flow 的問題,書上使用了一個叫 CPS 的手法,聽起來很潮的名詞,但巢狀的問題永遠都在,這是無法避免的,因為我們要的邏輯就是這樣,只是用另一種寫法把它攤平: ```javascript function taskC(dataC){ // ... } function taskB(dataB){ if (dataB) { c(dataB, taskC); } else { // ... } } function taskA(dataA){ if (dataA) { b(dataA, taskB); } else { // ... } } a(taskA); ``` 這樣每個 task 就只會有一層的巢狀,減少閱讀負擔,以及提升程式整潔度 CPS 就像是一直傳入 Callback 接續前一個函式所處理好的資訊,直到沒有事情要處理為止,唯一的詬病是一旦採用 CPS 整個程式撰寫風格會被迫改變。 CPS 更嚴格的定義是將所有回傳改成 callback 形式,並且是尾調用 [Continuation-passing style - Wikipedia](https://en.wikipedia.org/wiki/Continuation-passing_style) 在 express.js 也能看到 CPS 身影 [Express 4.x - API Reference (](https://expressjs.com/en/4x/api.html#app.get.method)[expressjs.com](expressjs.com)[)](https://expressjs.com/en/4x/api.html#app.get.method) node.js std API 也是 CPS 形式 [Usage and example | Node.js v18.16.1 Documentation (](https://nodejs.org/dist/latest-v18.x/docs/api/synopsis.html)[nodejs.org](nodejs.org)[)](https://nodejs.org/dist/latest-v18.x/docs/api/synopsis.html) 小知識:Monad 的 `flapMap` 某種程度上也化簡了疊層問題,目的是為了讓我專注在邏輯本身,減少重複判斷問題 **Array** ```typescript // 假設我們要將 [1, 2, 3] 轉成 [1, 1, 2, 2, 3, 3] const arr = [1, 2 ,3]; // 步驟一 // Array<number> -> Array<Array<number>> const mapedArr = arr.map((item) => [item, item]) // 步驟二 因為多包一層所以要攤平 // Array<Array<number>> -> Array<number> const result = mapedArr.flat() // flatMap 就是 flat 跟 map 的組合 const result2 = arr.flatMap((item) => [item, item]) ``` **Option (引用 fp-ts)** ```typescript // 我要知道一個未知的值是否是偶數,是就給我 Some<number> 不是就 None const getNumber = (value: unknown): Option<number> => typeof value === 'number' ? Option.some(value) : Option.none(); const evenNumber = (value: number): Option<number> => value % 2 === 0 ? Option.some(value) : Option.none() const num = 10; const optionValue = getNumber(num) // 步驟一 // Option<number> -> Option<Option<number>> const optionEvenNumWrap = Option.map(evenNumber)(optionValue) // 步驟二 // Option<Option<number>> -> Option<number> const optionEvenNum = Option.flatten(optionEvenNumWrap) // flatMap 就是 flat 跟 map 的組合 const optionEvenNum = Option.flatMap(evenNumber)(optionValue) ``` --- ## 8\.2 First-class asynchronous behavior with promises 透過前面的優化,雖然確實解決可讀性問題,但要達到 FP 理想的樣子還有一段距離,還有幾件事情要改善: - Using **composition** and **point-free** programming - Flattening the nested structure into a more linear flow\ 將巢狀結構展開為更線性的流程 - Abstracting the notion of temporal coupling so that you don’t need to be concerned with it\ 抽象化時間耦合的概念,使您不需要擔心它 - Consolidating error handling to a single function rather than multiple error callbacks so that it’s not in the way of the code\ 將錯誤處理整合到一個函數中,而不是多個錯誤 callback 函數中,以避免妨礙程式進行。 #### point-free ? 省略參數的函式,只專注動作的組合而不是我的參數是什麼 假設我要一個兩數平方相加的函式 $$ f(x,y) = x^2 + y^2 $$ ```typescript const square = (x: number) => x * x; const add = (x: number, y: number) => x + y; // 一般寫法 const sumOfSquare = (x: number, y: number) => { const a = square(x) const b = square(y) return add(a, b) } // point-free const sumOfSquare = (x: number, y: number) => add(square(x), square(y)) // 利用 Ramda import * as R from 'ramda' const sumOfSquare = R.useWith(add, [square, square]) ``` ```mermaid flowchart TD x --> square1[square] y --> square2[square] subgraph ide1 [useWith] square1 --> add square2 --> add end add --> result ``` 可以發現 point-free 風格更接近數學式,只專注運算式的展示,而不是運算過程的**步驟**。 ### Promise Promise 解決了上述幾個問題 #### 將巢狀結構展開為更線性的流程 ```typescript Promise.resolve(1) .then((x) => Promise.resolve(2 + x)) //會自動解開 Promise 傳遞給下一個 then .then((x) => Promise.resolve(3 + x)) // 6 ``` #### 抽象化時間耦合的概念,使您不需要擔心它 Promise 其實是一個簡易的**狀態機**,透過大量的撰寫非同步程式的經驗後,你可以從中看出我們主要在處理幾個問題 - 開始了不知道何時結束 - 結束了不知道成功還失敗 - 成功跟失敗的處理流程不知道安插在哪裡 ```mermaid stateDiagram-v2 [*] --> Promise state Promise { [*] --> pending pending --> rejected: reject pending --> fullfilled: fullfill } ``` ![Promise state machine](https://hackmd.io/_uploads/HJWBQELi2.png) 而 Promise 解決了上述問題,你只需要跟他說,什麼時候開始、成功的時候做什麼、失敗的時候做什麼,剩下的 Promise 會幫你在對的時機做完事情 #### 將錯誤處理整合到一個函數中,而不是多個錯誤 callback 函數中,以避免妨礙程式進行 ```typescript // 中途出錯會直接到 catch,而不是讓後續流程自己處理 Promise.resolve(1) .then((x) => Promise.resolve(x + 1)) .then((x) => Promise.reject('error')) .then((x) => Promise.resolve(x + 1)) .catch((err) => console.log(err)) ``` 書上小錯誤 Promise 嚴格來說並不是 Monad,他只是長得很像而已 而 fp-ts 的 [`Task`](https://gcanti.github.io/fp-ts/modules/Task.ts.html#task-interface) 雖然只是做了一層包裝但他卻是個 Monad ```typescript interface Task<T> { (): Promise<T> } ``` 延伸閱讀 [No, Promise is not a monad (](https://buzzdecafe.github.io/2018/04/10/no-promises-are-not-monads)[buzzdecafe.github.io](buzzdecafe.github.io)[)](https://buzzdecafe.github.io/2018/04/10/no-promises-are-not-monads) --- ## 8\.3 Lazy data generation 因為 Lazy 在 FP 有他的特殊用意,所以我將 Lazy 跟 generator function 分開來說 ### Lazy Evolution 惰性求值特性是「只有在所有參數備齊時才真正執行」,所以我可以不用一次給完所有參數 Ramda 所有函式都可以做到惰性求值 ```javascript import * as R from 'ramda' const double = (n) => n * 2 const doubleArr = R.map(double) doubleArr([1, 2, 3, 4]) // [2, 4, 6, 8] ``` Lazy Evolution 在 Haskell 是預設行為 ```haskell double :: Int -> Int double x = x * 2 doubleArr :: [Int] -> [Int] doubleArr = map double ``` 老師都叫我們套公式,我們都怎麼套公式 $$ f = x^2 + 2xy + y^2 $$ 我們通常都會把 $x$ 跟 $y$ 湊齊了才放進公式裡計算,我們很少拿一部分的值先算一部分出來(在計算過程中會這樣做),因為這樣我們還不會得到結果,不如我們就等到所有參數到齊了在做比較省事。 $$ x=5, f = 25 + 10y + y^2 $$ #### data first、data last? 你可能會注意到許多 FP function 會把參數傳遞順序對調,某種成度上是為了讓 function 本身不會直接被 data 綁死,並且因為 curry 的關係能使 function 只傳一部分並產生另一個 function,進而提高重用性。 ```javascript import * as _ from 'lodash'; import * as R from 'ramda' _.map([1, 2, 3], (x) => x * 2) // data first R.map((x) => x * 2, [1, 2, 3]) // data latest ``` ```javascript import * as R from 'ramda'; import * as _ from 'lodash' const sum = (arr) => _.reduce(arr, (acc, num) => acc + num, 0) const sum = (arr) => arr.reduce((acc, num) => acc + num, 0) const product = (arr) => arr.reduce((acc, num) => acc * num, 1) const sum = R.reduce(R.add, 0) const product = R.reduce(R.multiply, 1) ``` ### 用於耗時任務的 Lazy Type 在 fp-ts 最常出現的三種 lazy type definition ```typescript interface LazyArg<A> { (): A } interface IO<A> { (): A } interface Task<A> { (): Promise<A> } ``` 你會發現三種都刻意的在值上面多掛一層看似沒有意義的空 function 上去 我們可以用兩種說法來解釋 Lazy 的目的「盡可能拖延這一切」跟「非必要時刻我不計算」 #### 非必要時刻我不計算 ```typescript const getBooleanA => (x) => () => //... 可能是耗時的運算 const getBooleanB => (x) => () => //... 可能是耗時的運算 function fn(booleanA, booleanB){ // 假如 booleanA 結果是 false,那 booleanB 就不必執行了 if(booleanA() && booleanB()){ } } fn(getBooleanA(1), getBooleanB(2)) ``` #### 盡可能拖延這一切 ```typescript const double = (n: number): number => n * 2; const mainIO = pipe( IO.of(1), // 可以是任何同步操作 IO.map(double), IO.map(double), IO.map(double), IO.map(double), IO.map(double), IO.map(double), IO.map(double), IO.map(double), IO.map(double), IO.map(double) ); const mainTask = pipe( Task.of(1), // 可以是任何非同步操作 Task.map(double), Task.map(double), Task.map(double), Task.map(double), Task.map(double), Task.map(double), Task.map(double), Task.map(double), Task.map(double), Task.map(double) ); ``` 以上兩者在程式開始時接未執行,你必須明確表明 `mainIO()` 或 `mainTask()` 這一切才會動作,彷彿我們在推遲這一切的發生,所以我們可以這樣理解,「functional programming 會刻意的延遲 side effect 動作的發生,只有在確切需要當下執行時才**顯式地**執行它。」,這樣可以促使開發者意識到並且避免無意義的資源浪費。 #### 隱式(implicit)、顯式(explicit)? > #### 隱式 > > 指的是某些操作或行為是由編譯器或運行時環境自動完成的,而不需要你明確地指定 > #### 顯式 > > 指的是你明確地指定了某些操作或行為。 ```javascript const num = 10; const num2 = num; // 隱式複製 import * as R from 'ramda' const obj = { // ... } const obj2 = R.clone(obj) // 顯式複製 ``` Rust ```rust let num: i32 = 20; let num2: i32 = num; // 隱式複製 let arr: Vec<i32> = vec![1, 2, 3]; let arr2: Vec<i32> = arr.clone(); // 顯式複製 ``` 所以我們使用 FP 手法寫程式某種程度上像是在堆疊一個 app 的公式 什麼時候會執行?就是參數齊全的時候。 延伸閱讀 [How to deal with dirty side effects in your pure functional JavaScript (](https://jrsinclair.com/articles/2018/how-to-deal-with-dirty-side-effects-in-your-pure-functional-javascript/)[jrsinclair.com](jrsinclair.com)[)](https://jrsinclair.com/articles/2018/how-to-deal-with-dirty-side-effects-in-your-pure-functional-javascript/) ### Generator function > Generators are functions that can be **exited** and **later re-entered**. Their context (variable bindings) will be saved across **re-entrances.** [MDN definition](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function\*#description) generator function 起手式 ```typescript function* fn(){ yield 10 yield 20 } const gen = fn(); gen.next(); // { value: 10, done: false } gen.next(); // { value: 20, done: false } gen.next(); // { value: undefined, done: true } ``` 還可以疊層 ```typescript function* fn(){ yield 10 yield 20 } function* fn2(){ yield 1 yield fn() yield 2 } const gen = fn2(); gen.next() // { value: 1, done: false } gen.next() // { value: 10, done: false } gen.next() // { value: 20, done: false } gen.next() // { value: 2, done: false } gen.next() // { value: undefined, done: true } ``` 平常就存在於我們看不到的地方 #### Symbol.iterator 擁有該標記型別都是由 generator function 製作的,我們常用的 [spread operation](./https!developer.mozilla.org!en-US!docs!Web!JavaScript!Reference!Operators!Spread_syntax#description%20ed9d7b93-f82d-4de5-8ecc-3746a3101204.md "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#description"), [`for…of`](./https!developer.mozilla.org!en-US!docs!Web!JavaScript!Reference!Statements!for...of%2070bf73fa-2e7c-4f0c-bd12-14b10f685712.md "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of") 就是在 call Symbol.iterator 函數。 #### 誰擁有該標記? - [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/@@iterator) - [TypedArray](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/@@iterator) - [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/@@iterator) - [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/@@iterator) - [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/@@iterator) #### 我們可以自己加嗎? 可以 ```typescript // 獲得一個範圍內的偶數陣列 const evenNumberList = { start: 0, end: 20, *[Symbol.iterator]() { const start = this.start % 2 === 0 ? this.start : this.start + 1; for (let i = start; i <= this.end; i += 2) { yield i; } }, }; [...evenNumberList] // [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20] ``` 不過其實這樣寫就可以了 ```typescript import * as R from 'ramda'; const evenNumberList = R.range(0, 20).filter((i) => i % 2 === 0) ``` 除非今天你有一個自己的特殊資料結構想要可以被迭代,不然平常幾乎用不到 ```javascript class Node { constructor(val) { this.val = val; this.left = null; this.right = null; } setLeft(node = null) { this.left = node; } setRight(node = null) { this.right = node; } // inorder taversal 中序遍歷 *[Symbol.iterator]() { if (this.left) { yield* this.left; } yield this.val; if (this.right) { yield* this.right; } } } const node1 = new Node('1'); const node2 = new Node('2'); const node3 = new Node('3'); node2.setLeft(node1); node2.setRight(node3); [...node2] // ['1', '2', '3'] ``` ```mermaid flowchart TD 2 --- 1 2 --- 3 ``` 雖然用不太到,但我們還是可以了解它通常在處理什麼 - 高效率處理大型資料 - 處理無限序列 - 在兩個函數之間傳遞訊息 `async await` 語法糖是由 generator function 演變而來 [function\* - JavaScript | MDN (](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function\*#description)[mozilla.org](mozilla.org)[)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function\*#description) > Generators in JavaScript — especially when combined with Promises — are a very powerful tool for asynchronous programming as they mitigate — if not entirely eliminate -- the problems with callbacks, such as [++Callback Hell++](http://callbackhell.com/) and [++Inversion of Control++](https://frontendmasters.com/courses/rethinking-async-js/callback-problems-inversion-of-control/). However, an even simpler solution to these problems can be achieved with [++async functions++](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function). ```javascript const asyncDo = (runTasks) => (...args) => { const generator = runTasks(...args); const resolve = (next) => { if (next.done) { return Promise.resolve(next.value); } return next.value.then((data) => resolve(generator.next(data))); }; return resolve(generator.next()); }; const asyncFn = asyncDo(function* () { const v1 = yield Promise.resolve(123); const v2 = yield Promise.resolve(123 + v1); return v2; }); asyncFn().then((res) => console.log(res)); // 246 ``` 延伸思考 為什麼需要 `async await`?Promise 本身有什麼痛點? 延伸閱讀 <https://jrsinclair.com/articles/2022/why-would-anyone-need-javascript-generator-functions/> iterator 不只存在於 Javascript,因為他算是一種共同設計模式,稱作 iterator pattern,實作方法可能有所不同,但 API 設計都相當接近。 還有哪裡可以見到 iterator 的身影 - [Rust iterator](https://rust-lang.tw/book-tw/ch13-02-iterators.html?highlight=iterator#iterator-%E7%89%B9%E5%BE%B5%E8%88%87-next-%E6%96%B9%E6%B3%95) - [Scala iterator](https://docs.scala-lang.org/overviews/collections/iterators.html) - [Python iterator](https://wiki.python.org/moin/Iterator) - [C++ iterator](https://en.cppreference.com/w/cpp/iterator) - [Java Iterator](https://docs.oracle.com/javase/8/docs/api/java/util/Iterator.html) --- ## 8\.4 Functional and reactive programming with RxJS [reactive programming](https://reactivex.io/intro.html) ### RxJS 核心思想 > RxJS is a library for composing **asynchronous** and **event-based** programs by using observable sequences. It provides one core type, the [Observable](https://rxjs.dev/guide/observable), satellite types (Observer, Schedulers, Subjects) and operators inspired by `Array` methods ([`map`](https://rxjs.dev/api/index/function/map), [`filter`](https://rxjs.dev/api/index/function/filter), [`reduce`](https://rxjs.dev/api/index/function/reduce), [`every`](https://rxjs.dev/api/index/function/every), etc) to allow handling asynchronous events as collections. 簡潔一句話就是「觀察資料的變化並做出一系列反應 (react) 」 - 這個概念是否似曾相似 😏? 沒錯,就是 React、Vue 等前端框架在做的事,只是前端框架專注在反應 UI,而 Rx 則不受限,這也是為什麼當我們把 React、Vue 與 Rxjs 結合使用時會有不協調感,兩者做的事情重複了 Rx 由三個核心構成 - Observable - Observer - Operators ### Observable 建立一個可被觀察的對象,該對象的每次變化都會通知所有觀察者 ```javascript import { Observable } from 'rxjs'; const observable = new Observable((subscriber) => { subscriber.next(1); subscriber.next(2); subscriber.next(3); setTimeout(() => { subscriber.next(4); subscriber.complete(); }, 1000); }); ``` 利用 Observable 這個基礎模型,依不同情境包裝而成的函式 ```javascript import { fromEvent, of } from 'rxjs'; // 監聽 document 點擊 const clicks = fromEvent(document, 'click'); of(10, 20, 30) .subscribe({ next: value => console.log('next:', value), error: err => console.log('error:', err), complete: () => console.log('the end'), }); // Outputs // next: 10 // next: 20 // next: 30 // the end ``` <https://github.com/ReactiveX/rxjs/blob/c2cf0c4a9dfe5904d65e998b4831909c082f78f7/src/internal/observable/fromEvent.ts#LL239C25-L239C25> \ 嘗試自己利用 Observable 實作一個 `of` ```javascript import { Observable } from 'rxjs'; const of = (...args) => { return new Observable((subscriber) => { for (const nextValue of args) { subscriber.next(nextValue); } subscriber.complete(); }); }; ``` ### Observer 建立一個觀察者,分別撰寫在 **資料變化時**、**錯誤發生**以及**結束時**所要做的事 ```javascript const observer = { next: x => console.log('Observer got a next value: ' + x), error: err => console.error('Observer got an error: ' + err), complete: () => console.log('Observer got a complete notification'), }; ``` 訂閱 Observable ```javascript import { Observable } from 'rxjs'; const observable = new Observable((subscriber) => { subscriber.next(1); subscriber.next(2); subscriber.next(3); setTimeout(() => { subscriber.next(4); subscriber.complete(); }, 1000); }); const observer = { next: x => console.log('Observer got a next value: ' + x), error: err => console.error('Observer got an error: ' + err), complete: () => console.log('Observer got a complete notification'), } observable.subscribe(observer); ``` ### Operators 先將 Observable 的資訊先進行一段處理再分發給 Observer ```javascript import { of, map, take } from 'rxjs'; of(1, 2, 3) .pipe(map((x) => x * x), take(2)) .subscribe((v) => console.log(`value: ${v}`)); ``` ### 自己實作簡易模型 ```javascript class Observable { constructor(fn = () => {}) { this._fn = fn; this._operators = []; } pipe(...fns) { this._operators = fns; return this; } subscribe(observer) { let completed = false; let isError = false; const operators = this._operators; const newObserver = typeof observer === 'function' ? { next: (value) => { if (completed || isError) return; // operator flow const newValue = operators.reduce((acc, fn) => fn(acc), value); observer(newValue); }, complete: () => { completed = true; }, err: (error) => { isError = true; }, } : { next: (value) => { if (completed || isError) return; // operator flow const newValue = operators.reduce((acc, fn) => fn(acc), value); observer.next(newValue); }, complete: () => { completed = true; observer.complete(); }, err: (error) => { isError = true; observer.error(error); }, }; this._fn(newObserver); } } ``` ```javascript const ob = new Observable((subscriber) => { subscriber.next(1); subscriber.next(1); setTimeout(() => { subscriber.next(3); }, 1000); subscriber.next(2); }); ob.subscribe({ next(v) { console.log(v); }, complete(v) { console.log(v); }, err(v) { console.log(v); }, }); // logs // 1 // 1 // 2 // 3 ``` ## 總結 雖然這本書有些內容跟不上時代也有許多錯誤,不過我們可以透過對比新舊函式庫與糾錯的過程來鞏固我們的知識,透過這本書我們已經對 FP 有基礎的認知,再來就是實戰練習,以下是我認為比較好的練習過程 - 練習寫 pure function - 開始分辨程式純與不純的部分 - 嘗試利用 pipe, flow, compose 組合你的 pure function - 拆解一個複雜函式並將其組合 進階 - 將純邏輯操作與 side effect 隔離個別處理 - 嘗試使用 Either, Option 處理錯誤與空值 ### 現代 JS/TS 相關 Functional Programming library - fp-ts (把 FP 概念實現最完整的 lib) - ramda - lodash/fp - Effect-ts (這整個系統被拆成很多個模組) [Welcome to Effect](https://www.effect.website/) - [Effect/data](https://github.com/Effect-TS/data) (各種資料結構的實作) - [Effect/io](https://github.com/Effect-TS/io) (基於 Fiber 模型概念處理 side effect) - [Effect/scheme](https://github.com/Effect-TS/schema) (用於資料格式驗證,與 zod, yup 用途相同) - [Effect/match](https://github.com/Effect-TS/match) (用 TS 實現的 pattern matching) - [ts-belt](https://mobily.github.io/ts-belt/)