Chapter 4. Patterns in functional programming
覆盖Map、ForEach、Bind、Where、Option与IEnumerable组合及abstraction levels,以function shape和container effect重建核心FP patterns。
学习目标
- 能比较Map、ForEach、Bind与Where的function shape、structure result和effect semantics,判断错误combinator
- 能推导nested container与Bind flattening,设计Option、IEnumerable及cross-context组合的层级
- 能分析primitive combinator、domain workflow与effect adapter三个abstraction levels,重构混杂pipeline
机制总览
Chapter 4. Patterns in functional programming:机制路径
- 1
为什么先按Function Shape学习Pattern
Map、Bind、Where不是神秘术语,而是反复出现的function/container shapes。看到 F 时先问传入function返回plain value、bool、effect还是另一个 F ,就能推导result structure。背extension names不如掌握sha…
- 2
Applying a function to a stru…
Map接收 T - R ,在不改变structure kind的前提下把 F 转成 F 。IEnumerable Map是Select,对每个element产生R;Option Map只在Some时调用,None保持None。Map不负责flatten,也不应执行与result无关的effect。
- 3
Performing side effects with …
ForEach遍历inner values执行side effect,返回void/Unit或原structure外的completion;它不是pure Map。把logging、DB write塞进Select会产生“result被忽略”的伪Map,execution timing还可能因deferred sequence变化。
章级决策实验
Chapter 4. Patterns in functional programming:机制与证据
切换《Chapter 4. Patterns in functional programming》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么先按Function Shape学习Pattern
Map、Bind、Where不是神秘术语,而是反复出现的function/container shapes。看到 F 时先问传入function返回plain value、bool、effect还是另一个 F ,就能推导result structure。背extension names不如掌握sha…
可核验证据
以确定输入重复运行「为什么先按Function Shape学习Pattern」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
学完《Chapter 4. Patterns in functional programming》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
Chapter 4. Patterns in functional programming:失效与核验
为什么先按Function Shape学习Pattern
典型失效
若把「为什么先按Function Shape学习Pattern」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「为什么先按Function Shape学习Pattern」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
Applying a function to a stru…
典型失效
若把「Applying a function to a stru…」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「Applying a function to a stru…」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
Performing side effects with …
典型失效
若把「Performing side effects with …」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「Performing side effects with …」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
为什么先按Function Shape学习Pattern
Map、Bind、Where不是神秘术语,而是反复出现的function/container shapes。看到F<T>时先问传入function返回plain value、bool、effect还是另一个F<R>,就能推导result structure。背extension names不如掌握shape,因为Option、IEnumerable、Task-like等contexts都复用这些关系。
先预测:ForEach是否等于Map;Map一个返回Option的function是否自动flatten;Where是否改变inner value type;Option和IEnumerable能否不决定outer context直接Bind;使用LINQ query syntax是否自动遵守monad laws。答案都是否。
↡由container shape和传入function signature共同决定Map、Bind、Where或effect traversal的选择规则。Applying a function to a structure’s inner values
Map接收T -> R,在不改变structure kind的前提下把F<T>转成F<R>。IEnumerable Map是Select,对每个element产生R;Option Map只在Some时调用,None保持None。Map不负责flatten,也不应执行与result无关的effect。
Option<Customer> customer = FindCustomer(id);
Option<EmailAddress> email = customer.Map(value => value.Email);
IEnumerable<decimal> totals = orders.Select(CalculateTotal);切换Map、ForEach、Bind与Where
Performing side effects with ForEach
ForEach遍历inner values执行side effect,返回void/Unit或原structure外的completion;它不是pure Map。把logging、DB write塞进Select会产生“result被忽略”的伪Map,execution timing还可能因deferred sequence变化。
Effect应该在pipeline terminal/boundary集中。Core先Map出commands,shell再ForEach解释;这样decision可test,effect次数和failure policy可审计。若需要收集每次effect outcome,返回Result values再sequence/traverse,不要用void丢信息。
↡在container values上执行external effect的terminal traversal,必须明确次数、ordering和failure ownership。Chaining functions with Bind
当function是T -> F<R>时,Map会得到F<F<R>>;Bind先Map再flatten一层,得到F<R>。Option Bind让None short-circuit,IEnumerable Bind/SelectMany把每个element产生的sequences串平。Flatten只适用于同类或有明确定义的composed context。
Option<Customer> customer = FindCustomer(id);
Option<Order> latest = customer.Bind(value => FindLatestOrder(value.Id));
IEnumerable<Line> lines = orders.SelectMany(order => order.Lines);切换nested Map、Bind、sequence与cross-context
Filtering values with Where
Where接收T -> bool,保留满足predicate的values。IEnumerable可能从many变few/empty;Option Where在Some不满足时变None。它不解释为何被过滤,因此validation failure需要Either而不是Where,否则caller只能看到absence。
Predicate应尽量pure。若predicate调用remote service,enumeration次数会放大I/O并造成time-dependent membership。先materialize facts或把effectful decision建模为traversal/result,再纯filter。
Combining Option and IEnumerable with Bind
Cross-context组合必须选择shape。例如Option<IEnumerable<T>>表示整个sequence可能不存在;IEnumerable<Option<T>>表示每个element可能缺失。二者的Map/Bind行为、fallback与cardinality不同。先写domain sentence,再决定outer container。
IEnumerable<Order> orders = customerOption
.Map(customer => repository.FindOrders(customer.Id))
.IfNone(Enumerable.Empty<Order>());把None转empty sequence是policy,会丢“customer missing”和“customer has no orders”的区别;只在consumer确实等价时做。否则保留Option到更外层或转Either error。
↡多个containers/effects嵌套时,哪一种语义拥有outer layer以及何处允许转换/flatten的决策。Coding at different levels of abstraction
Good pipeline的每一步处在相近level:Validate -> Price -> Reserve -> Respond表达domain workflow;内部才用Map/Bind/Where。若一行是business action,下一行是SelectMany plumbing,再下一行直接SQL,reader会不断切换mental model。
Named combinators可隐藏已证明的mechanics,不能隐藏effect。Effect adapters清楚标I/O,domain functions用业务名,generic primitives留在library layer。Composition root负责把三层接起来。
↡一段function/pipeline同时谈domain workflow、generic structure或external effect中的哪一层语义。切换primitive、domain、effect与mixed levels
本章回顾:先看Arrow,再选Combinator
- Map用T-to-R保持structure,ForEach执行effect,Where按predicate过滤。
- Bind处理T-to-F-of-R并flatten同一context的一层。
- Option与IEnumerable组合前先决定outer semantic和absence/cardinality policy。
- Deferred execution会影响function/effect次数,所有operator都要测empty/Some/None/repeat。
- Domain、combinator与effect layers应各自清楚,再由composition连接。
练习
问题 1:为什么在Select里调用SendEmail是危险信号?
问题 2:Option<IEnumerable<Order>>和IEnumerable<Option<Order>>如何选?
问题 3:怎样证明自定义Bind实现不是只会编译?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- combinator shape
- structure-preserving map
- effectful traversal
- context layering
- abstraction level
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
Performing side effects with ForEach:机制、边界与证据
Chapter 4. Patterns in functional programming中的Performing side effects with ForEach应写成可组合的输入—输出契约,并把环境读取、状态改变与失败显式放在边界。用确定输入运行正常、空值和失败样本,同时记录返回值与副作用轨迹,证明结论不依赖隐藏状态。
Chaining functions with Bind:机制、边界与证据
Chapter 4. Patterns in functional programming中的Chaining functions with Bind应写成可组合的输入—输出契约,并把环境读取、状态改变与失败显式放在边界。用确定输入运行正常、空值和失败样本,同时记录返回值与副作用轨迹,证明结论不依赖隐藏状态。
Filtering values with Where:机制、边界与证据
Chapter 4. Patterns in functional programming中的Filtering values with Where应写成可组合的输入—输出契约,并把环境读取、状态改变与失败显式放在边界。用确定输入运行正常、空值和失败样本,同时记录返回值与副作用轨迹,证明结论不依赖隐藏状态。
Combining Option and IEnumerable with Bind:机制、边界与证据
Chapter 4. Patterns in functional programming中的Combining Option and IEnumerable with Bind通过类型表达允许的输入、成功结果和失败分支;类型只编码已声明的不变量,不能替代运行时校验。应准备能编译与必须被拒绝的调用案例,并以成功、空值/缺失和业务失败样本核对每个分支都被显式处理。
Coding at different levels of abstraction:机制、边界与证据
Chapter 4. Patterns in functional programming中的Coding at different levels of abstraction应写成可组合的输入—输出契约,并把环境读取、状态改变与失败显式放在边界。用确定输入运行正常、空值和失败样本,同时记录返回值与副作用轨迹,证明结论不依赖隐藏状态。