搜尋全站文章

沒有找到相關文章

試試其他關鍵字或檢查拼寫

找到 0 篇文章 • 包含部落格、筆記、旅遊文章

Medium GitHub LinkedIn

Grokking Simplicity FP CH12 ~ CH13

作者頭像
Sam

最近發佈

5分鐘閱讀

Grokking Simplicity FP CH12 ~ CH13

《簡約的軟體開發思維:用 Functional Programming 重構程式》CH12 ~ CH13

目錄

Ch12. 利用函數走訪

Code Smell: 有相似的函式實作

  • 隱性 -> 顯性函數
  • 回呼函式

map()、filter()、reduce() 是三個常見的高階函數,讓 FP 的邏輯看起來更簡潔。

特性for 迴圈forEach()map()
語法for (let i = 0; …)array.forEach(…)array.map(…)
回傳值無 (undefined)新陣列
可否中斷是 (break, continue)
修改原陣列可手動控制通常用於修改,但不回傳新陣列不會修改(創建新陣列)
適用對象任何可迭代物件陣列陣列
主要用途高度控制,處理通用迭代對每個元素執行副作用將每個元素轉換為新形式,生成新陣列

1. map()

使用情境:將陣列中的每個元素轉換為新形式,生成新陣列。

蘋果 🍎 -> 包裝機 📦 -> 貼標的蘋果 🍎 💰

const arr = [1, 2, 3]; const newArr = arr.map((item) => { return item * 2; }); console.log(newArr);

* 陣列的值可能為 null,使用 filter()?. 來避免拋出錯誤。

練習 12-1:Mega Mart 寄送賀卡
  • customers 陣列包涵所有 customer 物件
  • 使用 map() 產生賀卡,包涵 cursomer.firstName, cursomer.lastName, cursomer.address
const customers = [ { firstName: "John", lastName: "Doe", address: "123 Main St", habbit: "reading", }, { firstName: "Jane", lastName: "Smith", address: "456 Main St", habbit: "reading", }, { firstName: "Jim", lastName: "Beam", address: "789 Main St", habbit: "reading", }, ]; const cards = "YOUR ANSWER HERE";

2. filter()

使用情境:過濾陣列中的元素,生成新陣列。

一籃蘋果 🍎 🍏 -> 篩選 🔍 -> 好蘋果 🍎

  • 業務情境: Mega Mart 想要與高消費力的顧客建立關係,因此需要過濾出高消費力的顧客。
  • 總顧客 10 人 -> 高消費 4 人 / 低消費 6 人
function selectBestCustomers(customers) { return customers.filter((customer) => { return customer.total > 1000; }); }

如何整理過於複雜得篩選邏輯:回呼函式!

業務邏輯:「最近一年內有交易、總金額超過 $1000、且 email 有驗證、VIP 等級為 gold 或 platinum 的顧客」

function isHighSpender(customer) { return customer.total > 1000; } function isRecentBuyer(customer) { const oneYearAgo = new Date(); oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); return new Date(customer.lastPurchaseDate) > oneYearAgo; } function hasVerifiedEmail(customer) { return customer.emailVerified === true; } function isVipCustomer(customer) { return ["gold", "platinum"].includes(customer.vipLevel); } function selectBestCustomers(customers) { return customers.filter((customer) => { return ( isHighSpender(customer) && isRecentBuyer(customer) && hasVerifiedEmail(customer) && isVipCustomer(customer) ); }); }
練習 12-2:Mega Mart 寄測試信件給顧客清單中的 1/3 使用者
  • 測試名單 testGroup:顧客 id 可以整除 3 的顧客
  • 非測試名單:其餘顧客
const customers = [ { id: 1, name: "John" }, { id: 2, name: "Jane" }, { id: 3, name: "Jim" }, { id: 4, name: "Jill" }, { id: 5, name: "Jack" }, ]; const testGroup = customers.filter("YOUR ANSWER HERE"); const nonTestGroup = customers.filter("YOUR ANSWER HERE");

3. reduce()

使用情境:將陣列中的元素累積為一個值。

一堆水果 🍎 🍏 -> 變成果汁 🍹

function sum(numbers) { return reduce(numbers, 0, function (total, number) { return total + number; }); } / // 將所有數字相乘 function product(numbers) { return reduce(numbers, 1, function (total, number) { return total * number; }); }

reduce() 的常見用途:

功能概念說明應用場景核心特點實際例子
復原 (Undo)透過累積歷史狀態,可以回到之前的任何狀態文字編輯器、繪圖軟體、遊戲存檔狀態堆疊、歷史記錄Word 的 Ctrl+Z 功能
重複 (Redo)在復原後,可以重新執行被復原的操作與復原配合使用的前進功能雙向狀態管理Word 的 Ctrl+Y 功能
時間移動除錯記錄每一步的執行過程,可以跳到任意時間點程式除錯、狀態分析、效能監控時間軸記錄、快照Redux DevTools 的時間旅行
審計軌跡完整記錄所有操作的詳細資訊系統安全、合規檢查、操作追蹤不可變記錄、完整性銀行交易記錄、系統日誌
按編:這邊算是自己找的例子,不在讀書會及書中提及。
// ========================================== // 1. 復原 (Undo) - 純函數式文字編輯器 // ========================================== // 純函數:執行單一編輯操作 const applyEdit = (content, edit) => { switch (edit.type) { case "INSERT": return ( content.slice(0, edit.position) + edit.text + content.slice(edit.position) ); case "DELETE": return ( content.slice(0, edit.position) + content.slice(edit.position + edit.length) ); case "REPLACE": return ( content.slice(0, edit.position) + edit.newText + content.slice(edit.position + edit.oldText.length) ); default: return content; } }; // 純函數:建立編輯歷史狀態 const createEditHistory = (edits, initialContent = "") => { return edits.reduce( (history, edit) => { const previousContent = history[history.length - 1]; const newContent = applyEdit(previousContent, edit); return [...history, newContent]; }, [initialContent] ); }; // 純函數:復原操作 const undo = (history, currentIndex) => ({ content: history[Math.max(0, currentIndex - 1)], newIndex: Math.max(0, currentIndex - 1), }); // 純函數:重複操作 const redo = (history, currentIndex) => ({ content: history[Math.min(history.length - 1, currentIndex + 1)], newIndex: Math.min(history.length - 1, currentIndex + 1), }); // 使用範例 const edits = [ { type: "INSERT", position: 0, text: "Hello" }, { type: "INSERT", position: 5, text: " World" }, { type: "REPLACE", position: 6, oldText: "World", newText: "JavaScript" }, ]; const history = createEditHistory(edits); console.log("編輯歷史:", history); // ['', 'Hello', 'Hello World', 'Hello JavaScript'] const undoResult = undo(history, 3); console.log("復原後:", undoResult); // { content: 'Hello World', newIndex: 2 } // ========================================== // 2. 重複 (Redo) - 純函數式狀態管理 // ========================================== // 純函數:建立狀態管理器 const createStateManager = (actions, initialState = null) => { return actions.reduce( (stateHistory, action) => { const currentState = stateHistory.states[stateHistory.states.length - 1]; const newState = action.reducer(currentState, action.payload); return { states: [...stateHistory.states, newState], actions: [...stateHistory.actions, action], currentIndex: stateHistory.states.length, }; }, { states: [initialState], actions: [], currentIndex: 0, } ); }; // 純函數:執行復原/重複操作 const timeTravel = (stateManager, targetIndex) => ({ ...stateManager, currentIndex: Math.max( 0, Math.min(stateManager.states.length - 1, targetIndex) ), currentState: stateManager.states[ Math.max(0, Math.min(stateManager.states.length - 1, targetIndex)) ], }); // 使用範例 - 購物車狀態管理 const cartActions = [ { type: "ADD_ITEM", payload: { id: 1, name: "iPhone", price: 999 }, reducer: (state, item) => ({ ...state, items: [...(state?.items || []), item], total: (state?.total || 0) + item.price, }), }, { type: "ADD_ITEM", payload: { id: 2, name: "MacBook", price: 1999 }, reducer: (state, item) => ({ ...state, items: [...state.items, item], total: state.total + item.price, }), }, { type: "REMOVE_ITEM", payload: { id: 1 }, reducer: (state, payload) => { const newItems = state.items.filter((item) => item.id !== payload.id); const newTotal = newItems.reduce((sum, item) => sum + item.price, 0); return { ...state, items: newItems, total: newTotal }; }, }, ]; const cartHistory = createStateManager(cartActions, { items: [], total: 0 }); console.log("購物車狀態歷史:", cartHistory.states); // 復原到第1步 const undoToStep1 = timeTravel(cartHistory, 1); console.log("復原到第1步:", undoToStep1.currentState); // 重複到第2步 const redoToStep2 = timeTravel(cartHistory, 2); console.log("重複到第2步:", redoToStep2.currentState); // ========================================== // 3. 時間移動除錯 - 純函數式執行追蹤 // ========================================== // 純函數:建立除錯快照 const createDebugSnapshot = ( step, operation, input, output, metadata = {} ) => ({ step, timestamp: new Date().toISOString(), operation, input, output, metadata, performance: { executionTime: metadata.executionTime || 0, memoryUsage: metadata.memoryUsage || 0, }, }); // 純函數:執行可追蹤的操作序列 const executeWithTimeTravel = (operations, initialData) => { return operations.reduce( (debugState, operation, index) => { const startTime = performance.now(); const result = operation.fn(debugState.currentData, operation.params); const endTime = performance.now(); const snapshot = createDebugSnapshot( index + 1, operation.name, debugState.currentData, result, { params: operation.params, executionTime: endTime - startTime, } ); return { currentData: result, snapshots: [...debugState.snapshots, snapshot], totalExecutionTime: debugState.totalExecutionTime + (endTime - startTime), }; }, { currentData: initialData, snapshots: [], totalExecutionTime: 0, } ); }; // 純函數:跳轉到特定時間點 const jumpToSnapshot = (debugState, stepNumber) => { const targetSnapshot = debugState.snapshots.find( (s) => s.step === stepNumber ); return targetSnapshot ? targetSnapshot.output : debugState.currentData; }; // 使用範例 - 數據處理管道 const dataOperations = [ { name: "FILTER_ACTIVE_USERS", fn: (users) => users.filter((user) => user.active), params: { condition: "active === true" }, }, { name: "MAP_USER_SCORES", fn: (users) => users.map((user) => ({ ...user, score: user.points * 1.5 })), params: { multiplier: 1.5 }, }, { name: "SORT_BY_SCORE", fn: (users) => [...users].sort((a, b) => b.score - a.score), params: { order: "desc" }, }, { name: "TAKE_TOP_5", fn: (users) => users.slice(0, 5), params: { limit: 5 }, }, ]; const initialUsers = [ { id: 1, name: "Alice", active: true, points: 100 }, { id: 2, name: "Bob", active: false, points: 80 }, { id: 3, name: "Charlie", active: true, points: 120 }, { id: 4, name: "Diana", active: true, points: 90 }, { id: 5, name: "Eve", active: true, points: 110 }, ]; const debugResult = executeWithTimeTravel(dataOperations, initialUsers); console.log("執行結果:", debugResult.currentData); console.log("執行快照:", debugResult.snapshots); // 跳轉到第2步查看狀態 const step2State = jumpToSnapshot(debugResult, 2); console.log("第2步狀態:", step2State); // ========================================== // 4. 審計軌跡 - 純函數式操作日誌 // ========================================== // 純函數:建立審計記錄 const createAuditEntry = ( action, user, resource, oldValue, newValue, metadata = {} ) => ({ id: crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36), timestamp: new Date().toISOString(), action, user: { id: user.id, name: user.name, role: user.role, ip: user.ip || "unknown", }, resource: { type: resource.type, id: resource.id, name: resource.name, }, changes: { before: oldValue, after: newValue, }, metadata: { userAgent: metadata.userAgent || "unknown", sessionId: metadata.sessionId || "unknown", ...metadata, }, hash: `${action}-${user.id}-${resource.id}-${Date.now()}`, }); // 純函數:處理業務操作並生成審計軌跡 const executeBusinessOperations = (operations, initialState) => { return operations.reduce( (auditState, operation) => { const oldValue = auditState.currentState[operation.resourceId]; const newValue = operation.transformer(oldValue, operation.payload); const auditEntry = createAuditEntry( operation.action, operation.user, operation.resource, oldValue, newValue, operation.metadata ); return { currentState: { ...auditState.currentState, [operation.resourceId]: newValue, }, auditTrail: [...auditState.auditTrail, auditEntry], }; }, { currentState: initialState, auditTrail: [], } ); }; // 純函數:查詢審計軌跡 const queryAuditTrail = (auditTrail, filters = {}) => { return auditTrail.filter((entry) => { return Object.entries(filters).every(([key, value]) => { if (key === "user") return entry.user.id === value; if (key === "action") return entry.action === value; if (key === "resource") return entry.resource.type === value; if (key === "dateFrom") return new Date(entry.timestamp) >= new Date(value); if (key === "dateTo") return new Date(entry.timestamp) <= new Date(value); return true; }); }); }; // 使用範例 - 銀行帳戶操作審計 const bankOperations = [ { action: "DEPOSIT", user: { id: "user123", name: "John Doe", role: "customer", ip: "192.168.1.1", }, resource: { type: "account", id: "acc001", name: "Checking Account" }, resourceId: "acc001", payload: { amount: 1000 }, transformer: (currentBalance, payload) => (currentBalance || 0) + payload.amount, metadata: { branch: "downtown", teller: "jane_smith" }, }, { action: "WITHDRAW", user: { id: "user123", name: "John Doe", role: "customer", ip: "192.168.1.1", }, resource: { type: "account", id: "acc001", name: "Checking Account" }, resourceId: "acc001", payload: { amount: 200 }, transformer: (currentBalance, payload) => currentBalance - payload.amount, metadata: { atm: "atm_007", card: "**** 1234" }, }, { action: "TRANSFER", user: { id: "user123", name: "John Doe", role: "customer", ip: "192.168.1.1", }, resource: { type: "account", id: "acc001", name: "Checking Account" }, resourceId: "acc001", payload: { amount: 300, toAccount: "acc002" }, transformer: (currentBalance, payload) => currentBalance - payload.amount, metadata: { transferType: "internal", recipient: "acc002" }, }, ]; const bankAudit = executeBusinessOperations(bankOperations, { acc001: 0 }); console.log("帳戶最終狀態:", bankAudit.currentState); console.log("完整審計軌跡:", bankAudit.auditTrail); // 查詢特定用戶的操作記錄 const userOperations = queryAuditTrail(bankAudit.auditTrail, { user: "user123", }); console.log("用戶 user123 的操作記錄:", userOperations); // 查詢所有轉帳操作 const transferOperations = queryAuditTrail(bankAudit.auditTrail, { action: "TRANSFER", }); console.log("所有轉帳操作:", transferOperations); // ========================================== // 額外:組合式使用範例 // ========================================== // 結合四種功能的完整狀態管理器 const createCompleteStateManager = (actions, initialState) => { return actions.reduce( (manager, action, index) => { const startTime = performance.now(); const oldState = manager.states[manager.states.length - 1]; const newState = action.reducer(oldState, action.payload); const endTime = performance.now(); // 建立快照 (時間移動除錯) const snapshot = createDebugSnapshot( index + 1, action.type, oldState, newState, { executionTime: endTime - startTime } ); // 建立審計記錄 const auditEntry = createAuditEntry( action.type, action.user || { id: "system", name: "System" }, action.resource || { type: "state", id: "app", name: "Application State", }, oldState, newState ); return { // 狀態歷史 (支援復原/重複) states: [...manager.states, newState], currentIndex: manager.states.length, // 除錯快照 (時間移動除錯) snapshots: [...manager.snapshots, snapshot], // 審計軌跡 auditTrail: [...manager.auditTrail, auditEntry], // 效能統計 totalExecutionTime: manager.totalExecutionTime + (endTime - startTime), }; }, { states: [initialState], currentIndex: 0, snapshots: [], auditTrail: [], totalExecutionTime: 0, } ); }; // 使用範例 const appActions = [ { type: "LOGIN", payload: { userId: "user123", sessionId: "sess_abc" }, reducer: (state, payload) => ({ ...state, user: payload, isLoggedIn: true, }), user: { id: "user123", name: "John Doe", role: "user" }, }, { type: "UPDATE_PROFILE", payload: { name: "John Smith", email: "john.smith@example.com" }, reducer: (state, payload) => ({ ...state, user: { ...state.user, ...payload }, }), user: { id: "user123", name: "John Doe", role: "user" }, }, ]; const completeManager = createCompleteStateManager(appActions, { user: null, isLoggedIn: false, }); console.log("完整狀態管理器:", { currentState: completeManager.states[completeManager.currentIndex], stateHistory: completeManager.states, debugSnapshots: completeManager.snapshots, auditTrail: completeManager.auditTrail, performance: { totalTime: completeManager.totalExecutionTime }, });

章節提問

  1. 解決 null 的方法?
  • 使用不具有 null 的值
  • 使用 filter 過濾掉 null
  1. map() 和 filter() 的差異?
  • map() 會將每個元素轉換為新形式,生成新陣列。
  • filter() 會過濾掉不符合條件的元素,生成新陣列。

Ch13. 串連函數式工具

章節重點

  • 將函數式工具 串起來 成為多步驟的鏈式操作 (chain) 來處理複雜的任務
  • 每個步驟是一項簡單操作,易讀、易撰寫。
  • 為了寫函式鏈的下一步,有時需先產生新資料或擴增既有的資料。
    • 將隱性訊息,表示成顯性資料。

鏈式操作:將多個函數串接在一起,形成一個新的函數。

當業務情境變得複雜,需要將 map()、filter()、reduce() 串接在一起,形成一個新的函數。

13.1 計算高消費力的最高消費金額

  • 總顧客: 10 人 -> 高消費 4 人 / 低消費 6 人
  • 需要找到高消費力,且消費次數 3 次以上的顧客,並計算其最高消費金額
function biggestPurchaseBestCustomers(customers) { var bestCustomers = filter(customers, function (customer) { return customer.purchases.length > 3; }); var biggestPurchases = map(bestCustomers, function (customer) { return reduce( customer.purchases, { total: 0 }, function (biggestSoFar, purchase) { if (biggestSoFar.total > purchase.total) { return biggestSoFar; } else { return purchase; } } ); }); return biggestPurchases; }

問題:巢狀結構不好理解

  1. 命名高階函數
function biggestPurchaseBestCustomers(customers) { var bestCustomers = selectBestCustomers(customers); // 拆成高階函數 var biggestPurchases = getBiggestPurchase(bestCustomers); // 拆成高階函數 return biggestPurchases; } function isBestCustomer(customer) { return customer.purchases.length > 3; } function findBiggestPurchase(biggestSoFar, purchase) { if (biggestSoFar.total > purchase.total) { return biggestSoFar; } else { return purchase; } } function getBiggestPurchase(customer) { return reduce(customer.purchases, { total: 0 }, findBiggestPurchase); }
  • 缺點:如果直接命名成高階函數,無法重複使用。
  1. 依照步驟,命名回呼函數
function biggestPurchaseBestCustomers(customers) { var bestCustomers = filter(customers, isBestCustomer); var biggestPurchases = map(bestCustomers, getBiggestPurchase); return biggestPurchases; } // 將複雜的業務邏輯拆成回呼函數,供 filter()、map()、reduce() 使用 function isBestCustomer(customer) { return customer.purchases.length > 3; } function findBiggestPurchase(biggestSoFar, purchase) { if (biggestSoFar.total > purchase.total) { return biggestSoFar; } else { return purchase; } } function getBiggestPurchase(customer) { return reduce(customer.purchases, { total: 0 }, findBiggestPurchase); }

13.5 找出只消費過一次的顧客,以陣列回傳顧客的 email

  1. 可以使用哪些 javascript 高階函數?
點我看答案

答案是:你可以用 map()filter()

  1. 完整程式碼
點我看答案
function getFirstTimersEmails(customers) { var firstTimers = filter(customers, function (customer) { return customer.purchases.length === 1; }); var firstTimersEmails = map(firstTimers, function (customer) { return customer.email; }); return firstTimersEmails; }

流融合(stream fusion)

var names = map(customers, getFullName); var nameLengths = map(names, getLength);

流融合:

var nameLengths = map(map(customers, getFullName), getLength);

13.6 當 for loop 難以重構時

  1. 理解並重寫
  • 先讀懂 for loop 的目的
  • 再用函數式工具重構
  1. 依照線索進行重構、細化步驟
  • 無法讀懂 for loop 的邏輯
  • 根據 for loop 展示的低階訊息
  • 轉換成函式鏈

情境問題: 已經有 for loop,要如何重構?

var answer = []; var window = 5; for (var i = 0; i < numbers.length; i++) { var sum = 0; var count = 0; for (var w = 0; w < window; w++) { sum += numbers[i + w]; count++; } answer.push(sum / count); }

如何重構?

點我看提示
  • 外層迴圈:外層迴圈將 array 陣列的每個元素取出做處理
    • 使用 map()
  • 內層迴圈:內層迴圈將 array 陣列的每個元素累進成一個值
    • 使用 reduce()

重構後的程式碼

// 可以重複使用的函數 function range(start, end) { var ret = []; for (var i = start; i < end; i++) { ret.push(i); } return ret; } // 基於 functional programming 的程式碼 var window = 5; var indices = range(0, numbers.length); var subarrays = map(indices, function (i) { return range(i, i + window); }); var answer = map(subarrays, average);

走訪丟失的購物車

var itemAdded = ["shirt", "shoes", "pants","shirts"]; var shippingCart = reduce(itemAdded, {}, addOne); function addOne(shippingCart, item) { if(!cart[item]){ return additem... } }

各種練習題們

13.13 情境 :顧客購物車資料遺失,網站以陣列形式記錄所有曾出現過的商品

var itemAdded = ["shirt", "shoes", "pants", "shirts"];

情境問題:使用者的資料丟失,是否可以用上述的陣列,重建購物車?

點我看答案
var itemAdded = ["shirt", "shoes", "pants", "shirts"]; var shippingCart = reduce(itemAdded, {}, addOne); function addOne(shippingCart, item) { if(!cart[item]){ return additem... } }

練習 13-16~19:棒球練習賽系列

情境問題:Mega Mart 每年都會派員參加職業棒球比賽,教練會根據員工名單生成員工建議位置清單,並將評估資料存入 evaluations 陣列。

┌─────────────────┐ │ Megamart 員工名單 │ └─────────┬───────┘ ┌─────────────────┐ │ recommendations │ │ (推薦名單) │ └─────────┬───────┘ ┌─────────────────┐ │ evaluations 陣列 │ │ (評估資料) │ └─────────┬───────┘ ┌─────────────────┐ ┌─────────────────┐ │ 參賽人員名單 │────→│ 先發選手名單 │ └─────────────────┘ └─────────┬───────┘ ┌─────────────────┐ │ 教練給每個人的 │ │ 建議 │ └─────────────────┘
var evaluations = [ { name: "John", position: "catcher", score: 2, }, { name: "Jane", position: "pitcher", score: 3, }, { name: "Jim", position: "pitcher", score: 1, }, ]; var roaster = { pitcher: "Jane", catcher: "John", };
  1. 如何根據 evaluations 陣列,生成 roster 物件?
  2. 其他問題略,請見書本。

章節提問

  1. 麼是流融合?
  • 將 map、filter 等涵式串接再一起,避免過多中繼程式

探索更多精彩內容

繼續閱讀,了解更多技術與個人經歷的精彩文章。