- 后端
【免费下载链接】language-ext
C# pure functional programming framework - come and get declarative!
本指南聚焦 language-ext(C# 纯函数式编程框架)核心模块 LanguageExt.Core/Lens/README.md 所定义的 Lens 抽象:在不可变数据结构中"向内查看(look inside)并随后更新(update)"某个值,同时通过返回新版本结构来彻底规避突变。读完本文,你将掌握Lens<A, B>的构造与组合、lens函数与|操作符的用法、与 Map/Arr/Lst 等不可变集合itemLens 的协作,以及从 Lens 到 Prism 的转换,能够直接在项目中写出纯函数式的嵌套更新代码。
一、Lens 解决什么问题
在不可变编程模型中,嵌套结构(如Book → Editor → Car → Mileage)的更新令人头痛:没有指针可改,只能层层重建外层对象,代码会迅速退化为大量样板。Lens 正是为此而生。原文档 LanguageExt.Core/Lens/README.md 给出两句话的准确定义:
Lenses allow you to look inside a structure to access its value and then subsequently update that value. But because we don't want to do mutation, the updaters create new versions of the underlying structure.
拆解为三个要点:
- 查看(access):Lens 提供
Get,能从外层结构A中取出内层值B; - 更新(update):Lens 提供
Set,能把新的内层值B写回外层结构A; - 不突变(no mutation):
Set/Update返回的是外层结构的新版本,原结构保持不变——这与 language-ext 中所有不可变集合(Map、Arr、Lst等)的持久化语义完全一致。
需要强调的是,这里的"更新"是函数式的:写回操作实际构造了一条"旧结构 → 新结构"的纯函数管道,原对象不会被修改,因此天然适合多线程共享与函数式组合。
二、核心类型 Lens〈A, B〉:Get 与 SetF 的二元结构
Lens 的底层实现位于 LanguageExt.Core/Lens/LensAB.cs,它是一个readonly struct,只携带两个字段:
public readonly struct Lens<A, B> { public readonly Func<A, B> Get; // 从 A 中取出 B public readonly Func<B, Func<A, A>> SetF; // 给定新值 B,返回 A -> A 的更新函数 public A Set(B value, A cont) => SetF(value)(cont); public static Lens<A, B> New(Func<A, B> Get, Func<B, Func<A, A>> Set) => new(Get, Set); public Func<A, A> Update(Func<B, B> f) => a => Set(f(Get(a)), a); public A Update(Func<B, B> f, A value) => Set(f(Get(value)), value); }这里的类型签名值得细读:
Get : A -> B——读取方向;SetF : B -> A -> A——柯里化的写入方向:先接收新值B,返回一个A → A的转换函数,调用该函数时传入旧结构、产出新结构。这种柯里化设计正是 Lens 能被"顺序组合"(见第四节)的关键;Update(Func<B, B> f)——就地变换(modify):先Get出内层值,用f变换,再Set回去。这是比"取出→计算→写回"三步更原子、更优雅的 API。
从源码结构看,Set与Update都是Get+SetF的组合,因此只要正确构造New(Get, Set),四条操作天然成立。任何满足Set(v, a)后再Get能还原v的 Lens 即为"well-behaved"(行为良好的双向变换),这也是该类型注释 "well-behaved bidirectional transformations" 的由来。
三、构造 Lens 的常用工厂函数
大多数情况下不必手写New,LanguageExt.Core/Lens/Lens.cs 提供了现成的工厂:
| 工厂函数 | 签名 | 语义 |
|---|---|---|
identity<A>() | Lens<A, A> | 恒等透镜,Get原样返回,Set整体替换 |
fst<A, B>() | Lens<(A, B), A> | 二元组/三元组/四元组的第一个元素 |
snd<A, B>() | Lens<(A, B), B> | 第二个元素 |
thrd<A, B, C>() | Lens<(A, B, C), C> | 第三个元素 |
cond<A, B>(pred, Then, Else) | Lens<A, B> | 按谓词动态选择两个 Lens 之一 |
tuple(First, Second) | Lens<(A,B), (C,D)> | 将两个 Lens 合并作用于二元组 |
enumMap(la) | Lens<IEnumerable<A>, IEnumerable<B>> | 把作用于单个元素A的 Lens 提升到整个序列 |
以fst为例,其实现展示了"查看与更新必须对称"的写法:
public static Lens<(A, B), A> fst<A, B>() => Lens<(A, B), A>.New( Get: ab => ab.Item1, Set: a => ab => (a, ab.Item2));Set构造的新二元组保留原组的第二个元素,只替换第一个元素——这正是"创建新版本、不碰旧结构"的具体体现。cond则适合"同一结构中按条件走不同字段路径"的场景:Get/Set前先用pred(a)决定采用Then还是Else透镜。
四、顺序组合:lens 函数与 | 操作符
单个 Lens 只能访问一层,真实业务往往需要穿透多层嵌套。language-ext 提供了两条等价的组合途径。
4.1 Prelude 的 lens 函数
LanguageExt.Core/Lens/Prelude.Lens.cs 中lens提供了从 2 个到 10 个 Lens 的重载,逐一顺序组合。以三参版本为例:
public static Lens<A, D> lens<A, B, C, D>(Lens<A, B> la, Lens<B, C> lb, Lens<C, D> lc) => Lens<A, D>.New( Get: a => lc.Get(lb.Get(la.Get(a))), // 沿路径层层向下取 Set: v => la.Update(lb.Update(lc.SetF(v)))); // 从最内层开始层层向上重建注意Set的组装顺序与Get完全相反:Set先把新值交给最内层lc,产生C→C的更新,再被lb.Update提升为B→B,最后被la.Update提升为A→A。Update在此充当"把内层变换逐级外提"的粘合剂,这正是第二节柯里化SetF设计的意义。
4.2 中缀操作符 |
LanguageExt.Core/Lens/Lens.Operators.cs 为Lens<A, B>定义了|操作符,语义与lens相同,写作更接近管道风格:
public static Lens<A, C> operator |(Lens<A, B> lhs, Lens<B, C> rhs) => Lens<A, C>.New( Get: a => rhs.Get(lhs.Get(a)), Set: v => lhs.Update(rhs.SetF(v)));于是Book.editor | Editor.car | Car.mileage与lens(Book.editor, Editor.car, Car.mileage)完全等价,可按团队风格选用。
五、与不可变集合协作:item 系列 Lens
Lens 的实用价值很大一部分来自集合索引。language-ext 为各不可变集合内置了itemLens,使其可以直接作为组合链的中间环节:
- Map.cs:
Map<K, V>.item(key)与Map<K, V>.itemOrNone(key); - Arr.cs:
Arr<A>.item(index); - Lst.cs:
Lst<A>.item(index); - 同理可查
HashMap、TrackingHashMap等类型中的同名工厂。
Map的实现最典型——它把"按键取值"封装为可组合的透镜:
public static Lens<Map<K, V>, V> item(K key) => Lens<Map<K, V>, V>.New( Get: la => la[key], Set: a => la => la.AddOrUpdate(key, a)); // 返回新 Map,原 Map 不变itemOrNone则返回Lens<Map<K, V>, Option<V>>:Get用Find得到Option,Set时若为Some(x)则AddOrUpdate,若为None则Remove——这把"键可能不存在"的细节也封装进了透镜语义,组合链可放心使用。
六、测试用例验证:从 Record 嵌套结构看完整用法
LanguageExt.Tests/LensTests.cs 提供了 Lens 在Record类型上的完整演示。需要说明的是,该文件当前整体处于注释状态,文件头标注 "TODO: Restore when we have SourceGen",即等待源代码生成器恢复后启用;但它完整展示了[WithLens]特性配合Record派生类生成字段级 Lens 的设想用法,与 LanguageExt.Core/Record 的生成器设计一致,因此其代码仍具权威参考价值。
6.1 多层嵌套的读取与更新
定义Car → Editor → Book三层 Record(均标注[WithLens]以便生成Car.mileage、Editor.car、Book.editor等静态 Lens):
var car = new Car("Maserati", "Ghibli", 20000); var editor = new Editor("Joe Bloggs", 50000, car); var book = new Book("Editors of the World", "Sarah Bloggs", editor); var bookEditorCarMileage = lens(Book.editor, Editor.car, Car.mileage); var mileage = bookEditorCarMileage.Get(book); // 读取:20000 var book2 = bookEditorCarMileage.Set(25000, book);// 更新:返回新 Book Assert.True(book2.Editor.Car.Mileage == 25000); // 原 book 及其 editor、car 均未被修改Get沿Book → Editor → Car → Mileage下行取值;Set则从最内层Mileage开始,逐级生成新的Car、Editor、Book,最终book2与原book共享未被改动的部分,这是持久化结构天然带来的结构共享(structural sharing)收益。
6.2 集合元素级更新
测试还演示了 Lens 与Map的组合——按预约 id 更新某个预约的状态:
var person = new Person("Paul", "Louth", Map( (1, new Appt(1, DateTime.Parse("1/1/2010"), ApptState.NotArrived)), (2, new Appt(2, DateTime.Parse("2/1/2010"), ApptState.NotArrived)), (3, new Appt(3, DateTime.Parse("3/1/2010"), ApptState.NotArrived)))); Lens<Person, ApptState> arrive(int id) => lens(Person.appts, Map<int, Appt>.item(id), Appt.state); var person2 = arrive(2).Set(ApptState.Arrived, person); Assert.True(person2.Appts[2].State == ApptState.Arrived);组合链Person.appts → Map.item(2) → Appt.state将"人 → 预约表 → 指定预约 → 状态"四层访问压缩为一条声明式管道,这正是第五节itemLens 的实战价值。
七、从 Lens 到 Prism:处理可能缺失的值
当目标值可能不存在时,Lens 升级为 Prism。姊妹文档 LanguageExt.Core/Prism/README.md 说明了两者的唯一区别:Prism 的 getter 返回可选值(Func<A, Option<B>>),因此Get失败时更新会安全地落到原值。
- PrismAB.cs 提供两种构造:
Prism<A, B>.New(Lens<A, B>)与Prism<A, B>.New(Lens<A, Option<B>>),并定义了从Lens到Prism的隐式转换; - Prelude_Prism.cs 提供
prism(la)辅助函数,以及与lens结构对称的 2~10 个 Prism 顺序组合重载(Get使用Bind串联 Option,Set使用pa.Update/pb.Update提升); - Lens.cs 还提供扩展方法
ToPrism(),可一键把Lens<A, B>或Lens<A, Option<B>>转成 Prism。
典型用法:Map<int, Appt>.itemOrNone(5)得到Lens<Map<int, Appt>, Option<Appt>>,经ToPrism()或prism(...)转为Prism<Map<int, Appt>, Appt>后,Get返回Option<Appt>,Update在键不存在时直接返回原 Map(参见 PrismAB.cs 中Update的IfNone(a)兜底)。
八、适用范围与使用建议
结合 LensTests.cs、Lens.cs 与集合item实现,可以总结出以下实践准则:
- 读取深度嵌套的不可变数据时,
lens(...)/|组合链比逐层foreach/属性访问更声明式、更易复用; - 更新深层字段时,优先使用
Set或Update(f),让 Lens 替你完成整条路径的重建,避免手写多层with表达式; - 值可能缺失时改用 Prism(
itemOrNone+ToPrism),把 Option 处理并入透镜语义; - Record 类型可借助
[WithLens]特性(见 LanguageExt.Tests/LensTests.cs)生成字段级 Lens,配合Record<T>使用; - 所有操作均返回新结构,原对象保持不可变,可安全用于并发场景与缓存共享。
Lens 是 language-ext "不可变优先"设计哲学在数据访问层的落点:它把"查看 + 更新"封装为一对对称函数,让嵌套结构的读写变得可组合、可复用、无副作用。结合lens组合函数、|操作符、集合item工厂与 Prism 转换,你可以用极少的样板代码完成任意深度的持久化更新,这正是纯函数式编程在 C# 中落地的重要一步。
- 后端
【免费下载链接】language-ext
C# pure functional programming framework - come and get declarative!
相关推荐
Redux中的不可变更新模式详解
Redux中的不可变更新模式详解 前言:为什么不可变性如此重要? 在Redux应用中, 不可变性(Immutability) 是状态管理的核心原则。想象一下这样
前端Python通达信数据读取终极指南:快速获取股票行情与财务数据
Python通达信数据读取终极指南:快速获取股票行情与财务数据 想要用Python轻松读取通达信数据吗?mootdx就是你的完美解决方案!这是一个专门为Pyth
金融科技数据分析language-ext 不可变集合指南:Seq、Lst、HashMap、Map 与 HashSet、Set 的选型与性能解读
language ext 不可变集合指南:Seq、Lst、HashMap、Map 与 HashSet、Set 的选型与性能解读 导读 :本文围绕 languag
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考