函数式编程之-重新认识泛型(1)
目录
Select函数的来历
如果问C#这门语言那些特性是非常好的设计,那么泛型肯定是其中一个。泛型的引入间接带来了LINQ,大家大概都享受过LINQ带来的快感。泛型这个特性来自于函数式语言,F#的设计者Don syme参与了.NET中的泛型设计。C#中的泛型特性使用起来也很简单,以至于没有任何函数式基础就能把LINQ耍起来。本文将从函数式语言的角度来分析泛型,进而描述为什么会有Select、SelectMany这样的函数。
大家一定只用过List<T>这个泛型类型,当然你自己一定也设计过某种泛型类,比如Repository<T>等。在函数式编程之-拒绝空引用异常(Option类型)一文中还提到了避免NullReferenceException的类型Optional<T>。
在函数式编程语言中,泛型的应用更加广泛,比如你在F#中定义一个方法:
1 | let print x = printf "%A" x |
得到的方法签名如下:
1 | val print : x:'a -> unit |
'a表示任意类型,F#中定义的方法是自动泛化的,在C#则需要手动编写泛型方法。
Select函数的来历
对于任意类型a,总有那么一个对应的泛型类型E<a>与之对应,无论是List<a>,还是Optional<a>等。我们把从a到E<a>的过程叫做提升(lifting)
。在我们写代码的过程中,必然存在把a变换成E<a>,也有把E<a>变换成a的过程:
1 2 3 4 5 6 7 8 9 | public Optional< int > Add10(Optional< int > x) { if (x.HasValue) { return Optional.Some(x.Value + 10); } return Optional.None< int >(); } |
上面的代码描述了一个向Optional<int>加10的过程,如果参数x中的Optional没有缺失,就把Optional<int>变为int,同时在int的基础上加10,然后再转化为Optional<int>。
用F#实现相同的逻辑:
1 2 3 4 | let add10 x = match x with | Some s -> Some (s+10) | None -> None |
这看似很正常的代码片段,在函数式语言里是错误的思路。函数式编程语言的类型可以分为两类,类型a和被提升的类型E<a>,无论E<a>是List<a>、Optional<a>还是其他。当代码在a和被提升类型E<a>之间来回切换时,代码就会变得异常复杂:
数学家就想使用一些固定的套路来解决这个问题。如下图所示,你一旦拥有某个提升类型E<a>,就应该尽可能的让他保持在提升状态。
对于上面这个问题,你已经拥有一个被提升的类型Optional<int>,但是你想在Optional<int>上作用一个未被提升的函数:x = x + 10,最终想得到一个Optional<int>的结果。三个已知条件有两个是提升类型,只有函数x = x + 10是普通类型。如果存在一个函数,能够接受一个提升类型E<a>和一个普通函数a->b,并且能够返回E<b>,那么我们的问题就迎刃而解。这个函数就是Select,有的编程语言也叫做map或者lift。
F#在Option类型中已经内置了map函数:
1 2 | let add10 x = x |> Option.map (fun x -> x + 10) |
对于C#中我们自定义的Optional<T>类型,可以添加加一个Select函数:
1 2 3 4 5 6 7 8 9 | public Optional<T2> Select<T2>(Func<T, T2> f) { if (_hasValue) { return Optional.Some(f(_value)); } return Optional.None<T2>(); } |
一旦Optional<T>类型有了Select方法,就可以通过下面的方式实现在Optional<int>类型上加10的需求:
1 2 3 4 | public Optional< int > Add10(Optional< int > x) { return x.Select(v => v + 10); } |
上面例子的函数签名如下:
Optional<T>类型中Select函数的方法签名更加泛化一些:
进一步泛化Select函数:
所以对Select的另类解释为:当你已经拥有一个上升的类型E<a>,如果有一个a->b的函数,在不将E<a>切换回到a的情况下得到E<b>。
上面描述的提升类型E<T>以及定义在E<T>类型下的函数Select共同组成了Functor,那么到底符合什么样的规律就被称作是Functor? 见Functor laws。
- 第一个law是说让一个被提升的类型E<a>调用Select函数,如果传入的是id函数,(所谓id函数是指输入不会被修改的函数,F#和Haskell内置了id函数)那么得到的值E<b>跟E<a>是相等的。
1 2 3 4 5 6 7 8 9 10 11 12 | [Theory] [InlineData( "" )] [InlineData( "foo" )] [InlineData( "bar" )] public void OptionalObeysFirstFunctorLaw( string value) { Func< string , string > id = x => x; var m = Optional.Some(value); Assert.Equal(m, m.Select(id)); } |
- 第二个law说存在两个函数f和g,依次Select这两个函数得到的结果,跟先把这两个函数组合起来,然后Select组合好的函数得到的结果是一致的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | [Theory] [InlineData( "" )] [InlineData( "foo" )] [InlineData( "bar" )] public void OptionalObeysSecondFunctorLaw( string value) { Func< string , int > f = s => s.Length; Func< int , bool > g = i => i > 0; Func< string , bool > composed = s => g(f(s)); var m = Optional.Some(value); Assert.Equal(m.Select(composed), m.Select(f).Select(g)); } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述