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

starrow starrow     2022-12-05     471

关键词:

第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函数式编程思想》(天猫)

《javascript函数式编程思想》——从面向对象到函数式编程

...假如本书的写作时间倒退回十年前,书名可能会变成JavaScript面向对象编程思想。自上世纪90年代兴起的面向对象编程思想随Java的繁荣达于顶点,在JavaScript从一门只被用来编写零星的简单的表单验证代码的玩具语言变成日... 查看详情

《javascript函数式编程思想》

自序伴随着Web技术的普及,JavaScript已成为应用最广泛的编程语言之一。由于其在Web前端编程中的统治地位、语言本身的表现力、灵活性、开源的本质和ECMAScript标准近年来的快速发展,JavaScript向各个领域渗透的势头仍然... 查看详情

玩转javascript面试:何为函数式编程?(代码片段)

函数式编程在JavaScript领域着实已经成为一个热门话题。就在几年前,很多JavaScript程序员甚至都不知道啥是函数式编程,但是就在近三年里我看到过的每一个大型应用的代码库中都包含了函数式编程思想的大规模使用。函数式编... 查看详情

javascript系列:函数式编程(开篇)

...数以及函数柯里化等高级函数应用,同时,因为正在学习JavaScript·函数式编程,想整理一下函数式编程中,对于我们日常比较有用的部分。 为什么函数式编程很重要?   学习过C++,java这些面向对象编程语言,我... 查看详情

《javascript函数式编程思想》——列表

第8章 列表函数式编程与列表处理有很深的渊源。列表是最基础,也是使用最普遍的复合数据类型。作为最早出现的函数式编程语言之一,Lisp【注:它的名称就来源于“列表处理器”(LIStProcessor)】用函数参... 查看详情

函数式编程思想:耦合和组合,第2部分

...者可能会对这一方法的缺点及其他的可选做法视而不见,函数式编程使用不同的构建块来实现重用,其基于的是更一般化的概念,比如说列表转换和可移植代码。函数式编程思想的这一部分内容比较了作为重用机制的经由继承的... 查看详情

函数式编程思想:以函数的方式思考,第3部分

...ealFord发布:2011-07-0611:23:24挑错|查看译者版本|收藏本文在函数式编程思想的第一部分和第二部分中,我考察了一些函数式编程的主题,研究了这些主题如何与Java?及其相关语言产生关联。本篇文章继续这一探索过程,给出来自前... 查看详情

函数式编程思想:耦合和组合,第1部分

...重用的面向对象编程思想的一些影响,并把它们与一些更函数化的可选方法,比如说组合,进行比较。面向对象编程通过封装变动部分把代码变成易懂的,函数式编程则是通过最小化变动部分来把代码变成易懂的。——Mi 查看详情

《javascript函数式编程思想》——函数是一等值

第4章 函数是一等值在函数式编程的标准或特点中,“函数是一等值”是最基本和重要的,也是最为人所知的,所有介绍函数式编程的书籍和文章都会优先介绍这一点,以至于“一等值”几乎成为函数的专属头衔&... 查看详情

《javascript函数式编程思想》——名称

第1章 名称一般对函数式编程的介绍都会从一等值和纯函数等概念开始,本书却准备在那之前先花些篇章讨论两个通常未得到足够重视的主题:名称和类型系统。前者包括名称绑定、作用域和闭包等内容,后者包括类... 查看详情

javascriptes6函数式编程:柯里化偏应用组合管道(代码片段)

上一篇介绍了闭包和高阶函数,这是函数式编程的基础核心。这一篇来看看高阶函数的实战场景。首先强调两点:注意闭包的生成位置,清楚作用域链,知道闭包生成后缓存了哪些变量高阶函数思想:以变量作用域作为根基,以... 查看详情

《javascript函数式编程思想》——类型系统

第2章 类型系统为什么在许多编程语言中整数和浮点数是两种类型?结构体、数组、列表、映射……这些类型有什么关系?用户自定义的各种类型与它们又有什么关系?函数也是类型吗?强类型和弱类型意味着什... 查看详情

《javascript函数式编程思想》——递归

第7章 递归王二、张三和赵四一日无聊,决定玩击鼓传花讲冷笑话的游戏。王二和张三围成一圈传花,赵四负责击鼓。张三接连讲了几个诸如小菜、狐狸狡猾的笑话。花停在了王二的手中。王二:这个笑话很短。你要... 查看详情

《javascript函数式编程思想》——副作用和不变性

第6章 副作用和不变性6.1 副作用6.2 纯函数6.2.1 外部变量6.2.2 实现6.2.3 函数内部的副作用6.2.4 闭包6.3 不变性6.3.1 哲学上的不变性与身份6.3.2 简单类型和复合类型6.3.3 值类型和引用类型6.3.4 可变类型和不可变类型6.3.5 可变... 查看详情

基于函数式编程的 JavaScript 应用程序是如何布局的?

】基于函数式编程的JavaScript应用程序是如何布局的?【英文标题】:Howisafunctionalprogramming-basedJavaScriptapplaidout?【发布时间】:2011-02-1006:37:01【问题描述】:我在聊天应用程序上与node.js合作了一段时间(我知道,非常原创,但我... 查看详情

函数式编程

和Lisp、Haskell不同,javascript并非函数式编程语言,但在javascript中可以操控对象一样操控函数,也就是说可以在javascript中应用函数式编程技术。ES5中的数组方法(如map()和reduce())就可以非常适合用于函数式编程风格。本文将详细介... 查看详情

函数响应式编程(frp)思想-callback风格

...让你的代码像数学一样简洁,业务像流水一样清晰流畅。函数响应式编程响应式编程思想为体,函数式编程思想为用。 响应式编程例如,在命令式编程环境中,a:=b+c表示将表达式的结果赋给a,而之后改变b或c的值不会影响a... 查看详情

函数式编程思想

对于函数式编程来说,其只关心,定义输入数据和输出数据相关的关系,数学表达式里面其实是在做一种映射(mapping),输入的数据和输出的数据关系是什么样的,是用函数来定义的。http://www.yxtvg.com/toutiao/5413179/20180212a04ro500.ht... 查看详情