如何传递无形之物

洪 民憙 (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