如何傳遞無形之物

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub
在軟體程式設計中,有一個持久的挑戰:「我們如何傳遞無形之物?」記錄器、HTTP 請求上下文、當前語言環境、I/O 處理器——這些資訊在我們的程式中隨處需要,但若要明確地透過每個函數參數傳遞它們,將會變得難以忍受地冗長。
縱觀歷史,各種方法已經出現來解決這個問題。動態作用域、面向切面程式設計、上下文變數,以及最新的效果系統...有些代表了連續進程中的演進步驟,而其他則是獨立產生的。然而,我們可以透過統一的視角來看待所有這些概念。
動態作用域
動態作用域起源於 1960 年代的 Lisp,提供了最純粹的解決方案。「變數的值不是由它定義的位置決定,而是由它被呼叫的位置決定。」簡單而強大,但在 Common Lisp 和 Perl 之後,它在主流程式語言中失寵了,因為它的不可預測性。不過我們仍然可以在 JavaScript 的 this
綁定中追溯它的血統。
;; Common Lisp 範例 - 動態綁定的記錄器
(defvar *logger* nil)
(defun log-message (message)
(when *logger*
(funcall *logger* message)))
(defun process-user-data (data)
(log-message (format nil "Processing user: ~a" data))
;; 實際處理邏輯...
)
(defun main ()
(let ((*logger* (lambda (msg) (format t "[INFO] ~a~%" msg))))
(process-user-data "john@example.com"))) ; 記錄器被隱式傳遞
面向切面程式設計
AOP(面向切面程式設計)結構化了「模組化橫切關注點」的核心理念。其哲學是:「注入上下文,但有規則。」通過將日誌記錄和交易等橫切關注點分離到切面中,它保持了動態作用域的靈活性,同時追求更可預測的行為。然而,除了 Java 和 .NET 生態系統外,除錯困難和效能開銷限制了它的普及。
// Spring AOP 範例 - 日誌記錄作為橫切關注點分離
@Aspect
public class LoggingAspect {
private Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Around("@annotation(Loggable)")
public Object logMethodCall(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
logger.info("Entering method: " + methodName);
Object result = joinPoint.proceed();
logger.info("Exiting method: " + methodName);
return result;
}
}
@Service
public class UserService {
@Loggable // 記錄器透過切面隱式注入
public User processUser(String userData) {
// 實際處理邏輯...
return new User(userData);
}
}
*AOP: 面向切面程式設計
上下文變數
上下文變數代表為現代需求重新設計的動態作用域——非同步和平行程式設計。Python 的 contextvars
和 Java 的 ThreadLocal
體現了這種方法。然而,它們仍然受到執行時依賴的影響,以及 API 上下文需求只能透過文件發現的事實。
上下文變數的另一種表現形式出現在 React 的 contexts 和其他 UI 框架中的類似概念。雖然它們的用法各不相同,但它們都解決了相同的問題:prop drilling(屬性鑽取)。透過元件樹的隱式傳播反映了透過函數呼叫堆疊的傳播。
# Python contextvars 範例 - 透過上下文傳播自定義記錄器
from contextvars import ContextVar
# 將自定義記錄器函數定義為上下文變數
logger_func = ContextVar('logger_func')
def log_info(message):
log_fn = logger_func.get()
if log_fn:
log_fn(f"[INFO] {message}")
def process_user_data(data):
log_info(f"Processing user: {data}")
validate_user_data(data)
def validate_user_data(data):
log_info(f"Validating user: {data}") # 記錄器被隱式傳播
def main():
# 在上下文中設置特定的記錄器函數
def my_logger(msg):
print(f"CustomLogger: {msg}")
logger_func.set(my_logger)
process_user_data("john@example.com")
單子
單子(Monads)從不同的起點處理這個問題。單子不是隱式上下文傳遞,而是嘗試在型別系統中編碼效果——解決更基本的問題。Reader
單子特別對應於上下文變數。然而,當透過單子轉換器(monad transformers)組合多種效果時,複雜性爆炸性增長。開發人員必須與笨重的型別如 ReaderT Config (StateT AppState (ExceptT Error IO))
搏鬥。層次順序很重要,每一層都需要明確提升,且可用性受到影響。因此,單子的概念主要局限於嚴肅的函數式程式語言,如 Haskell、Scala 和 F#。
-- Haskell Logger 單子範例 - 自定義 Logger 單子定義
newtype Logger a = Logger (IO a)
instance Functor Logger where
fmap f (Logger io) = Logger (fmap f io)
instance Applicative Logger where
pure = Logger . pure
Logger f <*> Logger x = Logger (f <*> x)
instance Monad Logger where
Logger io >>= f = Logger $ do
a <- io
let Logger io' = f a
io'
-- 日誌記錄函數
logInfo :: String -> Logger ()
logInfo msg = Logger $ putStrLn $ "[INFO] " ++ msg
processUserData :: String -> Logger ()
processUserData userData = do
logInfo $ "Processing user: " ++ userData
validateUserData userData
validateUserData :: String -> Logger ()
validateUserData userData = do
logInfo $ "Validating user: " ++ userData -- 記錄器透過單子傳遞
runLogger :: Logger a -> IO a
runLogger (Logger io) = io
main :: IO ()
main = runLogger $ processUserData "john@example.com"
效果系統
效果系統(Effect systems)的出現是為了解決單子的組合複雜性。在 Koka 和 Eff 等語言中實現,它們透過代數效果和處理器運作。多個效果層組合不受順序限制。多個重疊層不需要明確提升。效果處理器不是固定的——它們可以被動態替換,提供顯著的靈活性。
然而,編譯器優化仍不成熟,與現有生態系統的互操作性帶來挑戰,效果推斷的複雜性及其對型別系統的影響仍是持續研究的問題。效果系統代表了這裡討論的最新方法,隨著它們獲得更廣泛的採用,其局限性將被探索。
// Koka 效果系統範例 - 日誌記錄效果靈活傳播
effect logger
fun log-info(message: string): ()
fun log-error(message: string): ()
fun process-user-data(user-data: string): logger ()
log-info("Processing user: " ++ user-data)
validate-user-data(user-data)
fun validate-user-data(user-data: string): logger ()
log-info("Validating user: " ++ user-data) // 日誌記錄效果隱式傳播
if user-data == "" then
log-error("Invalid user data: empty string")
fun main()
// 可以動態選擇不同的日誌記錄實現
with handler
fun log-info(msg) println("[INFO] " ++ msg)
fun log-error(msg) println("[ERROR] " ++ msg)
process-user-data("john@example.com")
傳遞無形之物的藝術——這是所有這裡討論的概念所共享的本質,它將繼續以新的形式演變,成為軟體程式設計中的永恆主題。