函数式编程简介-附入门方法(代码片段)

apolis apolis     2022-12-17     478

关键词:

WHAT? 什么是函数式编程?

函数式编程是一种编程范式。

编程范式又是什么?
编程范式是一种解决问题的思路。
我们熟悉的命令式编程把程序看作一系列改变状态的指令;而函数式编程把程序看作一系列数学函数映射的组合
编程范式和编程语言无关,任何编程语言都可以按照函数式的思维来组织代码。

i++; // 命令式 关心指令步骤
[i].map(x => x + 1); // 函数式 关心映射关系

WHY? 函数式有什么好处?

  • 易写易读 聚焦重要逻辑,摆脱例如循环之类的底层工作
  • 易复用 面向对象可复用的单位是类,函数式可复用的是函数,更小更灵活
  • 易测 纯函数【后面会讲】不依赖外部环境,测试起来准备工作少
  • 看起来很厉害 被人夸奖能增强信心和动力,所以这点也很重要

HOW? 如何做起?

方法不难,回学校念个博士,搞清楚范畴论,幺半群之类的就可以了。

技术分享图片

人生苦短,还是来点实际的吧。

  1. filter map reduce 三板斧用好,从循环中解放出来
  2. small pure function 多写小的纯函数,小指功能聚焦
  3. compose pipeline curry 三个工具利用好,把小函数像搭积木一样拼成大函数

filter map reduce 三板斧

来个例子:找出集合中的素数,算出它们平方的和。

独孤九剑之命令式

const isPrimeNumber = x => 
    if (x <= 1) return false;

    let testRangStart = 2,
        testRangeEnd = Math.floor(Math.sqrt(x));

    let i = testRangStart;
    while (i <= testRangeEnd) 
        if (x % i == 0) return false;
        i++;
    

    return true;
;

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

let sum = 0;

for (let i = 0; i < arr.length; i++) 
    if (isPrimeNumber(arr[i])) 
        sum += arr[i] * arr[i];
    


console.log(sum);

破——剑——嗯....函数式

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const sum = arr.filter(isPrimeNumber)
    .map(x => x * x)
    .reduce((acc, cur) => acc + cur, 0);

console.log(sum);

看吧,for循环没了,代码意图也更明显了。

  1. filter(isPrimeNumber) 找出素数
  2. map(x => x * x) 变成平方
  3. reduce((acc, cur) => acc + cur, 0) 求和

是不是比命令式看着更清晰了?

isPrimeNumber的函数式写法也放出,去掉了循环,看看好懂不。

// 输入范围,获得一个数组,例如 输入 1和5,返回 [1, 2, 3, 4, 5]
const range = (start, end) => start <= end ? [start].concat(range(start + 1, end)) : [];
const isPrimeNumber = x => 
    x >= 2 ? range(2, Math.floor(Math.sqrt(x))).every(cur => x % cur != 0) : false;

有人说函数式的效率不高,因为filter map reduce每次调用,内部都会遍历一遍集合,而命令式只遍历了一次。

函数式是更高级的抽象,主要声明解决问题的步骤,把性能优化交给框架或者runtime来解决。

  • 框架
    transducer 可以让集合只遍历一次【篇幅有限,这里不展开】
    memorize 记录已经算过的,提高效率【后面讲纯函数的时候,会给出实现】
  • runtime
    有的语言map是多线程运行的,函数式代码不变,runtime一优化,性能就大幅的提升了,而前面的命令式,就做不到这一点。

small pure function

纯函数有两点要求:

  1. 相同的传参,返回值一定相同
  2. 函数调用不会对外界造成影响,如不会修改外部对象

看个例子

let name = ‘apolis‘;
const greet = () => console.log(‘Hello ‘ + name);

greet();
name = ‘kzhang‘;
greet();

greet函数依赖外部变量name,相同的传参【都不传参也算相同的传参】屏幕输出的内容却不一样,所以它不纯,鉴定完毕。

const greet = name => console.log(‘Hello ‘ + name);

这样就好多了,不受外部变量的影响了。

不过更严格的认为,调用这个函数造影响了控制台console,所以还不算纯。

const greet = name => ‘Hello ‘ + name;

这样才够纯,同时greet也摆脱了对控制台的依赖,可以适用的范围更广了。

我们要学会把纯的留给自己,把不纯的甩给别人......咳咳,关在函数外面。

由于它的纯,同样的传参,返回值一定相同。
我们可以把算过的结果保存下来,下次调用传的参数发现算过了,直接返回之前计算的结果,提升效率。

const memorize = fn => 
    let cache = ;
    return x => 
        if (cache.hasOwnProperty(x)) return cache[x];
        else 
            const result = fn(x);
            cache[x] = result;
            return result;
        
    
;

利用上面的工具函数,我们可以缓存纯函数的计算结果,三板斧的例子filter改一下就可以了。

const sum = arr.filter(memorize(isPrimeNumber))
    .map(x => x * x)
    .reduce((acc, cur) => acc + cur, 0);

console.log(sum);

如果数组中包含重复元素,这样就能减少计算次数了。
命令式写法要达到这个效果,改动就大的多了。

compose pipeline curry

写了一堆small pure function,怎么把他们组合成更强大的功能呢?

compose pipeline curry这三位该出场了。

compose

举个例子。

const upperCase = str => str.toUpperCase();
const exclaim = str => str + ‘!‘;
const holify = str => ‘Holy ‘ + str;

现在需要一个amaze方法,字符串前面添加Holy,后面添加叹号,全部转为大写。

const amaze = str => upperCase(exclaim(holify(str)));

很不优雅对不对?

看看compose怎么帮我们解决这个问题。

const compose = (...fns) => x => fns.reduceRight((acc, cur) => cur(acc), x);
const amaze = compose(upperCase, exclaim, holify)
console.log(amaze(‘functional programing‘));

这里用到了reduceRight,和reduce的区别就是数组是从后往前遍历的。
compose内的函数是从右往左运行的,也就是先holifyexclaimupperCase

有人可能看不惯从右往左运行,于是又有了一个pipeline

pipeline

compose的区别就是换个方向,compose用的是reduceRightpipeline用的是reduce

const pipeline = (...fns) => x => fns.reduce((acc, cur) => cur(acc), x);
const amaze = pipeline(holify, exclaim, upperCase)
console.log(amaze(‘functional programing‘));

curry

上面compose pipeline里的函数参数都只是一个,如果函数要传多个参数怎么办?

解决办法就是用curry【柯里化】,把函数变成一个参数的。

const add = (x, y) => x + y;
const multiply = (x, y) => x * y;

这两个函数都是需要传两个参数的,现在我需要一个函数,把数字先加5再乘2。

const add5ThenMultiplyBy2 = x => multiply(add(x, 5), 2)

很不好看,我们来curry一下再compose看看。

怎么curry
把括号去掉,逗号变箭头就可以了。
这样传入一个参数x的时候,返回了一个新函数,等待着接收参数y

const add = x => y => x + y;
const multiply = x => y => x * y;

接下来,我们又可以用compose

const add5ThenMultiplyBy2 = x => compose(multiply(2), add(5));

不过curry之后的add方法要这么调用了

add(2)(3)

原先的调用方式add(2, 3)都得改掉了。不喜欢这个副作用?再奉上一个工具函数curry

const curry = fn => 
    const inner = (...args) => 
        if (args.length >= fn.length) return fn(...args);
        else return (...newArgs) => inner(...args, ...newArgs);
    
    return inner;
;

传入fn返回一个新函数,新函数调用时判断传入的参数个数有没有达到fn的要求,达到了,直接返回fn调用的结果;没达到,继续返回一个新新函数,记录着之前已传入的参数。

const add = (x, y) => x + y;
const curriedAdd = curry(add);

这样两种调用方式都支持了。

curriedAdd(2)(3);
curriedAdd(2, 3);

总结

函数式是一种编程思维,声明式、更抽象。

这种思维方式的利弊,大型项目里怎么用,我还没深刻的体会,练习还不足。

建议新手和我一样从下面三点开始多写多思考。

  1. filter map reduce 三板斧用好,从循环中解放出来
  2. small pure function 多写小的纯函数,小指功能聚焦
  3. compose pipeline curry 三个工具利用好,把小函数像搭积木一样拼成大函数

后面我会继续学习functor monad相关的知识,感兴趣可以关注。












前端开发函数式编程入门(代码片段)

前端开发函数式编程入门函数式编程是一门古老的技术,从上个世纪60年代Lisp语言诞生开始,各种方言层出不穷。各种方言带来欣欣向荣的生态的同时,也给兼容性带来很大麻烦。于是更种标准化工作也在不断根据现... 查看详情

编程范式——函数式编程入门(代码片段)

该系列会有3篇文章,分别介绍什么是函数式编程、剖析函数式编程库、以及函数式编程在React中的应用,欢迎关注我的blog命令式编程和声明式编程拿泡茶这个事例进行区分命令式编程和声明式编程命令式编程1.烧开水(为第一人... 查看详情

什么是函数式编程(代码片段)

导读建议先阅读一下这几篇博客:函数式编程初探函数式编程入门教程图解Monad什么是函数式编程函数式编程中的函数指的并不是编程语言中的函数(或方法),它指的是数学意义上的函数,即映射关系(如:y=f(x)),就是y和x... 查看详情

scala成长之路函数入门(代码片段)

众所周知,scala作为一门极客型的函数式编程语言,支持的特性包括:函数拥有“一等公民”身份;支持匿名函数(函数字面量) 支持高阶函数 支持闭包 部分应用函数 柯里化首先需要指出,在scala中有方法和函数对... 查看详情

kotlin函数式编程①(函数式编程简介|高阶函数|函数类别|transform变换函数|过滤函数|合并函数|map变换函数|flatmap变换函数)(代码片段)

文章目录一、函数式编程简介1、编程范式2、高阶函数3、函数式编程4、前端开发技术二、函数类别三、变换函数四、map变换函数1、map函数原型分析2、map函数设计理念3、代码示例五、flatMap变换函数1、flatMap函数原型分析2、代码... 查看详情

python入门--04函数式编程(代码片段)

...,抽象程度高,执行效率低,比如Lisp语言。函数式编程就是一种抽象程度很高的编程范式,纯 查看详情

python入门--04函数式编程(代码片段)

...,抽象程度高,执行效率低,比如Lisp语言。函数式编程就是一种抽象程度很高的编程范式,纯 查看详情

rxjs简介(代码片段)

函数式编程1.声明式(Declarativ)和声明式相对应的编程?式叫做命令式编程(ImperativeProgramming),命令式编程也是最常见的?种编程?式。//命令式编程:functiondouble(arr)constresults=[]for(leti=0;i<arr.length;i++)results.push(arr[i]*2)returnresultsfun... 查看详情

python入门到精通python函数式编程与应用详解(代码片段)

...欢迎小伙伴们点赞👍、收藏⭐、留言💬目录python函数式编程lambda表达式的用法及其使用场景什么是匿名函数?ambda表达式的基本格式lambda表达式的使用场景Python中的高阶函数之map函数中带两个参数的map 查看详情

scalaidea安装配置入门helloworld(代码片段)

...门多范式的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性。Scala是一门以java虚拟机(JVM)为目标运行环境并将面向对象和函数式编程的最佳特性结合在一起的静态类型编程语言。Scala是一门多范式(multi-paradi... 查看详情

php函数式编程(代码片段)

什么是函数式编程与面向对象编程(Object-orientedprogramming)和过程式编程(Proceduralprogramming)并列的编程范式。最主要的特征是,函数是第一等公民。强调将计算过程分解成可复用的函数,典型例子就是map方法和reduce方法组合而... 查看详情

函数式编程(代码片段)

编程的三种方法论:1面向过程2函数式3面向对象---------------------------------------------#把函数当做参数传给另一个函数deffoo(n):print(n)defbar(name):print(‘mynameis%s‘%name)foo(bar(‘alex‘))输出结果:mynameisalexNone------------------------# 查看详情

函数式编程--为什么要学习函数式编程?(代码片段)

函数式编程(FunctionalProgramming,FP)什么是函数式编程?通过纯函数来实现一些细粒度的函数,然后把这些细粒度的函数组合成功能更强大的函数,这一过程就是函数式编程,经典函数式编程库:lodash函数式编程是编程范式之一,... 查看详情

函数式编程(代码片段)

编程的方法论:面向过程面向对象函数式 函数式编程的定义:函数式=编程语言定义的函数+数学意义上的函数(先想到一个数学模型,再用python上的功能实现这个逻辑) y=x+1defcat():returnx+1  特性:1.不用变量保存状... 查看详情

kotlin初学者函数式编程(代码片段)

...),移动端:https://bbs.csdn.net/topics/603956616目录一、函数式编程概念1.1面向函数编程(FOP)1.2高阶函数1.3为什么使用函数式编程二、函数式编程类别2. 查看详情

kotlin初学者函数式编程(代码片段)

...),移动端:https://bbs.csdn.net/topics/603956616目录一、函数式编程概念1.1面向函数编程(FOP)1.2高阶函数1.3为什么使用函数式编程二、函数式编程类别2. 查看详情

kotlin初学者函数式编程(代码片段)

...),移动端:https://bbs.csdn.net/topics/603956616目录一、函数式编程概念1.1面向函数编程(FOP)1.2高阶函数1.3为什么使用函数式编程二、函数式编程类别2. 查看详情

2w字合集|函数式编程—stream流(代码片段)

...Lambda总结二、方法引用|::三、Optional概述使用四、常用的函数式接口SupplierConsumerFunctionPredicateStream流概述Stream流的创建使用Stream流常用转换操作map()filter()排序sorted()去重distinct()截取合并concat()flatMap常用聚合操作reduce()其他聚合方... 查看详情