如何读懂并写出厉害的函数式代码_如何读懂并写出厉害的函数式代码呢
itomcoil 2025-09-04 07:46 4 浏览
在微博上看到了 有人分享了下面的这段函数式代码,我把代码贴到下面,不过我对原来的代码略有改动,对于函数式的版本,咋一看,的确令人非常费解,仔细看一下,你可能就晕掉了,似乎完全就是天书,看上去非常装逼,哈哈。不过,我感觉解析那段函数式的代码可能会一个比较有趣过程,而且,我以前写过一篇《函数式编程》的入门式的文章,正好可以用这个例子,再升华一下原来的那篇文章,顺便可以向大家更好的介绍很多基础知识,所以写下这篇
文章。
先看代码
这个代码平淡无奇,就是从一个数组中找到一个数,O(n)的算法,找不到就返回 null。
下面是正常的 old-school 的方式。不用多说。
//正常的版本
function find (x, y) {
for ( let i = 0; i < x.length; i++ ) {
if ( x[i] == y ) return i;
}
return null;
}
let arr = [0,1,2,3,4,5]
console.log(find(arr, 2))
console.log(find(arr, 8))
结果到了函数式成了下面这个样子(好像上面的那些代码在下面若影若现,不过又有点不太一样,为了消掉if语言,让其看上去更像一个表达式,动用了 ? 号表达式):
//函数式的版本
const find = ( f => f(f) ) ( f =>
(next => (x, y, i = 0) =>
( i >= x.length) ? null :
( x[i] == y ) ? i :
next(x, y, i+1))((...args) =>
(f(f))(...args)))
let arr = [0,1,2,3,4,5]
console.log(find(arr, 2))
console.log(find(arr, 8))
为了讲清这个代码,需要先补充一些知识。
Javascript的箭头函数
首先先简单说明一下,ECMAScript2015 引入的箭头表达式。箭头函数其实都是匿名函数,其基本语法如下:
(param1, param2, …, paramN) => { statements } (param1, param2, …, paramN) => expression // 等于 : => { return expression; } // 只有一个参数时,括号才可以不加:(singleParam) => { statements }singleParam => { statements } //如果没有参数,就一定要加括号:() => { statements }
下面是一些示例:
var simple = a => a > 15 ? 15 : a;
simple(16); // 15
simple(10); // 10
let max = (a, b) => a > b ? a : b;
// Easy array filtering, mapping, ...
var arr = [5, 6, 13, 0, 1, 18, 23];
var sum = arr.reduce((a, b) => a + b); // 66
var even = arr.filter(v => v % 2 == 0); // [6, 0, 18]
var double = arr.map(v => v * 2); // [10, 12, 26, 0, 2, 36, 46]
看上去不复杂吧。不过,上面前两个 simple 和 max 的例子都把这箭头函数赋值给了一个变量,于是它就有了一个名字。有时候,某些函数在声明的时候就是调用的时候,尤其是函数式编程中,一个函数还对外返回函数的时候。比如下在这个例子:
function MakePowerFn(power) {
return function PowerFn(base) {
return Math.pow(base, power);
}
}
power3 = MakePowerFn(3); //制造一个X的3次方的函数
power2 = MakePowerFn(2); //制造一个X的2次方的函数
console.log(power3(10)); //10的3次方 = 1000
console.log(power2(10)); //10的2次方 = 100
其实,在 MakePowerFn 函数里的那个 PowerFn 根本不需要命名,完全可以写成:
function MakePowerFn(power) {
return function(base) {
return Math.pow(base, power);
}
}
如果用箭头函数,可以写成:
MakePowerFn = power => {
return base => {
return Math.pow(base, power);
}
}
我们还可以写得更简洁(如果用表达式的话,就不需要 { 和 }, 以及 return 语句 ):
MakePowerFn = power => base => Math.pow(base, power)
我还是加上括号,和换行可能会更清楚一些:
MakePowerFn = (power) => (
(base) => (Math.pow(base, power))
)
好了,有了上面的知识,我们就可以进入一个更高级的话题——匿名函数的递归。
匿名函数的递归
函数式编程立志于用函数表达式消除有状态的函数,以及for/while循环,所以,在函数式编程的世界里是不应该用for/while循环的,而要改用递归(递归的性能很差,所以,一般是用尾递归来做优化,也就是把函数的计算的状态当成参数一层一层的往下传递,这样语言的编译器或解释器就不需要用函数栈来帮你保存函数的内部变量的状态了)。
好了,那么,匿名函数的递归该怎么做?
一般来说,递归的代码就是函数自己调用自己,比如我们求阶乘的代码:
function fact(n){
return n==0 ? 1 : n * fact(n-1);
};
result = fact(5);
在匿名函数下,这个递归该怎么写呢?对于匿名函数来说,我们可以把匿名函数当成一个参数传给另外一个函数,因为函数的参数有名字,所以就可以调用自己了。 如下所示:
function combinator(func) {
func(func);
}
这个是不是有点作弊的嫌疑?Anyway,我们再往下,把上面这个函数整成箭头函数式的匿名函数的样子。
(func) => (func(func))
现在你似乎就不像作弊了吧。把上面那个求阶乘的函数套进来是这个样子:
首先,先重构一下fact,把fact中自己调用自己的名字去掉:
function fact(func, n) {
return n==0 ? 1 : n * func(func, n-1);
}
fact(fact, 5); //输出120
然后,我们再把上面这个版本变成箭头函数的匿名函数版:
var fact = (func, n) => ( n==0 ? 1 : n * func(func, n-1) )
fact(fact, 5)
这里,我们依然还要用一个fact来保存这个匿名函数,我们继续,我们要让匿名函数声明的时候,就自己调用自己。
也就是说,我们要把
(func, n) => ( n==0 ? 1 : n * func(func, n-1) )
这个函数当成调用参数,传给下面这个函数:
(func, x) => func(func, x)
最终我们得到下面的代码:
( (func, x) => func(func, x) ) ( //函数体
(func, n) => ( n==0 ? 1 : n * func(func, n-1) ), //第一个调用参数
5 //第二调用参数
);
好像有点绕,anyway, 你看懂了吗?没事,我们继续。
动用高阶函数的递归
但是上面这个递归的匿名函数在自己调用自己,所以,代码中有hard code的实参。我们想实参去掉,如何去掉呢?我们可以参考前面说过的那个 MakePowerFn 的例子,不过这回是递归版的高阶函数了。
HighOrderFact = function(func){
return function(n){
return n==0 ? 1 : n * func(func)(n-1);
};
};
我们可以看,上面的代码简单说来就是,需要一个函数做参数,然后返回这个函数的递归版本。那么,我们怎么调用呢?
fact = HighOrderFact(HighOrderFact);
fact(5);
连起来写就是:
HighOrderFact ( HighOrderFact ) ( 5 )
但是,这样让用户来调用很不爽,所以,以我们一个函数把 HighOrderFact ( HighOrderFact ) 给代理一下:
fact = function ( hifunc ) {
return hifunc ( hifunc );
} (
//调用参数是一个函数
function (func) {
return function(n){
return n==0 ? 1 : n * func(func)(n-1);
};
}
);
fact(5); //于是我们就可以直接使用了
用箭头函数重构一下,是不是简洁了一些?
fact = (highfunc => highfunc ( highfunc ) ) (
func => n => n==0 ? 1 : n * func(func)(n-1)
);
上面就是我们最终版的阶乘的函数式代码。
回顾之前的程序
我们再来看那个查找数组的正常程序:
//正常的版本
function find (x, y) {
for ( let i = 0; i < x.length; i++ ) {
if ( x[i] == y ) return i;
}
return null;
}
先把for干掉,搞成递归版本:
function find (x, y, i=0) {
if ( i >= x.length ) return null;
if ( x[i] == y ) return i;
return find(x, y, i+1);
}
然后,写出带实参的匿名函数的版本(注:其中的if代码被重构成了 ?号表达式):
( (func, x, y, i) => func(func, x, y, i) ) ( //函数体
(func, x, y, i=0) => (
i >= x.length ? null :
x[i] == y ? i : func (func, x, y, i+1)
), //第一个调用参数
arr, //第二调用参数
2 //第三调用参数
)
最后,引入高阶函数,去除实参:
const find = ( highfunc => highfunc( highfunc ) ) (
func => (x, y, i = 0) => (
i >= x.length ? null :
x[i] == y ? i : func (func) (x, y, i+1)
)
);
注:函数式编程装逼时一定要用const字符,这表示我写的函数里的状态是 immutable 的,天生骄傲!
再注:我写的这个比原来版的那个简单了很多,原来版本的那个又在函数中套了一套 next, 而且还动用了不定参数,当然,如果你想装逼装到天上的,理论上来说,你可以套N层,呵呵。
现在,你可以体会到,如此逼装的是怎么来的了吧?。
其它
你还别说这就是装逼,简单来说,我们可以使用数学的方式来完成对复杂问题的描述,那怕是递归。其实,这并不是新鲜的东西,这是Alonzo Church 和 Haskell Curry 上世纪30年代提出来的东西,这个就是 Y Combinator 的玩法。
相关推荐
- Filter函数在WPS里的正确用法,官方教程里都没有说......
-
Filter函数是office365新增的筛选函数,WPS也紧跟添加了它。但在二个软件中的使用方法却完全不同。office365有单元格溢出功能,只需要输入一个Filter公式即可完成数据筛选。但在W...
- 跳过VLOOKUP天坑!FILTER函数10个招式让同事以为你开了外挂?
-
还在为VLOOKUP的"一对多"限制头疼?是否还在为INDEX+MATCH的嵌套抓狂?今天教你用Excel新晋顶流——FILTER函数,10个高能用法让你秒变数据操控大师!用法1:精准...
- Filter函数的三种用法,比用VLOOKUP一对多查询,更加灵活方便
-
文章最后有彩蛋!好礼相送!Excel秘籍大全,正文开始FILTER函数可以基于定义的条件筛选一系列数据。在没有filter函数之前,如果实现一对多查询,常见的是构建辅助列,然后使用VLOOKUP+R...
- Filter函数公式,快速实现订单核对,1分钟学会
-
举个例子,我们有一份公司所有的订单源数据表格,这里我们只用两列信息来模拟,实际可能有很多列数据,几百行数据然后我们有另外一个表,里面有部分已经处理过的订单数据,如下所示,这里举例是4个,实际可能有上百...
- FILTER函数结合及经典用法2:一对多筛选
-
FILTER经典用法2:一对多筛选。FILTER函数的经典用法2:一对多的筛选。比如左边这个表格,需要根据部门筛选出每个部门的人员,应该怎样做?·把鼠标放在单元格内,在编辑栏输入等于FILTER。·第...
- 干掉VLOOKUP,FILTER函数9大用法全解析!
-
1.单条件基础筛选场景:筛选销量>5000的记录公式:=VSTACK(A1:D1,FILTER(A2:D9,D2:D9>5500))解析:A2:D9为需要筛选的数据区域,D2:D9&...
- Excel新函数公式Filter,秒杀VLOOKUP,人人必学
-
以前VLOOKUP公式是必学的公式,自从新版本更新之后,VLOOKUP已经变得可有可无了,但是新出来的Filter函数公式,你必须学会,它非常的强大,工作中用到非常频繁1、Filter公式背景在学会这...
- 第一讲:filter的基本用法及拓展_filter详解
-
全能查找函数filter的基本用法及拓展初学者,务必观看。进阶者,可互相学习,欢迎在回复中补充新用法。首次撰写此函数相关内容,若有不足之处,请予以指教,请勿诋毁,多谢。提示:以下内容以WPS最新版本为...
- 测一测你是什么粒子?_测测你是什么质
-
大亚湾实验。|图片来源:RoyKaltschmidt,LawrenceBerkeleyNationalLaboratory/WikimediaCommons2020年12月12日,大亚湾...
- SpringBoot如何处理配置文件的密文
-
在SpringBoot应用中,直接在配置文件(如application.yml或application.properties)中明文存储数据库密码、API密钥等敏感信息是严重的安全风险,...
- 大语言模型解释Python的 类装饰器
-
一、什么是类装饰器?在Python中,装饰器(Decorator)是一种高阶函数,它接受另一个对象(通常是函数或类),并返回一个经“增强”处理后的新对象。我们常见的是对函数进行装饰:@my_dec...
- Thymeleaf_thymeleaf属于前端吗
-
一、Thymeleaf简介Thymeleaf是用来开发Web和独立环境项目的服务器端的Java模版引擎Spring官方支持的服务的渲染模板中,并不包含jsp。而是Thymeleaf和Freemarke...
- Win9去哪了?Win10避讳Windows95、98
-
10月1日,微软在旧金山发布了新一代操作系统预览版。但不是名为Windows9,而是win10,有业内人士猜测,跳过9而取10为命名是为了预示十全十美。可是小编还觉得9还代表长长久久呢!恐怕这里又说...
- 仓颉编程练习-字符串操作_仓颉编译器
-
main.cj:importstd.convert.Parsablemain():Int64{//字符串比较lets1:String="abc"...
- 一课译词:断断续续_一课译词:断断续续的意思
-
PhotobyMikefromPexels“断断续续”,或“时断时续”,意思是时而中断,时而继续地接连下去(continuefromtimetotime)。与英文惯用语“fitsan...
- 一周热门
- 最近发表
- 标签列表
-
- ps图案在哪里 (33)
- super().__init__ (33)
- python 获取日期 (34)
- 0xa (36)
- super().__init__()详解 (33)
- python安装包在哪里找 (33)
- linux查看python版本信息 (35)
- python怎么改成中文 (35)
- php文件怎么在浏览器运行 (33)
- eval在python中的意思 (33)
- python安装opencv库 (35)
- python div (34)
- sticky css (33)
- python中random.randint()函数 (34)
- python去掉字符串中的指定字符 (33)
- python入门经典100题 (34)
- anaconda安装路径 (34)
- yield和return的区别 (33)
- 1到10的阶乘之和是多少 (35)
- python安装sklearn库 (33)
- dom和bom区别 (33)
- js 替换指定位置的字符 (33)
- python判断元素是否存在 (33)
- sorted key (33)
- shutil.copy() (33)