Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

函数式编程上篇 #31

Open
1 of 2 tasks
Ray-56 opened this issue Jul 26, 2020 · 0 comments
Open
1 of 2 tasks

函数式编程上篇 #31

Ray-56 opened this issue Jul 26, 2020 · 0 comments

Comments

@Ray-56
Copy link
Owner

Ray-56 commented Jul 26, 2020

函数式编程上篇

函数式编程是一种编程范式,面向对象编程、命令式编程也都属于不同范式

函数式编程(functional programming)文章中使用FP表示

  • 函数式编程上篇
  • 函数式编程下篇

特点

具有五个鲜明特点

1. 函数是一等公民

一等公民是指要将函数和其它数据类型(数组、对象、字符串等)一样对待,可以存在数组中、当做参数传递、赋值给变量等等

举个栗子,变量hi就是一个函数,当做另一个函数的参数

const hi = name => `Hello ${name}`;
['张三', '李四', '王五'].forEach(hi);

2. 只用表达式,不用语句

  • 表达式(expression)是一个单纯的运算过程,总有返回值
  • 语句(statament)是执行某种操作,没有返回值

函数式编程的开发动机就是为了处理运算(computation),不考虑系统的读写(I/O)。语句属于对系统的读写操作,所以被排斥在外

实际应用中,不做I/O是不可能的。因此编程过程中,将I/O限制到最小,不要有不必要的读写,保持计算过程的单纯性。

3. 无副作用

副作用(side effect)是指函数内部与外部互动,产生运算意外的其它结果

意味着函数要保持独立,所有功能就是返回一个新的值,没有其它行为,尤其不得修改外部变量

// 副作用函数
// 使用了外部变量
let name = 'Ray';
const hi = () => `hi ${name}`;

// 无副作用
const hi = (name) => `hi ${name}`;

4. 不修改状态

FP只是返回新的值,不修改系统变量。所以不修改变量,也是一个重要的特点

在其它类型范式中,变量往往用来保存状态(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。

举个栗子,构建一个斐波那契数列,演示了不同的参数如何决定运算所处的状态

const fb = n => {
    if (n === 0) return 0;
    if (n === 1) return 1;
    return fb(n - 1) + fb(n - 2)
}

由于使用了递归,FP的运行速度比较慢,这也是弊端

5. 引用透明

引用透明(referential transparency)是指函数的运行不依赖外部变量,只依赖输入参数,任何时候只要参数相同,所得到的的返回值总是相同的。

如果返回值于系统状态有关,不同状态下返回值不同。这就是引用不透明,不利于观察和理解程序的行为

const getQQEmail = qqNo => {
    console.log('user info');
    return `${qqNo}@qq.com`;
}
const getUserInfo = user => ({
    no: user.no,
    email: getQQEmail(user.no)
});
getUserInfo({
    no: '123456789',
    address: 'shanghai',
});

上面代码看起来只依赖user,也有唯一的返回值。我们也可以替换getUserInfoemail

const getUserInfo = user => ({
    no: user.no,
    email: `${qqNo}@qq.com`,
});

但是这改变了程序,没有了console.log。这里的console.log是依赖系统状态,说明getQQEmail引用不透明

意义

FP到底有什么好处,为什么会越来越流行?

1. 代码简介,开发快速

因为FP使用函数、减少了代码的重复,因此程序比较短,开发速度较快

2. 接近自然语言,易于理解

函数式编程的自由度很高,可以写出很接近自然语言的代码

将表达式(1 + 2) * 3 - 4写成函数式语言:

subtract(multiply(add(1, 2), 3), 4);

对它进行变形,可以得到另一种写法:

add(1, 2).multiply(3).substract(4);

这基本上就是自然语言的表达了。在看下面的代码,应该一眼就能明白它的意思吧

merge([1, 2], [3, 4]).sort().search(2);

因此,FP的代码更容易理解

3. 更方便的代码管理

FP不依赖、也不改变外界状态,只要给定输入参数,返回的结果必定相同。

因此,每个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。

4. 易于并发编程

函数式编程不需要考虑死锁(deadlock),因为它不修改变量,所以根本不存在线程的问题。

不必要担心一个线程的数据被另一个线程修改,所以可以放心的把工作分摊到多个线程,部署并发编程(concurrency)。

举个栗子

const s1 = Op1();
const s2 = Op2();
const s3 = concat(s1, s2);

由于s1s2互不干扰,不会修改变量,谁先执行无所谓,所以可以放心的增加线程,把它们分配在两个线程上完成。

其它类型就做不到这一点,因为s1可能会修改系统状态,而s2可能会用到这些状态,所以必须保证s2s1之后运行,自然也就不能部署到其他线程上了。

多核CPU是将来的潮流,所以FP的这个特性非常重要

5. 代码的热升级

FP没有副作用,只要保证接口不变,内部实现是与外部无关的。所以可以再运行状态下直接升级代码,不需要重启,也不需要停机。

柯里化

把接受多个参数函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。这个技术由克里斯托弗·斯特雷奇以逻辑学家哈斯凯尔·加里命名的,尽管它是Moses Schönfinkel戈特洛布·弗雷格发明的。

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

const increment = add(1);
const addTen = add(10);

increment(2); // 3
addTen(2); // 12

使用场景

场景一:减少重复传递不变的部分参数

function ajax(type, url, data) {
    const xhr = new XMLHttpRequest();
    xhr.open(type, url, true);
    xhr.send(data);
}
function simpleUrl(protocol, domain, path) {
    return `${protocal}://${domain}/${path}`;
}

// 柯里化之前
ajax('post', 'url1/getdata1', 'foo=bar&lorem=ipsum');
ajax('post', 'url2/getdata2', 'foo=bar&lorem=ipsum');

const url1 = simpleUrl('http', 'site1', 'home.html');
const url2 = simpleUrl('https', 'site2', 'home1.html');

// 使用柯里化
const ajaxCurry = curry(ajax);
const post = ajaxCurry('post'); // 定义POST类型请求数据
const getData1 = post('url1/getdata1'); // 定义以 POST 类型请求来自 url1/getdata1 的数据
getData1('foo=bar&lorem=ipsum');

const simpleUrlCurry = curry(simpleUrl);
const site1 = simpleUrlCurry('http', 'site1');
const url1 = site('home.hteml');

场景二:柯里化 map、filter 等函数的 callback 方法

const personList = [
    { name: '张三', age: 23 },
    { name: '李四', age: 24 },
];

// 柯里化之前
const names = personList.map((item) => item.name);

// 使用柯里化
const getProp = curry((key, obj) => obj[key]);
const names = personList.map(getProp('name'));
const ages = personList.map(getProp('age'));

代码实现

const slice = Array.prototype.slice;
function subCurry(fn) {
    const args = slice.call(arguments, 1);
    return function() {
        return fn.apply(this, args.concat(slice.call(arguments)));
    }
}

function curry(fn, length) {
    const len = length || fn.length;
    return function() {
        if (arguments.length < len) {
            const combined = [fn].concat(slice.call(arguments));
            return curry(subCurry.apply(this, combined), len - arguments.length);
        } else {
            return fn.apply(this, arguments);
        }
    }
}

一句话总结:使用闭包将参数保存起来,当参数的数量与传入函数的参数数量相等就执行函数

see also

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant