什么是函数式编程

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

编程范式又是什么?

编程范式是一种解决问题的思路。

  • 命令式编程 把程序看作 一系列改变状态的指令;
  • 函数式编程 把程序看作 一系列数学函数映射的组合。
i++; // 命令式 关心指令步骤
[i].map(x => x + 1); // 函数式 关心映射关系

函数式编程有什么好处

  • 易写易读:聚焦重要逻辑,摆脱了循环之类的底层工作。
  • 易复用:面向对象可复用单位是类,函数式可复用单位是函数,更小更灵活。
  • 易测:纯函数【后面有写】不依赖外部环境,测试起来准备工作少。

函数式编程怎么学

方法不难,念个数学博士,搞清楚范畴论、幺半群什么的就可以了。

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

  1. filter,map,reduce:三板斧用好,从循环中解放出来。
  2. Pure Function:多写纯函数。
  3. compose,pipeline,curry:三个工具利用好,把纯函数像搭积木一样搭成想要的功能。

1. 三板斧 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, ...range(start + 1, end)]
: [];
const isPrimeNumber = x =>
x >= 2 &&
range(2, Math.floor(Math.sqrt(x))).every(
cur => x % cur !== 0
);

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

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

  • 框架

    • transducer 可以让集合只遍历一次。【TODO 写篇博客来介绍】
    • memoize 记录算过的,下次再算,直接拿上次的结果。【后面写纯函数的部分会给出实现】
  • runtime

    有的语言 map 是多线程运行的。代码不变,runtime 一优化,性能就大幅的提升了。而前面的命令式,就做不到这一点。


2. 纯函数 - Pure Function

一个函数满足下面 2 点要求就称为纯函数:

  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 memoize = fn => {
let cache = {};
return x => {
if (cache.hasOwnProperty(x)) return cache[x];
else {
const result = fn(x);
cache[x] = result;
return result;
}
};
};

利用 memoize 函数,我们可以缓存纯函数的计算结果。

三板斧的例子 filter 改一下就可以了。

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

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


三个工具 compose,pipeline,curry

写了一堆 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 内的函数是从右往左运行的,也就是先 holify 再 exclaim 再 upperCase。

看不惯从右往左运行?没事,还有一个 pipeline。

pipeline

pipeline 和 compose 的区别就是换个方向:

  • compose 用 reduceRight
  • pipeline 用 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;

这两个函数都需要传 2 个参数,现在我需要一个把数字先加 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);

总结

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

想入门可以从下面 3 点开始:

  1. filter,map,reduce:三板斧用好,从循环中解放出来。
  2. Pure Function:多写纯函数。
  3. compose,pipeline,curry:三个工具利用好,把纯函数像搭积木一样搭成想要的功能。

函数式编程 - Functional Programming的更多相关文章

  1. 关于函数式编程(Functional Programming)

    初学函数式编程,相信很多程序员兄弟们对于这个名字熟悉又陌生.函数,对于程序员来说并不陌生,编程对于程序员来说也并不陌生,但是函数式编程语言(Functional Programming languag ...

  2. Java 中的函数式编程(Functional Programming):Lambda 初识

    Java 8 发布带来的一个主要特性就是对函数式编程的支持. 而 Lambda 表达式就是一个新的并且很重要的一个概念. 它提供了一个简单并且很简洁的编码方式. 首先从几个简单的 Lambda 表达式 ...

  3. 面向函数范式编程(Functional programming)

    函数编程(简称FP)不只代指Haskell Scala等之类的语言,还表示一种编程思维,软件思考方式,也称面向函数编程. 编程的本质是组合,组合的本质是范畴Category,而范畴是函数的组合. 首先 ...

  4. 编程范式:命令式编程(Imperative)、声明式编程(Declarative)和函数式编程(Functional)

    主要的编程范式有三种:命令式编程,声明式编程和函数式编程. 命令式编程: 命令式编程的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么. 比如:如果你想在一个数字集合 collec ...

  5. 函数式编程( Functional)与命令式编程( Imperative)对比

    1.函数式编程带来的好处 函数式编程近些年异军突起,又重新回到了人们的视线,并得到蓬勃发展.总结起来,无外乎如下好处: 1.减少了可变量(Immutable Variable)的声明,程序更为安全.  ...

  6. Scala函数与函数式编程

    函数是scala的重要组成部分, 本文将探讨scala中函数的应用. scala作为支持函数式编程的语言, scala可以将函数作为对象即所谓"函数是一等公民". 函数定义 sca ...

  7. Python进阶【第五篇】函数式编程及某些特殊函数

    一.函数式编程——Functional Programming 函数式=编程语言定义的函数+数学意义的函数 在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语 ...

  8. Python学习札记(二十) 函数式编程1 介绍 高阶函数介绍

    参考: 函数式编程 高阶函数 Note A.函数式编程(Functional Programming)介绍 1.函数是Python内建支持的一种封装,我们通过一层一层的函数调用把复杂任务分解成简单的任 ...

  9. 最通俗易懂的方式让你理解 Swift 的函数式编程

    函数式编程(Functional Programming)是相对于我们常用的面向对象和面向过程编程的另外一种开发思维方式,它更加强调以函数为中心.善用函数式编程思路,可以对我们的开发工作有很大的帮助和 ...

随机推荐

  1. 201871010119-帖佼佼《面向对象程序设计(java)》第六—七周学习总结

    博文正文开头格式:(2分) 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https://www.cnblogs.co ...

  2. GoLand不同目录(包)方法调用

    新手学go,跨目录调用方法是真的难,弄了好几天,几乎要放弃go了,在此演示一下如何跨目录(包)调用~ 需求是main.go调用model包下mysql.go中Query方法,目录结构如下. 要点: 1 ...

  3. ps命令输出进程状态S后面加号的含义

    最近发现一个问题,ps命令输出里面进程状态为S+的含义,网上好多文章都说是表明进程“位于在后台进程组”. 例如下面这个ps命令输出说明: D 不可中断 Uninterruptible sleep (u ...

  4. 关于爬取babycenter.com A-Z为顺序的所有英文名及其详细属性

     这一次爬取的内容已经在标题里提到了,下面是详细要求及其图示: 1.首先以A-Z的顺序获取所有英文名,最后爬取该英文名的详细信息. 2.CSV的header以3中的单词为准,请别拼错.如果没有对应的数 ...

  5. cuckoo沙箱技术分析全景图

    从事信息安全技术行业的小伙伴们都知道沙箱技术(有些也称沙盒),用来判断一个程序或者文件是否是恶意的病毒.木马.漏洞攻击exp或其他恶意软件.其原理简单来说就是提供了一个虚拟的环境,把分析目标放到这个虚 ...

  6. JS---DOM---为同一个元素绑定多个不同事件指向同一个事件处理函数

    为同一个元素绑定多个不同事件指向同一个事件处理函数 1. 用了switch(e.type){} 来修改 2. break <input type="button" value ...

  7. IDEA开发、测试、生产环境pom配置及使用

    pom文件 一般放在最下面,project里 <!--开发环境.测试环境.生产环境--> <!--生产环境--> <profiles> <profile> ...

  8. CentOS 7上的系统管理之:Systemd和systemctl

    参考资料: Chapter 10. Managing Services with systemd Red Hat Enterprise Linux 7 | Red Hat Customer Porta ...

  9. 7款Chrome去广告插件

     互联网时代,大家都习惯在电脑或手机上查阅信息.观看视频.遇到不知道的东西,百度一下:想看的电视剧,爱奇艺腾讯A站B站搜一下.整个操作十分简单,大到八九十岁的老人,小到几岁孩子,都能做到.然而,很多时 ...

  10. OpenLDAP的docker版安装

    分为服务器和client的web版. ldap.sh #!/bin/bash -e docker run --name ldap-service -- docker run --name phplda ...