zc's blog


  • Home

  • Archives

  • Tags

从函数式编程的特点出发,理解函数式编程

Posted on 2017-08-08

从函数式编程的特点出发,理解函数式编程

前言

什么是函数式编程?这个问题可能很难组织语言描述清楚。很多资料对函数式编程的定义让人难以琢磨,使得大家对函数式编程望而却步。本文尝试从函数式编程的特点出发,阐述函数式编程的优点,希望能启发大家对函数式编程的理解。

递归

如果一种编程语言里没有循环,如何处理重复的操作?函数式编程的一个重要特点就是没有循环,在一些纯函数式编程语言(如haskell)里更是没有循环的语法。
在函数式编程里,所有的重复操作通过递归来实现。这听起来可能很不可思议,但是如果你尝试从这一角度编写代码,你会发现一个新的大陆。

我们从几个小例子,体验一些递归编程的思路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 阶乘
function factorial (n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
// 求数组最大值
function maximum (list) {
if (list.length === 0) throw "maximum of empty list";
if (list.length === 1) return list[0];
const [x, ...xs] = list;
const maxTail = maximum(xs);
return x > maxTail ? x : maxTail;
}
// 数组排序
function quicksort(list) {
if (list.length === 0) return [];
const [x, ...xs] = list;
const smaller = quicksort(xs.filter(d => d <= x));
const bigger = quicksort(xs.filter(d => d > x));
return [...smaller, x, ...bigger];
}
// reverse
function reverse(list) {
if (!list.length) return [];
const [x, ...xs] = list;
return [...reverse(xs), x];
}

以上的范例,是不是与大家使用循环的思路相比,显得清新脱俗,更有bigger?这便是函数式编程的魅力。让我们来捋一捋其中的思路:

  1. 判断一些‘边界条件’的结果(如以上1的阶乘,空数组,单元素数组);
  2. 从一堆元素中取一个并做点事情后,假设这个函数已经能完成对应的功能,把余下的操作重新交给这个函数(递归);
  3. 对比、处理递归函数的返回值,返回正确的结果。

对比使用循环解决这些问题,我们可以发现函数式编程有以下特点:

  • 用声明函数是什么的形式来写程序。不是像命令式语言那样命令电脑「要做什么」,而是通过用函数来描述出问题「是什么」,如「阶乘是指从1到某个数的乘积」,「一个串列中数字的和」是指把第一个数字跟剩余数字的和相加。

  • 【没有变量】或者说变量一经定义就不会修改。这使得函数即不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置(若以同样的参数调用同一个函数两次,得到的结果一定是相同),这样写的代码容易进行推理,不容易出错。这使得单元测试和调试都更容易。

高阶函数

高级函数,是指接受函数作为参数或返回函数作为结果的函数。由于javascript有函数是‘一等公民’、闭包等特性,可以方便的实现高阶函数。
下面有一些高阶函数的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// map
function map(fn, list){
if (!list.length) return [];
const [x, ...xs] = list;
return [fn(x), ...map(fn, xs)];
}
// filter
function filter(fn, list){
if (!list.length) return [];
const [x, ...xs] = list;
const filterOther = filter(fn, xs);
return fn(x) ? [x, ...filterOther] : filterOther;
}
// reduce
function reduce(fn, list, acc){
if (!list.length) return acc;
const [x, ...xs] = list;
if (arguments.length <= 2) return reduce(fn, xs, x);
return reduce(fn, xs, fn(acc, x));
}

柯里化(curry) 和 代码组合(compose) 是函数式编程里常用的两个概念。
在javascript中,curry 和 compose 的实现函数,也是高阶函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 柯里化, 把接受多个参数的函数变换成接受一个单一参数,并且返回接受余下的参数而且返回结果的新函数
function curry(fn) {
return x => ((...arg) => fn(x ,...arg));
}
const add = (x, y) => x + y;
const curriedAdd = curry(add); // 柯里化 add 函数
curriedAdd(1)(2); // 3
// 代码组合
function compose(f, g) {
return (x) => f(g(x));
}
const square = x => x * x;
const multipliedByPi = x => x * Math.PI;
const areaOfCircle = compose(multipliedByPi, square); // compose

有了以上两个方法,我们便可以组合出各种不同的函数:

1
2
3
4
5
6
7
8
9
10
11
12
const first = list => list[0];
// 返回数组最后一个元素
const last = compose(first, reverse);
// 返回数组 >5 的元素集合
const gt5 = curry(filter)(x => x > 5);
// 数组求和
const sum = curry(reduce)(add);
// 返回数组的平方和
const squareSum = compose(sum, curry(map)(square));
// 在js中,我们可以使用另一种组合方式
const squareSum = list => list.map(square).reduce(add);

当然,你也可以自己编写高阶函数。许多js工具库(如lodash)也提供了许多有用的高阶函数。运用这些高阶函数,可以将方法组合起来,形成一个‘管道’。这意味着你不用再去根据过程实现新的方法。通过组合已有和必要的新函数,可以更可靠、快速的实现一个新方法,使代码简单而富有可读性。这也是函数式编程所倡导的‘声明函数’,通过定义问题”是什么”来解决问题。

总结

函数式编程强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算。它的优点有:引用透明,没有副作用;更接近人的语言,容易被理解。缺点是牺牲了一定的执行效率。

Reference

  • HASKELL 趣學指南
  • [知乎] 什么是函数式编程思维?
  • [wikipedia] 函数编程语言

ES6类的静态方法/属性的继承

Posted on 2017-01-05

ES6类的静态方法/属性的继承

如果在React组件中使用context,需要定义组件类的contextTypes。如果在contextTypes中没有定义需要使用的context属性类型,那么访问该属性的值会得到undefined;

当用到context的组件被另一个组件继承,父类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default class SaleHouseItem extends React.Component {
static propTypes = {
name: React.PropTypes.string,
};
static contextTypes = {
houseItemConfig:React.PropTypes.object,
actions:React.PropTypes.object,
ht:React.PropTypes.number,
onHouseItemClick:React.PropTypes.func
};
...
}

子类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
export default class RentHouseItem extends SaleHouseItem{
_getHouseName(props,config){
return [
config.showEstateName ? props.estateName : null,
`${props.bedroomSum}室${props.livingRoomSum}厅${props.wcSum}卫`,
props.spaceArea ? `${props.spaceArea}m²` : null,
]
.filter(Boolean)
.join(' · ');
}
...
}

以上代码在IE10及以下会出现不可描述的效果。debug之后发现是SaleHouseItem.contextTypes没有被继承到RentHouseItem里。

ES6类的静态方法的继承

ES6 标准里父类的静态方法/属性能不能被子类继承?查了一些资料后发现是可以继承的(准确的说ES6标准里没有规定类的静态属性,但父类的静态方法是可以被子类继承的);
查看Babel编译之后的代码,继承的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

注意以上代码的最后一行,作用就是继承父类的静态属性。父类的静态属性,实际是继承在子类的__proto__上的。由于IE10及以下不支持__proto__,Babel编译后的代码继承类的静态属性需要插件实现。(其实Babel的文档里有警告,原谅我没有完整的撸完Babel的文档T T)

使用Redux构造RESTful风格的Actions

Posted on 2016-12-01

使用Redux构造RESTful风格的Actions

用redux管理前端数据流总是要根据业务逻辑定义许多actions,通常这些actions的数据操作并不复杂,无非是更新一个字段或者向数组中插入/删除一个元素,构造一套统一标准的actions可以减少工作量和复杂度。RESTful风格的actions简洁明了,并可以实现大部分资源修改需求。

目标功能

传入store数据结构,自动生成相应的actions。此处以一个todolist为例,config如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
key:'formData', //store的名字,也是action的名字
value:{ //value会作为默认值
input:'',
star:false
}
},
{
key: 'todos',
value: []
},
]

在store中创建了formData储存用户输入,进行表单验证;todos储存已添加的代办项。

如果store是Object,例如:

  1. 更改input的值,调用actions.formData('UPDATE',{input:'买电影票'});
  2. 清空input的值,调用actions.formData('UPDATE',{input:undefined})
  3. ……

如果store是Array,例如:

  1. 当向todos添加待办时,调用actions.todos('ADD',{id:1, star:false, text:'买电影票'});
  2. 当向todos修改待办时,调用actions.todos('UPDATE',{id:1},{star:true});
  3. 当向todos删除待办时,调用actions.todos('DELETE',{id:1});
  4. ……

具体实现

首先,在actionTypes.js中定义资源操作动作:

1
2
3
4
5
6
7
8
'use strict';
export const SET = 'SET';
export const UPDATE = 'UPDATE';
export const PATCH = 'PATCH';
export const ADD = 'ADD';
export const DELETE = 'DELETE';

实现目标功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
'use strict';
import {bindActionCreators} from 'redux';
import {SET,UPDATE,PATCH,ADD,DELETE} from './actionTypes.js';
// 获取action type
function getType (key,action){
return `${key}.${action}`;
}
// 从数组中找到条目
function arrayFind(array,find){
let index = _.findIndex(array,find);
return [index,array[index]];
}
// 获取Reducers
function getReducers(states){
let reducers = {};
states.forEach(data => {
let {key,value} = data;
if (_.isArray(value)){
// 这里为数组定义了5总操作方法
let set = getType(key,SET); //设置store的值
let update = getType(key,UPDATE); //修改某一项的值
let patch = getType(key,PATCH); //接收一个修改方法,更新某一项的值
let add = getType(key,ADD); //添加一项
let del = getType(key,DELETE); //删除某项
reducers[key] = function(state = value,action){
let {type,find,data} = action;
let [index,item] = find ? arrayFind(state,action.find) : [-1];
switch (type){
case set:
return data;
case add:
return [...state,data];
case update:
if (index !== -1){
state[index] = {...item,...data};
return [...state];
}
case del:
if (index !== -1){
state.splice(index,1);
return [...state];
}
case patch:
if (index !== -1){
state[index] = data(item);
return [...state];
}
default :
return state;
}
}
}else{
// 为Object定义了3总操作方法
let set = getType(key,SET); //设置Object的值
let update = getType(key,UPDATE); //更新Object的值
let patch = getType(key,PATCH); //接收一个修改方法,更新Object的值
reducers[key] = function(state = value,action){
let {type,data} = action;
switch (type){
case set:
return data;
case update:
return {...state , ...data};
case patch:
return {...data(state)};
default:
return state;
}
}
}
});
return reducers;
}
// 获取Actions
function getActions(states){
let actions = {};
states.forEach(data => {
let {key,value} = data;
if (_.isArray(value)){
actions[key] = function(act,...arg) {
let type = getType(key,act);
let [find,data] = arg.lenght === 1 ? [undefined,arg[0]] : arg;
return {type,find,data};
}
}else{
actions[key] = function(act,data) {
let type = getType(key,act);
return {type,data};
}
}
});
return actions;
}
// 输出方法,接受storesConfig输出actions和reducers;
export default function(storesConfig){
let actions = getActions(states);
let reducers = getReducers(states);
return {actions, reducers};
}

需要改进

以上代码可以实现大部分数据流操作,但一些不满足需求的actions(如批量操作)还是需要自己编写。

zzzzc

zzzzc

3 posts
© 2017 zzzzc
Powered by Hexo
Theme - NexT.Muse