什么是函数式编程

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

编程范式又是什么?

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

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

函数式编程有什么好处

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

函数式编程怎么学

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

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

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

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
);

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

函数式是更高级的抽象。它只声明解决问题的步骤,把性能优化的事情交给框架或者 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,怎么把他们组合成想要的功能呢?

composepipelinecurry 这三位该出场了。

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

pipelinecompose 的区别就是换个方向:

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

curry

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

解决办法就是用 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. filtermapreduce:三板斧用好,从循环中解放出来。
  2. Pure Function:多写纯函数。
  3. composepipelinecurry:三个工具利用好,把纯函数像搭积木一样搭成想要的功能。

函数式编程 - 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. 有一部分程序员还不知道Java 中的注解到底是如何工作的?

    作者:人晓 自Java5.0版本引入注解之后,它就成为了Java平台中非常重要的一部分.开发过程中,我们也时常在应用代码中会看到诸如@Override,@Deprecated这样的注解. 这篇文章中, ...

  3. super()派生使用中的常见两个错误

    """ super()派生可以继承父类的属性 --super()派生继承父类的语法是:super().__init__() --super().__init__()中的_ ...

  4. 【Java必修课】图说Stream中的skip()和limit()方法及组合使用

    1 简介 本文将讲解Java 8 Stream中的两个方法:skip()和limit().这两个方法是Stream很常用的,不仅各自会被高频使用,还可以组合出现,并能实现一些小功能,如subList和 ...

  5. Nginx 常用配置方式说明

    原文内容来自于LZ(楼主)的印象笔记,如出现排版异常或图片丢失等问题,可查看当前链接:https://app.yinxiang.com/shard/s17/nl/19391737/7619763f-1 ...

  6. python的tqdm模块介绍

    https://www.jianshu.com/p/b27318efdb7b Tqdm 是 Python 进度条库,可以在 Python 长循环中添加一个进度提示信息用法:tqdm(iterator) ...

  7. Maven 项目在 Eclipse 的创建配置

    第1步 New 菜单 Other.. -> Maven -> Maven Project ,然后单击 Next .如下图所示 第2步 在New Maven Project向导中,选择Cre ...

  8. Android Activity启动流程, app启动流程,APK打包流程, APK安装过程

    1.Activity启动流程 (7.0版本之前) 从startActivity()开始,最终都会调用startActivityForResult() 在该方法里面会调用Instrumentation. ...

  9. Android 再次打开APP进入按Home键退出时的界面(thisTaskRoot)

    问题 Android 设置页面的启动模式为 singletask 之后,当按Home 退出时,再重新打开应用,还会进入首启动页.就会造成一些应用需要重新登录,当前页数据丢失等问题 解决 去除启动页的 ...

  10. React组件的属性

    组件的三大属性 state props refs 写组件的要求: 1>组件必须大写 2>组件必须只有一个根元素 state是组件的重要对象 值可以是对象 组件被称之为 状态机 通过跟新组件 ...