JS 函数式编程思维简述(三):柯里化

JavaScript079

JS 函数式编程思维简述(三):柯里化,第1张

       在探讨柯里化之前,我们首先聊一聊很容易跟其混淆的另一个概念—— 偏函数(Partial Application) 。在维基百科中,对 Partial Application 的定义是这样的:

其含义是:在计算机科学中,局部应用(或偏函数应用)是指将 多个参数 固定在一个函数中,从而 产生另一个函数 的过程。

举个例子,假设我们是一个加工厂,用于生产梯形的零件,生产过程中我们要根据 订单来源方 给的一系列参数计算面积:

突然有一天,我们发现了一个问题:我们的大部分订单零件,都是高度为 28 的规格,此时面积函数调用经常是这个样子的:

此时,我们便可以 以第一个函数为模板 ,来创建 存储了固定值的新的计算函数

当然,这个示例中并没有以明显的 偏函数 的方式去呈现,我们可以让返回结果变成一个新的函数,因此我们可以加以改造:

也可以将其简化为:

这里,我们就可以将 trapezoidAreaByHeight15() 、 trapezoidAreaByHeight28() 和 trapezoidAreaByHeight33() 视为 trapezoidArea() 的偏函数。

        偏函数 往往不能改变一个函数的行为,通常是根据一个已有函数而生成一个新的函数,这个新的函数具有已有函数的相同功能,区别在于在新的函数中有一些参数 已被固定 不会变更。偏函数的设计通常:

        柯里化(Currying) 是以美国数理逻辑学家哈斯凯尔·科里(Haskell Curry)的名字命名的函数应用方式。 与偏函数很像的地方是:都可以缓存参数,都会返回一个新的函数,以提高程序中函数的适用性。 而不同点在于, 柯里化(Currying) 通常用于分解原函数式,将参数数量为 n 的一个函数,分解为参数数量为 1 的 n 个函数,并且支持连续调用。例如:

可见, 柯里化(Currying) 用于将多元任务分解成单一任务,每一个独立的任务都 缓存了上一次函数生成时传递的入参 ,并且让新生成的函数更简单、专注。上述演变也可以写作:

        柯里化(Currying) 分解了函数设计过程,将运行的步骤拆分为每一个单一参数的 lambda 演算。这里例举一个在 JavaScript 中用于做强制类型判断的示例:

使用这一的方式构建的函数 checkType() 具备了高通用性,但适用性则略差。我们发现 每次的调用过程,使用者都需要编写参数 typeStr 表示的类型字符串 ,增加了函数的应用复杂度。此时作为设计者,就可以对该函数加以改造,使其生成多个具备高适用性的独立函数:

        柯里化(Currying) 分解了函数设计过程,将运行的步骤拆分为每一个单一参数的 lambda 演算。我们可以通过递归的方式,来构造出一个可进行无限调用,并返回相同的累加函数的 柯里化函数

调用方式如:

以这样的方式,我们构建的参数是一个简单对象 nexter ,该对象至少包含一个 value 属性,用于描述本次累加的值。如果希望获取累加结果,则为 nexter 对象赋予函数属性 success 即可。结果会以实参的形式,传递给 success 函数用于传递通知。

        Promise 对象无论是构造函数还是后续的链式调用中,都能看到 柯里化 设计的影子:接收单一参数,返回一个 Promise :

调用方式为:

node.js最大的卖点在于完全异步的I/O模型。比于阻塞I/O,异步I/O模型极大提高web服务的并发性(可以参见我在另外一个问题的回答htt p:/ /ww w.z hih u.c om/question/20122137,node.js在各个平台上使用epoll,kqueue和IOCP等I/O事件框架)。

对于I/O密集型应用来说,会有明显的性能提升。例如对于大量采用Ajax long-polling的网站(典型的如聊天室,知乎的通知推送)能带来很大的提升;因为每一个的long-polling请求都会占用服务器的连接数,虽然大部分连接处在空闲状态,但并发性不强的web框架最终会被大量的连接耗死。而node.js的异步I/O模型最适合于处理这样的情形。

单线程不仅没有降低吞吐率(因为异步),也远离了死锁等同步不当导致的问题。有人会说node.js只有一个线程,不能很好的利用CPU;但实际上这不是问题:node.js可以通过多进程来提高并发能力;其次单进程多线程的程序往往在内存中共享数据,而内存不能跨机器访问,因此限制了扩展到集群的能力。

同时,在非技术层面上:

node.js基于js语言,相比于Go, Erlang,Scala等,目前大部分js程序员主要做的都是前端工作,对于HTTP协议等都具备相当的了解,熟练的js攻城狮只要稍加学习就能转到node.js平台上。因此很更容易招到程序员。

良好的社区支持,github上星标最多的那些项目,很大部分都是node.js。至少目前,node.js社区基本具备了搭建一个复杂web应用可能用到的库。我甚至认为node.js的社区现状要好于Python的twisted和ternado。

前后端语言统一,相当一部分的node.js库还能同时应用于网页前端(如http://socket.io库),技术成本进一步下降。尤其在小公司,一个程序猿往往兼顾前后端。

当然node.js也不是没有缺点的,

先说非技术的:

node.js诞生于2009年,十分年轻,必然有诸多不成熟的地方,更别说广大社区维护的组件。这一点只能靠时间来检验。

其次是是异步带来的一些问题,:

层层嵌套的回调,异常难以处理。由于程序的模型变成了异步,node.js的代码风格亦偏函数式,没有太多js开发经验的程序员需要一定的时间来适应。

调试工具和手段尚匮乏

提问者提到的层层嵌套等便是第一个问题,就是如何组织异步控制流的问题。目前在node.js社区中,有async、step、和我编写的xchain等库解决了这个问题。都将嵌套的异步回调平铺开来。

版本一不考虑那么多,仅仅使用 apply 来实现一个简单的 bind。调用语法和 bind 相似:

这里核心就是在 myBind 函数中如何获取原函数?

利用 this 关键字即可达到目的,具体请看 梳理 this 关键字的指向 中的 对象方法 。

调用示例:

这里参数有两种情况:

调用示例:

在例子中利用 myBind 实现了偏函数,新的绑定函数 add 在调用时,只传入了一个参数 2, 但是结果是 1 + 2 。

3.1 如果将绑定函数作为构造函数,通过关键字 new 调用,则忽略参数 context。

通过 instanceof 即可判断是否通过 new 关键字调用构造函数。

运行示例:

从以下例子中可以看出,Person 是通过 myBind 生成的新的绑定函数:

3.2 原型链的影响

上述的仍旧不够完美,Person 当作构造函数时,如果在函数 person 中执行语句 this.say() ,就会报错。

因为 myBind 函数返回的绑定函数 fBound 的 prototype 和原函数的 prototype 并不相同。

那能不能直接简单粗暴地执行语句 fBound.prototype = fToBind.prototype ,将原函数的 prototype 赋值给 fBound 呢?

很明显这样的操作把 fBound 和 原函数的 prototype 强关联起来了,如果 fBound 函数的 prototype 将会影响到原函数的 prototype。

所以可以联想到通过 fBound.prototype = Object.create(fToBind.prototype) ,以原函数的 prototype 为模板,生成一个新的实例对象,并赋值给 fBound.prototype。

运行示例:

从以下例子中可以发现,eat 函数之后被添加到 fBound 的原型上,而不会影响到原函数。

3.3 mdn 中并未使用 Object.create()

因为 Object.create() 和 bind 都是 ES5 规范提出的,如果不支持 bind, 那么bind 的 polyfill 里面自然不支持 Object.create()。

Object.create 的实现:

原型式继承:(参见 js 高级程序设计)

应用到 myBind 中:

疑惑 :为什么需要对 fToBind.prototype 进行判断?

因为 Function.prototype 是一个函数,而且它没有 prototype。

即防止有人这么调用 bind 方法: Function.prototype.bind()

3.4 instanceof 不准确

这个使用 new.target 即可。

MDN bind

从一道面试题的进阶,到“我可能看了假源码”

stackoverflow

MDN Object.create

从一道面试题的进阶,到“我可能看了假源码”(2)

面试官问:能否模拟实现JS的bind方法

不用call和apply方法模拟实现ES5的bind方法