函数式编程之-重新认识泛型(2)
目录
return函数
通过map函数来提升函数
apply函数
中缀表达式
map2和map3函数
什么样的类型支持map/apply/return?
applicative和apply到底有什么用?
回顾上一节,为了丰富建模类型,编程语言引入了泛型,例如Optional<T>,Result<T>等。我们把泛型也叫做类型提升(lifting),这样带来的问题是以往的函数不能再适应提升类型,试想之前已经存在一个a->b的函数,但是此时你拥有一个E<a>变量,你无法直接把E<a>传入到a->b的函数中。上一节还提到,一旦你的类型被提升(lifting),你应该竟可能的让他保持在提升的状态,而不是随意在提升类型(E<a>)和a直接来回切换。
为了达到这个目的,数学家们发现了一些规律,通过一些函数来达到这个目的。例如:当你已经有一个定义好的函数a->b,而这时候又有一个被提升的类型E<a>,此时你可以通过map/select函数直接将a->b应用在E<a>上得到E<b>。
从某种意义上来说,map/select函数有提升函数的作用。之所以a->b可以作用在E<a>上面,是因为map/select函数把函数a->b提升为E<a->b>。正因为如此,某些语言也将map函数叫做lift函数。
return函数
在继续往下介绍之前,我们先了解另一个函数return
,有的语言也称为pure/unit/point
。
return
函数的作用在于将普通类型a提升为E<a>。例如下面的C#代码:
1 2 3 | var x = Optional.Some(10); //将int提升为Optional<int> var y = Optional.None< int >; //将int提升为Optional<int> var z = new List< int >(){1,2,3}; //将int提升为List<int> |
一般来说你并不需要单独定义return
函数,但是当我们提到return
函数的时候你应该要知道他的意图。
通过map函数来提升函数
除了return
函数能够提升类型,map函数也有提升类型的作用。
考虑下面的情况:
1 2 | let add1 x = x + 1 let result = Some 2 |> Option.map add1 |
给定一个函数add1: a->b,然后通过Option.map将add1提升为E<(a->b)>,并传入Some 2,得到结果Some 3。
上面的函数add1只有一个参数,如果对拥有两个参数的函数做map会发生什么?
1 2 | let add x y = x + y let result' = Some 2 |> Option.map add |
因为add接受两个参数x和y,通过map提升并传入第一个参数Some 2,得到的结果result'是一个提升函数Option<(a->b)>。C#并不支持这种方式,C#中的Select方法只接受Func<TSource, TResult>,也就是说C#中的Select方法只接受一个参数的函数。你不能通过Select提升具有多个参数的函数。
同理,通过map提升拥有3个参数的函数:
1 2 | let add x y z = x + y + z let result' = Some 2 |> Option.map add |
得到的result'是Option<(a->b->c)>。
apply函数
对于普通类型的函数a->b->c,你可以通过partial应用依次传入a和b,最终得到c。
1 2 3 4 5 6 | //定义一个函数 add: a -> b -> c let add a b = a + b let add10 = add 10 // add10: b -> c let result = add10 2 // result: 12 |
我们已经知道有两种途径可以提升函数:return和map,那么:
假如你有一个Option<(a->b->c)>的函数,你能否在提升类型的世界里做partial应用呢?
1 2 3 4 | // 通过return 函数创建一个提升函数Option<(a->b->c)> let add' = Some (fun x y -> x + y) // 试图在add'函数上传入Some 2做partial application let add2 ' = add' (Some 2) |
上面的函数会发生编译失败,也就是说,对于一个提升函数,无法做partial application. 如果我们能够定义一个函数,他可以接受一个提升类型的函数和一个提升类型的参数,同时得到另一个提升类型的结果,那么我们的目的就达到了:
下面是Option<T>类型的apply定义:
1 2 3 4 5 | module Option = let apply fOpt xOpt = match fOpt,xOpt with | Some f, Some x -> Some (f x) | _ -> None |
有了apply函数就可以对提升类型的函数做partial应用了:
1 2 3 | let add' = Some (fun x y -> x + y) let add2 ' = Some 2 |> Option.apply add' let add23 ' = Some 3 |> Option.apply add' 2 |
中缀表达式
F#或者C#中的函数都是前缀表达式,例如有一个add函数,接受两个参数:
1 2 | let add x y = x + y let result = add x y // 函数名在前面,两个参数在后面 |
但是数学中的运算符通常都是中缀表达式,例如数学中的加号运算符:
1 | let result = 1 + 2 |
加号写在中间,而两个参数分别写在两边。同样的道理,任意一个拥有两个参数的函数,我们都可以通过定义运算符的方式,让他变为中缀表达式,例如在F#通过下面的方式定义运算符:
1 | let (<*>) = Option.apply |
有了运算符<*>,上面的apply过程就可以写成下面的样子:
1 | (Some add) <*> (Some 2) <*> (Some 3) |
上面的代码通过return函数来提升函数,我们知道map函数也可以提升函数:
1 2 3 4 | let (<!>) = Option.map let (<*>) = Option.apply add <!> (Some 2) <*> (Some 3) |
跟Functor Laws一样,同样有四个所谓的"Applicative Laws",这四个Laws我将不一一描述,从代码的角度来说,本文描述的apply函数就是所谓的Applicative Functor。
map2和map3函数
对于上面的实例,能够将拥有两个参数的函数提升,并且接受两个提升类型的过程,F#定义了一个函数叫做map2:
1 | let result = Option.map2 add (Some 2) (Some 3) |
同样的道理,如果是3个参数的函数,则可以通过map3函数来完成。
什么样的类型支持map/apply/return?
几乎所有你能用到的泛型都可以支持这三个函数,如果是你自己编写的泛型类型,请尝试添加这三个函数。
applicative和apply到底有什么用?
如果你看到这里你已经明白了什么是applicative,但是到底什么样的场景能够使用applicative呢?对于实际的软件工程到底有什么用呢?后来的文章将描述具体的用法,请持续关注。
【推荐】国内首个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的设计模式综述