如何傳遞無形之物

洪 民憙 (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)的出現是為了解決單子的組合複雜性。在 KokaEff 等語言中實現,它們透過代數效果和處理器運作。多個效果層組合不受順序限制。多個重疊層不需要明確提升。效果處理器不是固定的——它們可以被動態替換,提供顯著的靈活性。

然而,編譯器優化仍不成熟,與現有生態系統的互操作性帶來挑戰,效果推斷的複雜性及其對型別系統的影響仍是持續研究的問題。效果系統代表了這裡討論的最新方法,隨著它們獲得更廣泛的採用,其局限性將被探索。

// 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")

傳遞無形之物的藝術——這是所有這裡討論的概念所共享的本質,它將繼續以新的形式演變,成為軟體程式設計中的永恆主題。

11
1
0

1 comment

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/0197a310-52f3-7c96-8440-da4f6000074e on your instance and reply to it.

1