在程式開發中,「副作用」指會影響外部世界的操作,例如記錄日誌、發送網路請求、存取資料庫、寫入檔案,甚至 console.log
。相對地,在記憶體中計算 1 + 1 則屬於不影響外界的純計算。
因此,程式若要真正「有用」,就離不開副作用;沒有輸出、沒有持久化、沒有畫面,軟體就毫無價值。
副作用讓系統「能做事」,但也最容易失控:
真正棘手的,往往不是程式本身是否正確運行,而是副作用伴隨而來的錯誤、併發與資源管理問題。
要打造生產等級(production‑grade)的軟體,妥善處理副作用是不可避免的;而 Effect 提供一種宣告式(declarative)的方法來處理它。
在程式中,我們用泛型外觀 Effect<Success, Error, Requirements>
來描述一段計算:它在某個需求環境 (Requirements
) 中執行,成功時產生 Success
,失敗時產生 Error
。要注意的是,Effect
是 effect library 定義的一種函數式資料型別 (data type)。它不是 TypeScript 內建的型別,而是用來抽象描述「副作用運算」的型別結構。所以雖然在 TypeScript 裡 Effect
確實還是個「型別」,但在函數式編程(FP)的語境,會特別稱它為「資料型別 (data type)」,因為它表達的不只是靜態結構,而是一個「可組合、帶行為的抽象」。
此外,Effect 是
1.「惰性(lazy)」的,只描繪要執行的過程,不直接執行。
2. 具有組合性,可以與其他 Effect 進行組合,組合後的結果也會是一個 Effect。
3. 具有 immutable(不可變)的特性,一旦被定義,程式要如何運行就被確定了,不會再改變。
因為上面三個特性,讓我們可以先以宣告式的方式組裝流程,之後再透過 Effect 模組提供的各種 run
function 來執行。這也讓我們在錯誤處理和依賴資源的管理上,可以更輕鬆準確的控制。
傳統的 Promise 與 async/await
確實改善了非同步的可讀性,但仍有明顯限制:
try/catch
能攔錯,但型別系統不會告訴你「可能發生哪些錯誤、哪些需要處理」。Effect 的設計理念是:既然副作用無法避免,就把它變成「可描述的管線節點」來組裝起來,讓副作用以可描述、可推理、可組合的方式存在。
Effect 提供三個泛型型別 Effect<Success, Error, Requirements>
:
Success
:成功回傳的結果(成功通道)Error
:可能失敗的錯誤類型(錯誤通道)Requirements
:這個副作用需要什麼(環境 / 依賴)小詞彙表(例子):
Success
範例:User
、number
、void
Error
範例:NotFoundError
、TimeoutError
、PermissionError
Requirements
範例:記錄器(Logger)、資料庫連線、環境設定(Config)把副作用封裝成 Effect<Success, Error, Requirements>
後,你可以在型別層面精準描述需求與風險,並用一組可重用的操作來安全地串接、並行、重試、超時,並確保資源取得與釋放。程式從「滿地亂飛的副作用」變成「一段段可描述、可組裝的生產線」。
requirements
顯式表達需求,呼叫端不必了解底層實作或連線細節,只要聲明需要的服務即可。requestId
、Logger
這類每個請求才會有的資訊,不需要依賴 AsyncLocalStorage(Node.js 提供的全域非同步儲存 API)。Effect 採用 service 抽象來管理這些依賴,你只需在請求的入口設定一次,內層邏輯需要時可直接從 Context 取出,而不用把參數一路往下傳遞。不是每個情境都值得導入 Effect。當你追求的是最短開發時間,或問題本身非常單純時,Effect 反而可能成為額外負擔。幾個常見情況如下。
首先,如果你的程式只涉及純計算或單純資料轉換(沒有任何 I/O),像是字串與日期處理、資料驗證或演算法實作,Effect 帶來的抽象就未必合算。這類邏輯本來就容易以純函式表達,可靠性問題也有限。
其次,在一次性腳本、極簡 demo、或短命的 POC 階段,目標是「快速得到回饋」。此時長期的可靠性、觀測性與可維護性不是優先,導入 Effect 的學習與設置成本通常超過收益。
再者,若問題空間很簡單,或外部平台已提供你需要的穩定機制(如自動重試、超時、熔斷,就不必再自建一套 Effect 化的可靠性模型;把握邊際效益即可。有一種常見情境是「邊界很薄,且由框架/SDK 接管副作用」。所謂接管,指的是 SDK 內部已處理了 HTTP/連線池、認證、重試、超時與錯誤分類,並以一致的回傳格式暴露結果。你的程式只負責把請求參數交給 SDK,再把回傳值往上層傳遞。例如呼叫雲端儲存或金流的官方 SDK:你通常只需 await sdk.doSomething()
,其間的網路可靠性、資源管理與錯誤分類多半已被 SDK 標準化。若你的業務邏輯在這層幾乎不加工,於此處再以 Effect 建模,就可能沒有太多實質收益。
最後,還有組織面的考量:若團隊目前不具備 Effect 經驗,或專案處於高度趕工階段,學習與遷移成本可能壓過短期回報。此時維持現狀,並在關鍵路徑做最小增量優化,通常更務實。
實務上也可以採「混合導入」:把 Effect 用在 I/O 密集、需要取消/超時/重試/資源安全保障的核心路徑;其他輕量路徑就維持現有的 Promise/async 寫法,避免全面改寫的風險。
簡易判斷清單:
unknown
)?Logger
、requestId
、權限)?在學習一個新工具時,了解趨勢能幫助我們保持動力。下圖為 GitHub 上 Effect 的星標數趨勢;自 2023 年下半年起成長明顯,值得關注與評估。
接下來我們會正式開始講解 Effect 的語法。
參考資料: