Chapter 11. Lazy computations, continuations, and the beauty of monadic composition
覆盖laziness、Try exception boundary与DB middleware continuation,把evaluation timing、failure和resource lifetime纳入可组合contract。
学习目标
- 能区分eager、deferred、memoized与concurrent lazy evaluation,写出evaluation count和exception policy
- 能设计Try adapter,把明确的throwing boundary提升为typed computation,同时保留unexpected fault、stack与cancellation语义
- 能分析continuation组合的connection、transaction、commit/rollback和dispose,证明DB resource lifetime覆盖真正执行期
机制总览
Chapter 11. Lazy computations, continuations, and the beauty of monadic composition:机制路径
- 1
为什么“现在算还是以后算”属于程序语义
函数式组合不只关心输入输出值,还关心计算何时启动、是否重复、失败是否缓存、资源是否仍有效。把昂贵查询包装成function会延迟执行,却不自动memoize;把它包装成 Lazy<T> 通常共享一次结果,却又引入并发publication和异常缓存策略。Continuation则把“算完…
- 2
The virtue of laziness
Eager expression在construction point立刻求值;thunk Func<T> 把computation变成可调用值,每次调用通常重新执行;memoized lazy cell则在第一次demand后保存value。Laziness可避免未使用分支、建立inf…
- 3
Laziness, caching, and invali…
Memoization的key不只是function arguments,还隐含locale、clock、authorization、configuration和data version。对impure computation缓存会冻结旧world state;对failure缓存则可能把短暂故障永…
章级决策实验
Chapter 11. Lazy computations, continuations, and the beauty of monadic composition:机制与证据
切换《Chapter 11. Lazy computations, continuations, and the beauty of monadic composition》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么“现在算还是以后算”属于程序语义
函数式组合不只关心输入输出值,还关心计算何时启动、是否重复、失败是否缓存、资源是否仍有效。把昂贵查询包装成function会延迟执行,却不自动memoize;把它包装成 Lazy<T> 通常共享一次结果,却又引入并发publication和异常缓存策略。Continuation则把“算完…
可核验证据
以确定输入重复运行「为什么“现在算还是以后算”属于程序语义」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
学完《Chapter 11. Lazy computations, continuations, and the beauty of monadic composition》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
Chapter 11. Lazy computations, continuations, and the beauty of monadic composition:失效与核验
为什么“现在算还是以后算”属于程序语义
典型失效
若把「为什么“现在算还是以后算”属于程序语义」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「为什么“现在算还是以后算”属于程序语义」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
The virtue of laziness
典型失效
若把「The virtue of laziness」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「The virtue of laziness」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
Laziness, caching, and invali…
典型失效
若把「Laziness, caching, and invali…」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「Laziness, caching, and invali…」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
为什么“现在算还是以后算”属于程序语义
函数式组合不只关心输入输出值,还关心计算何时启动、是否重复、失败是否缓存、资源是否仍有效。把昂贵查询包装成function会延迟执行,却不自动memoize;把它包装成Lazy<T>通常共享一次结果,却又引入并发publication和异常缓存策略。Continuation则把“算完以后做什么”变成参数,使外层可以围住内层的resource与outcome。
先预测:lambda创建时会执行body吗;同一个deferred function调用两次只查一次库吗;Lazy<T>失败后默认一定重试吗;Try应捕获所有Exception吗;transaction middleware能在返回deferred query前dispose connection吗。答案都是否。
The virtue of laziness
Eager expression在construction point立刻求值;thunk Func<T>把computation变成可调用值,每次调用通常重新执行;memoized lazy cell则在第一次demand后保存value。Laziness可避免未使用分支、建立infinite sequence、推迟昂贵I/O,但它也把failure和cost从定义处移动到消费处,reviewer必须追踪真正的evaluation boundary。
Func<Report> deferred = () => repository.BuildReport(day);
Lazy<Report> shared = new(
() => repository.BuildReport(day),
LazyThreadSafetyMode.ExecutionAndPublication);
var first = shared.Value;
var second = shared.Value; // same published resultIEnumerable<T>也是deferred recipe而非materialized collection。每次enumeration可能重新读文件、重查数据库或重新触发日志;Any()后再ToList()并非免费。API需说明single-use/multi-use、cold/hot、snapshot/live和thread safety。若consumers需要stable snapshot,就在owner boundary materialize并返回immutable view,不让调用者猜测。
切换eager、deferred、memoized与concurrent
Laziness, caching, and invalidation boundaries
Memoization的key不只是function arguments,还隐含locale、clock、authorization、configuration和data version。对impure computation缓存会冻结旧world state;对failure缓存则可能把短暂故障永久化。设计时写出cache identity、success TTL、negative-cache policy、eviction与single-flight。若value持有connection/stream等resource,缓存resource owner而非纯data通常是错误边界。
并发下需要区分at-most-one publication与at-most-one execution:某些publication mode允许多个threads同时运行factory,但只发布一个结果,这对pure CPU computation可接受,对扣款、发送消息不可接受。测试用barrier同时触发demand,并断言factory调用次数、最终identity和fault传播;不要只做顺序unit test。
Exception handling with Try
Try把可能throw的调用表示为deferred Try<T>,运行后得到success或failure。它的价值不是消灭exception,而是让expected composition显式:Map只转换success,Bind只在success继续,Recover按typed policy处理failure。捕获点必须窄,通常放在第三方parse、filesystem或DB driver adapter;pure domain函数仍返回Option/Either等业务outcome。
Try<Customer> load = Try.Of(() => gateway.Load(customerId));
Try<Invoice> invoice =
load.Bind(customer =>
Try.Of(() => renderer.Render(customer)));
var result = invoice.Run();捕获Exception后还需taxonomy:argument/programmer bug通常不应变成可恢复Left;OperationCanceledException要保留cancellation;fatal runtime conditions不能被普通retry吞掉。Failure对象保存原cause或ExceptionDispatchInfo,boundary再映射stable external error。若只保存message,stack、inner cause和machine-readable type都丢失,运维无法定位。
切换success、expected、throw与cancel
Continuations make control flow a value
Continuation-passing style把函数从A -> B改写为接收“拿到B后如何继续”的参数,例如A -> (B -> R) -> R。外层不必把resource泄露给caller,而是把resource交给continuation并等待它完成,再统一commit/dispose。Map、Bind、Try和async都可看成受约束的continuation composition;关键是外层仍拥有lifetime与terminal outcome。
Creating a middleware pipeline for DB access
DB pipeline可分为open connection、begin transaction、run use case、commit or rollback、dispose五层。每层接收next并返回新function,composition order决定lifetime nesting。Business handler只依赖query/execute capability,不负责connection string、retry或transaction。最外层shell运行完整pipeline,result成功才commit,typed rejection与fault按policy rollback。
Func<DbConnection, Either<Error, T>> program = connection =>
useCase(new SqlCustomerStore(connection));
Either<Error, T> RunInTransaction<T>(
Func<DbConnection, Either<Error, T>> next)
{
using var connection = Open();
using var tx = connection.BeginTransaction();
var result = next(connection);
if (result.IsRight) tx.Commit(); else tx.Rollback();
return result;
}真实async版本必须await inner task以后才能dispose;返回IEnumerable<T>或callback也要确保enumeration/callback在scope内完成。Retry应包围完整transaction attempt并重新open/redecide,不能复用failed transaction。Nested middleware要明确ambient transaction、isolation level、timeout、idempotency与commit-uncertain情形,避免“数据库报错就重试”导致重复业务effect。
切换open、begin、commit与rollback
Production gate: prove timing, ownership, and retries
验收lazy/Try/middleware不能只比较最终value。Telemetry需记录attempt id、evaluation count、connection/transaction id、start/end、outcome category和retry reason;敏感SQL参数不可进入普通log。测试以fake clock、counter、barrier和recording adapter证明未使用分支不执行、shared lazy只按policy执行、cancellation不翻译为failure、resource总在inner完成后释放。
部署前再做一条真实integration path:让数据库在command执行后、commit response前断开,观察system如何处理unknown commit outcome。若command具有idempotency key,可查询结果后安全恢复;否则自动retry可能重复写入。Continuation让控制边界清晰,但可靠性仍来自明确protocol和证据。
本章回顾:Composition也必须保留Evaluation语义
- Laziness推迟计算,memoization才共享结果;两者不能混为一谈。
- Evaluation count、thread publication、exception caching与invalidation都是observable contract。
- Try只提升明确throwing boundary,不能吞掉programmer fault和cancellation。
- Continuation让外层拥有resource lifetime与terminal outcome,适合构造middleware。
- DB pipeline按acquire、transaction、use case、outcome、dispose嵌套,并用fault tests证明。
练习
问题 1:怎样证明一个Lazy factory在并发下符合约定?
问题 2:Try adapter应保留哪些failure evidence?
问题 3:Transaction middleware为何不能直接返回Deferred Query?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- deferred evaluation contract
- memoized computation
- Try computation
- continuation boundary
- transaction middleware
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
Exception handling with Try:机制、边界与证据
Chapter 11. Lazy computations, continuations, and the beauty of monadic composition中的Exception handling with Try应写成可组合的输入—输出契约,并把环境读取、状态改变与失败显式放在边界。用确定输入运行正常、空值和失败样本,同时记录返回值与副作用轨迹,证明结论不依赖隐藏状态。
Creating a middleware pipeline for DB access:机制、边界与证据
Chapter 11. Lazy computations, continuations, and the beauty of monadic composition中的Creating a middleware pipeline for DB access应写成可组合的输入—输出契约,并把环境读取、状态改变与失败显式放在边界。用确定输入运行正常、空值和失败样本,同时记录返回值与副作用轨迹,证明结论不依赖隐藏状态。