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

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的函数吗

@luckyAisen
Copy link

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

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

No branches or pull requests