《JavaScript函数式编程思想》——部分应用和复合

版权声明:原创内容,转载须注明出处。 https://blog.csdn.net/starrow/article/details/86985386

第5章  部分应用和复合

一等值的函数,是函数式编程的基石。部分应用和复合,则是函数式编程的重要特征。采用命令式编程时,每当我们感觉需要抽象出一个新的功能时,就会定义一个函数。在函数式编程中,被同样需要的新函数,往往无需定义,就能像变魔术一样产生,两位魔术师的名字就叫做部分应用和复合。
5.1  部分应用

5.2  柯里化

我们已经体会到部分应用一个函数的好处,那么对部分应用得到的函数,假如有再次部分应用的必要,自然没有理由不能这样做。还是以rangeRoutine2函数为例。对rangeRoutine2函数的step参数进行部分应用,得到一个产生间隔为1的序列的函数range,这个新函数能满足绝大多数情况的需要,使用起来又比原函数方便,就像调用rangeRoutine函数时省略step参数一样。接下来,大部分场景中序列的起点为0,为此可以对range函数的start参数进行部分应用,得到一个调用时只需提供一个参数的rangeFrom0函数,就像调用rangeRoutine函数时省略start参数一样。更为灵活的是,对于需要序列的起点为其他数字的场景,可以对range函数的start参数用该数字进行部分应用,比如rangeFrom1函数返回的就是以1为起点的序列。

const range = f.partial(rangeRoutine2, 1);

const rangeFrom0 = f.partial(range, 0);

const rangeFrom1 = f.partial(range, 1);

f.log(rangeFrom0(10));
//=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

f.log(rangeFrom1(10));
//=> [1, 2, 3, 4, 5, 6, 7, 8, 9]

于是,乘着想象力的翅膀,我们可以设计出一种自动化的过程:将一个多参数函数变成一个单参数函数链,其中每个函数依次接收原函数的一个参数,返回链中的下一个函数,直到接收最后一个参数的函数返回原函数应用于这些参数得到的值。可以用函数类型的记法来直观地表示:

//一个二元函数,参数类型分别为a和b,返回值类型为c。
(a, b) -> c
//柯里化得到的函数链。
a -> ( b -> c)
//考虑到->操作符是右结合的,以上记法可简化为:
a -> b -> c

//一个三元函数。
(a, b, c) -> d
//柯里化得到的函数链。
a -> b -> c -> d

这种对函数做的转换最初是由数学家Gottlob Frege提出的,后来经过同行Moses Schönfinkel和Haskell Brooks Curry的发展,并以后者的名字命名为柯里化(Currying)【注:英文的Curry(咖喱)就是音译词柯里化的来源,所以我认为把柯里化这个拗口的术语译成咖喱能吸引更多吃货来学习函数式编程,和增加本书的销量。】,成为在数学和计算机科学中都很有用的技巧。总的说来,柯里化的意义在于将对多参数函数的处理简化为对单参数函数的处理。在函数式编程中,它可以发挥类似部分应用的作用,但是两者在行为上有差异:部分应用返回的是一个普通的函数,永远需要再调用一次才会返回原函数的结果,即使在部分应用时已传入全部参数,依然会得到一个无参数的函数。柯里化得到的是一个函数链,每调用一次获得链中的下一个函数,当调用链的最后一个函数,也就是传递完所有的参数时,会立即返回原函数的结果。下文陆续介绍的许多运用和好处对于部分应用和柯里化来说是相同的,为了简便,有时就只称柯里化。

有些函数式编程语言中的函数是自动柯里化的,如ML和Haskell(又是以上面那位数学家命名的,此外还有两门编程语言也是如此,即Brooks和Curry)。JavaScript不具备这项功能,只能由我们编写函数来实现。针对特定元数函数的curry函数很容易写,如将二元函数柯里化的curry2。

function curry2(fn) {
    return function (a) {
        return function (b) {
            return fn(a, b);
        }
    }
}

有兴趣的读者可以把curry3当作练习。有难度的是编写针对任意元数函数的curry。因为后续还会遇到curry函数的其他版本,下面这个对应经典柯里化概念的函数被命名为curryClassic。curryClassic是个高阶函数,它不仅要基于函数参数返回一个新函数,还要使新函数返回一个更新的函数,使更新的函数继续如此……棘手的是,这一系列函数既有行为上的共性,又有差异,那就是每个函数都要记住迄今为止调用函数传递的参数。下面代码中的注释解释了实现该函数时遇到的问题和所用的解决方案。

/**
 * @param fn 要柯里化的函数。
 * @param arity 函数的元数。对于定义的参数数目固定的函数,可以
 * 通过length属性获得,无需传递。对于定义的参数数目不固定的(使用
 * 了可选、默认参数或剩余参数)或者通过计算得出的函数,length属性
 * 值不准确,需要传递。
 */
export function curryClassic(fn, arity = fn.length) {
    function _curry(savedArgs) {
        //柯里化一个函数所返回的函数,如果直接使用某个内嵌函数,
        //该函数藉以记忆参数的闭包只有一个,每次调用函数都会修改记忆的参数。
        //要使得每次返回的函数都使用唯一的闭包,就必须返回一个新创建的函数,
        //它记忆的参数通过包容它的函数的参数来传递。
        return function (arg) {
            let curArgs = append(arg, savedArgs);
            if (gte(curArgs.length, arity)) {
                return fn(...curArgs);
            } else {
                return _curry(curArgs);
            }
        }
    }

    return _curry([]);
}

我们来看看柯里化在各种场合带给编程的便捷。

//读对象属性。
export function get(name, object) {
    return object[name];
}


//柯里化get函数以获取返回对象特定属性的函数。
const get = f.curryClassic(f.get);
const name = get('name');
const length = get('length');

f.log(name({name: 'Jack', age: 13}));
//=> Jack

f.log(length([1, 2]));
//=> 2

//咖哩add函数以获取类似于++和--操作符的函数。
const add = f.curryClassic(f.add);
const inc = add(1);
const dec = add(-1);

f.log(inc(0));
//=> 1

f.log(dec(3));
//=> 2

//柯里化nAry函数以获取改变参数数目到特定值的函数。
function nAry(arity, fn) {
    return function (...args) {
        let accepted = take(arity, args);
        return fn(...accepted);
    }
}

const binary = f.curryClassic(nAry)(2);

回想在4.3.2小节中,为了达到类似的效果,nAry函数被写成如下形式。
function nAry(arity) {
    return function (fn) {
        return function (...args) {
            let accepted = f.take(arity, args);
            return fn(...accepted);
        }
    }
}

这相当于在编写具体函数时实现柯里化的效果。

5.2.1  增强的柯里化
5.2.2  从右向左柯里化
5.2.3  进一步增强的柯里化
5.2.4  柯里化的性能成本
5.2.5  应用柯里化的方式
5.2.6  参数的顺序
5.2.7  柯里化与高阶函数
5.3  复合
5.3.1  管道和数据流
5.3.2  函数类型与柯里化
5.4  一切都是函数
5.4.1  操作符的函数化
5.4.2  方法的函数化
5.4.3  控制流语句的函数化
5.5  性能和可读性
5.6  小结

更多内容,请参看拙著:

《JavaScript函数式编程思想》(京东)

《JavaScript函数式编程思想》(当当)

《JavaScript函数式编程思想》(亚马逊)

《JavaScript函数式编程思想》(天猫)

展开阅读全文

没有更多推荐了,返回首页