Skip to content
New issue

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

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

Already on GitHub? # to your account

JavaScript专题之函数组合 #45

Open
mqyqingfeng opened this issue Sep 1, 2017 · 25 comments
Open

JavaScript专题之函数组合 #45

mqyqingfeng opened this issue Sep 1, 2017 · 25 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented Sep 1, 2017

需求

我们需要写一个函数,输入 'kevin',返回 'HELLO, KEVIN'。

尝试

var toUpperCase = function(x) { return x.toUpperCase(); };
var hello = function(x) { return 'HELLO, ' + x; };

var greet = function(x){
    return hello(toUpperCase(x));
};

greet('kevin');

还好我们只有两个步骤,首先小写转大写,然后拼接字符串。如果有更多的操作,greet 函数里就需要更多的嵌套,类似于 fn3(fn2(fn1(fn0(x))))

优化

试想我们写个 compose 函数:

var compose = function(f,g) {
    return function(x) {
        return f(g(x));
    };
};

greet 函数就可以被优化为:

var greet = compose(hello, toUpperCase);
greet('kevin');

利用 compose 将两个函数组合成一个函数,让代码从右向左运行,而不是由内而外运行,可读性大大提升。这便是函数组合。

但是现在的 compose 函数也只是能支持两个参数,如果有更多的步骤呢?我们岂不是要这样做:

compose(d, compose(c, compose(b, a)))

为什么我们不写一个帅气的 compose 函数支持传入多个函数呢?这样就变成了:

compose(d, c, b, a)

compose

我们直接抄袭 underscore 的 compose 函数的实现:

function compose() {
    var args = arguments;
    var start = args.length - 1;
    return function() {
        var i = start;
        var result = args[start].apply(this, arguments);
        while (i--) result = args[i].call(this, result);
        return result;
    };
};

现在的 compose 函数已经可以支持多个函数了,然而有了这个又有什么用呢?

在此之前,我们先了解一个概念叫做 pointfree。

pointfree

pointfree 指的是函数无须提及将要操作的数据是什么样的。依然是以最初的需求为例:

// 需求:输入 'kevin',返回 'HELLO, KEVIN'。

// 非 pointfree,因为提到了数据:name
var greet = function(name) {
    return ('hello ' + name).toUpperCase();
}

// pointfree
// 先定义基本运算,这些可以封装起来复用
var toUpperCase = function(x) { return x.toUpperCase(); };
var hello = function(x) { return 'HELLO, ' + x; };

var greet = compose(hello, toUpperCase);
greet('kevin');

我们再举个稍微复杂一点的例子,为了方便书写,我们需要借助在《JavaScript专题之函数柯里化》中写到的 curry 函数:

// 需求:输入 'kevin daisy kelly',返回 'K.D.K'

// 非 pointfree,因为提到了数据:name
var initials = function (name) {
    return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};

// pointfree
// 先定义基本运算
var split = curry(function(separator, str) { return str.split(separator) })
var head = function(str) { return str.slice(0, 1) }
var toUpperCase = function(str) { return str.toUpperCase() }
var join = curry(function(separator, arr) { return arr.join(separator) })
var map = curry(function(fn, arr) { return arr.map(fn) })

var initials = compose(join('.'), map(compose(toUpperCase, head)), split(' '));

initials("kevin daisy kelly");

从这个例子中我们可以看到,利用柯里化(curry)和函数组合 (compose) 非常有助于实现 pointfree。

也许你会想,这种写法好麻烦呐,我们还需要定义那么多的基础函数……可是如果有工具库已经帮你写好了呢?比如 ramda.js

// 使用 ramda.js
var initials = R.compose(R.join('.'), R.map(R.compose(R.toUpper, R.head)), R.split(' '));

而且你也会发现:

Pointfree 的本质就是使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。即不使用所要处理的值,只合成运算过程。

那么使用 pointfree 模式究竟有什么好处呢?

pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用,更符合语义,更容易复用,测试也变得轻而易举。

实战

这个例子来自于 Favoring Curry

假设我们从服务器获取这样的数据:

var data = {
    result: "SUCCESS",
    tasks: [
        {id: 104, complete: false,            priority: "high",
                  dueDate: "2013-11-29",      username: "Scott",
                  title: "Do something",      created: "9/22/2013"},
        {id: 105, complete: false,            priority: "medium",
                  dueDate: "2013-11-22",      username: "Lena",
                  title: "Do something else", created: "9/22/2013"},
        {id: 107, complete: true,             priority: "high",
                  dueDate: "2013-11-22",      username: "Mike",
                  title: "Fix the foo",       created: "9/22/2013"},
        {id: 108, complete: false,            priority: "low",
                  dueDate: "2013-11-15",      username: "Punam",
                  title: "Adjust the bar",    created: "9/25/2013"},
        {id: 110, complete: false,            priority: "medium",
                  dueDate: "2013-11-15",      username: "Scott",
                  title: "Rename everything", created: "10/2/2013"},
        {id: 112, complete: true,             priority: "high",
                  dueDate: "2013-11-27",      username: "Lena",
                  title: "Alter all quuxes",  created: "10/5/2013"}
    ]
};

我们需要写一个名为 getIncompleteTaskSummaries 的函数,接收一个 username 作为参数,从服务器获取数据,然后筛选出这个用户的未完成的任务的 ids、priorities、titles、和 dueDate 数据,并且按照日期升序排序。

以 Scott 为例,最终筛选出的数据为:

[
    {id: 110, title: "Rename everything", 
        dueDate: "2013-11-15", priority: "medium"},
    {id: 104, title: "Do something", 
        dueDate: "2013-11-29", priority: "high"}
]

普通的方式为:

// 第一版 过程式编程
var fetchData = function() {
    // 模拟
    return Promise.resolve(data)
};

var getIncompleteTaskSummaries = function(membername) {
     return fetchData()
         .then(function(data) {
             return data.tasks;
         })
         .then(function(tasks) {
             return tasks.filter(function(task) {
                 return task.username == membername
             })
         })
         .then(function(tasks) {
             return tasks.filter(function(task) {
                 return !task.complete
             })
         })
         .then(function(tasks) {
             return tasks.map(function(task) {
                 return {
                     id: task.id,
                     dueDate: task.dueDate,
                     title: task.title,
                     priority: task.priority
                 }
             })
         })
         .then(function(tasks) {
             return tasks.sort(function(first, second) {
                 var a = first.dueDate,
                     b = second.dueDate;
                 return a < b ? -1 : a > b ? 1 : 0;
             });
         })
         .then(function(task) {
             console.log(task)
         })
};

getIncompleteTaskSummaries('Scott')

如果使用 pointfree 模式:

// 第二版 pointfree 改写
var fetchData = function() {
    return Promise.resolve(data)
};

// 编写基本函数
var prop = curry(function(name, obj) {
    return obj[name];
});

var propEq = curry(function(name, val, obj) {
    return obj[name] === val;
});

var filter = curry(function(fn, arr) {
    return arr.filter(fn)
});

var map = curry(function(fn, arr) {
    return arr.map(fn)
});

var pick = curry(function(args, obj){
    var result = {};
    for (var i = 0; i < args.length; i++) {
        result[args[i]] = obj[args[i]]
    }
    return result;
});

var sortBy = curry(function(fn, arr) {
    return arr.sort(function(a, b){
        var a = fn(a),
            b = fn(b);
        return a < b ? -1 : a > b ? 1 : 0;
    })
});

var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(prop('tasks'))
        .then(filter(propEq('username', membername)))
        .then(filter(propEq('complete', false)))
        .then(map(pick(['id', 'dueDate', 'title', 'priority'])))
        .then(sortBy(prop('dueDate')))
        .then(console.log)
};

getIncompleteTaskSummaries('Scott')

如果直接使用 ramda.js,你可以省去编写基本函数:

// 第三版 使用 ramda.js
var fetchData = function() {
    return Promise.resolve(data)
};

var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(R.prop('tasks'))
        .then(R.filter(R.propEq('username', membername)))
        .then(R.filter(R.propEq('complete', false)))
        .then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
        .then(R.sortBy(R.prop('dueDate')))
        .then(console.log)
};

getIncompleteTaskSummaries('Scott')

当然了,利用 compose,你也可以这样写:

// 第四版 使用 compose
var fetchData = function() {
    return Promise.resolve(data)
};

var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(R.compose(
            console.log,
            R.sortBy(R.prop('dueDate')),
            R.map(R.pick(['id', 'dueDate', 'title', 'priority'])
            ),
            R.filter(R.propEq('complete', false)),
            R.filter(R.propEq('username', membername)),
            R.prop('tasks'),
        ))
};

getIncompleteTaskSummaries('Scott')

compose 是从右到左依此执行,当然你也可以写一个从左到右的版本,但是从右向左执行更加能够反映数学上的含义。

ramda.js 提供了一个 R.pipe 函数,可以做的从左到右,以上可以改写为:

// 第五版 使用 R.pipe
var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(R.pipe(
            R.prop('tasks'),
            R.filter(R.propEq('username', membername)),
            R.filter(R.propEq('complete', false)),
            R.map(R.pick(['id', 'dueDate', 'title', 'priority'])
            R.sortBy(R.prop('dueDate')),
            console.log,
        ))
};

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

@liuxinqiong
Copy link

不觉明厉,成长的路任重道远

@mqyqingfeng
Copy link
Owner Author

@liuxinqiong 对呀,我也是这种感叹!😂

@yangchongduo
Copy link

高阶函数呢? 你有项目是使用函数式编程写的吗?

@mqyqingfeng
Copy link
Owner Author

@yangchongduo 柯里化就是用高阶函数实现的呀,我没有在项目中用到函数式编程,这主要是因为我对于函数式编程掌握的依然不熟练,对于更深层次的如 Monad 依然没有掌握

@xuchaobei
Copy link

给个redux中的实现,很简洁:

function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

@mqyqingfeng
Copy link
Owner Author

@xuchaobei 感谢补充~

@xue1234jun
Copy link

image

@mqyqingfeng
Copy link
Owner Author

@xue1234jun 感谢指出,已经更改~

@ghost
Copy link

ghost commented Jul 18, 2018

看Learning React时突然想起这个教程,里面有个用ES6语法写的compose函数,想了半天突然发现就是这个教程里的这个概念啊 😎

const compose = (...fns) =>  
  (arg) =>  
    fns.reduce(  
      (composed, f) => f(compose),  
      arg  
    )  

@eltonchan
Copy link

const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));

@aikeProject
Copy link

脑袋疼

@uuidzyl
Copy link

uuidzyl commented Sep 20, 2018

let compose = (...fns) => (arg) => {
    return dispatch(0)
    function dispatch(index) {
        arg = fns[index](arg)
        if (index === fns.length - 1) return arg
        else return dispatch(++index)
    }
}

@18355166248
Copy link

看了半天 这个 函数式编程思想很不错 compose结合柯里化很有意思 学习了

@HuangQiii
Copy link

补充一个非常常见的场景,react中的HOC组合

调用很多HOC时,代码如下:

withRoute(observer(inject('Store')(Index)))

可以预想,当HOC更多时会变得非常难以维护,可以用compose进行一定的阅读性提升

const enhance = compose(withRoute, observer, inject('Store'));

enhance(Index);

因为HOC的本质就是接受一个组件并且返回一个组件的函数!

当然如果用装饰器就更方便了~

@inject('Store')
@withRoute
@observer
export default class Index extends Component {
  ...
  ...
}

@shmmly
Copy link

shmmly commented May 21, 2019

@HuangQiii 如果使用hooks 就没法用装饰器了 还是老老实实的hoc吧 =。= 悲剧

@liuestc
Copy link

liuestc commented Nov 8, 2019

虽然很优雅,但是我觉得要是实际业务中用ramda的话,不熟的人接手会一脸蒙逼啊

@yadongxie150
Copy link

专题很优秀,学到不少知识,感觉楼主的分享,期待其他分类

@xch1029
Copy link

xch1029 commented Mar 9, 2020

这篇知识量有点大,在lodash中有类似compose的函数吗

@aisen60
Copy link

aisen60 commented Jun 1, 2020

const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)

@Vuact
Copy link

Vuact commented Dec 10, 2020

compose这么写更好些,注意用reduceRight

const compose = (...funcs) => {
  return (val) => {
    return funcs.reduceRight((a, b) => b(a), val);
  };
};

@anjina
Copy link

anjina commented Dec 13, 2020

函数式编程 感觉更适合 用在底层都一些框架 ,库之类的编写上, 对于业务来讲,代码不太好看懂

@lonfger
Copy link

lonfger commented Apr 14, 2021

最近这几篇深深的感受到脑子不够用了

@qiugu
Copy link

qiugu commented Feb 15, 2022

pointfree这个概念,大佬从哪里看来的,厉害呀

@justorez
Copy link

感觉从右到左有点反直觉啊。评论里很多 reduce 实现,需要注意处理 this

@justorez
Copy link

pointfree这个概念,大佬从哪里看来的,厉害呀

https://www.ruanyifeng.com/blog/2017/03/pointfree.html

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

No branches or pull requests