如何传递无形之物

洪 民憙 (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")
传递无形之物的艺术——这是所有这里讨论的概念共享的本质,它将继续以新的形式演变,成为软件编程中的永恒主题。