[原创译书] JS函数式编程 2.1 函数式编程语言

?? Functional Programming in Javascript 主目录第二章 函数式编程基础

函数式编程语言

函数式编程语言是那些方便于使用函数式编程范式的语言。简单来说,如果具备函数式编程所需的特征,
它就可以被称为函数式语言。在多数情况下,编程的风格实际上决定了一个程序是否是函数式的。

是什么让一个语言具有函数式特征?

函数式编程无法用C语言来实现。函数式编程也无法用Java来实现(不包括那些通过大量变通手段实现的近似函数式编程)。
这些语言不包含支持函数式编程的结构。他们是纯面向对象的、严格非函数式的语言。

同时,纯函数语言也无法使用面向对象编程,比如Scheme、Haskell以及Lisp。

然而有些语言两种模式都支持。Python是个著名的例子,不过还有别的:Ruby,Julia,以及我们最感兴趣的Javascript。
这些语言是如何支持这两种差别如此之大的设计模式呢?它们包含两种编程范式所需要的特征。
然而对于Javascript来说,函数式的特征似乎是被隐藏了。

但实际上,函数式语言所需要的比上述要多一些。到底函数式语言有什么特征呢?

特点 命令式 函数式
编程风格 一步一步地执行,并且要管理状态的变化 描述问题和和所需的数据变化以解决问题
状态变化 很重要 不存在
执行顺序 很重要 不太重要
主要的控制流 循环、条件、函数调用 函数调用和递归
主要的操作单元 结构体和类对象 函数作为一等公民的对象和数据集

函数式语言的语法必须要顾及到特定的设计模式,比如类型推断系统和匿名函数。大体上,这个语言必须实现lambda演算。
并且解释器的求值策略必须是非严格、按需调用的(也叫做延迟执行),它允许不变数据结构和非严格、惰性求值。

译注:这一段用了一些函数式编程的专业词汇。lambda演算是一套函数推演的形式化系统(听起来很晕),
它的先决条件是内部函数和匿名函数。非严格求值和惰性求值差不多一个意思,就是并非严格地按照运算规则把所有元素先计算一遍,
而是根据最终的需求只计算有用的那一部分,比如我们要取有一百个元素的数组的前三项,
那惰性求值实际只会计算出一个具有三个元素是数组,而不会先去计算那个一百个元素的数组。

优点

当你最终掌握了函数式编程它将给你巨大的启迪。这样的经验会让你后面的程序员生涯更上一个台阶,
无论你是否真的会成为一个全职的函数式程序员。

不过我们现在不是在讨论如何去学习冥想;我们正在探讨如何去学习一个非常有用的工具,它将会让你成为一个更好的程序员。

总的来说,什么是使用函数式编程真正实际的优点呢?

更加简洁的代码

函数式编程更简洁、更简单、更小。它简化了调试、测试和维护。

例如,我们需要这样一个函数,它能将二维数组转化为一维数组。如果只用命令式的技术,我们会写成这样:

function merge2dArrayIntoOne(arrays) {
   var count = arrays.length;
   var merged = new Array(count);
   var c = 0;
   for (var i = 0; i < count; ++i) {
     for (var j = 0, jlen = arrays[i].length; j < jlen; ++j) {
       merged[c++] = arrays[i][j];
     }
   }
   return merged
}

现在使用函数式技术,可以写成这样:

merge2dArrayIntoOne2 = (arrays) ->
  arrays.reduce (memo, item) ->
    memo.concat item
  , []
var merge2dArrayIntoOne2 = function(arrays) {
 return arrays.reduce( function(p,n){
   return p.concat(n);
 }, []);
};

译注:原著中代码有误,调用reduce函数时少了第二个参数空数组,这里已经补上。

这两个函数具有同样的输入并返回相同的输出,但是函数式的例子更简洁。

模块化

函数式编程强制把大型问题拆分成解决同样问题的更小的情形,这就意味着代码会更加模块化。
模块化的程序具有更清晰的描述,更易调试,维护起来也更简单。测试也会变得更加容易,
这是由于每一个模块的代码都可以单独检测正确性。

复用性

由于其模块化的特性,函数式编程会有许多通用的辅助函数。你将会发现这里面的许多函数可以在大量不同的应用里重用。

在后面的章节里,许多最通用的函数将会被覆盖到。然而,作为一个函数式程序员,你将会不可避免地编写自己的函数库,
这些函数会被一次又一次地使用。例如一个用于在行间查找配置文件的函数,如果设计好了也可以用于查找Hash表。

减少耦合

耦合是程序里模块间的大量依赖。由于函数式编程遵循编写一等公民的、高阶的纯函数,
这使得它们对全局变量没有副作用而彼此完全独立,耦合极大程度上的减小了。
当然,函数会不可避免地相互依赖,但是改变一个函数不会影响其他的,只要输入和输出的一对一映射保持正确。

数学正确性

最后一点更理论一些。由于根植于lambda演算,函数式编程可以在数学上证明正确性。
这对于一些研究者来说是一个巨大的优点,他们需要用程序来证明增长率、时间复杂度以及数学正确性。

我们来看看斐波那契数列。尽管它很少用于概念性证明以外的问题,但是用它来解释这个概念非常好。
对一个斐波那契数列求值标准的办法是建立一个递归函数,像这样:

fibonnaci(n) = fibonnaci(n-2) + fibonnaci(n–1) 

还需要加上一个一般情形:

return 1 when n < 2

这使得递归可以终止,并且让递归调用栈里的每一步从这里开始累加。

下面列出详细步骤

var fibonacci = function(n) {
  if (n < 2) {
    return 1;
  }else {
    return fibonacci(n - 2) + fibonacci(n - 1);
  }
}
console.log( fibonacci(8) );
// Output: 34

然而,在一个懒执行函数库的辅助下,可以生成一个无穷大的序列,它是通过数学方程来定义整个序列的成员的。
只有那些我们最终需要的成员最后才会被计算出来。

var fibonacci2 = Lazy.generate(function() {
  var x = 1,
  y = 1;
  return function() {
    var prev = x;
    x = y;
    y += prev;
    return prev;
  };
}());
console.log(fibonacci2.length());
// Output: undefined
console.log(fibonacci2.take(12).toArray());
// Output: [1, 1, 2, 3, 5,8, 13, 21, 34, 55, 89, 144]
var fibonacci3 = Lazy.generate(function() {
  var x = 1,
  y = 1;
  return function() {
    var prev = x;
    x = y;
    y += prev;
    return prev;
  };
}());
console.log(fibonacci3.take(9).reverse().first(1).toArray());
//Output: [34]

第二个例子明显更有数学的味道。它依赖Lazy.js函数库。还有一些其它这样的库,比如Sloth.js、wu.js,
这些将在第三章里面讲到。

我插几句:后面这个懒执行的例子放这似乎仅仅是来秀一下函数式编程在数学正确性上的表现。
更让人奇怪的是作者还要把具有相同内部函数的懒加载写两遍,完全没意义啊……
我觉得各位看官知道这是个懒执就行了,不必深究。

非函数式世界中的函数式编程

函数式和非函数式编程能混合在一起吗?尽管这是第七章的主题,但是在我们进一步学习之前,
还是要弄明白一些东西。

这本书并没要想要教你如何严格地用纯函数编程来实现整个应用。这样的应用在学术界之外不太适合。
相反,这本书是要教你如何在必要的命令式代码之上使用纯函数的设计策略。

例如,你需要在一段文本中找出头四个只含有字母的单词,稚嫩一些的写法会是这样:

var words = [], count = 0;
text = myString.split(‘ ‘);
for (i=0; count < 4, i < text.length; i++) {
  if (!text[i].match(/[0-9]/)) {
    words = words.concat(text[i]);
    count++;
  }
}
console.log(words);

函数式编程会写成这样:

var words = [];
var words = myString.split(‘ ‘).filter(function(x){
  return (! x.match(/[1-9]+/));
}).slice(0,4);
console.log(words);

如果有一个函数式编程的工具库,代码可以进一步被简化:

var words = toSequence(myString).match(/[a-zA-Z]+/).first(4);

判断一个函数是否能被写成更加函数式的方式是寻找循环和临时变量,比如前面例子里面的“words”和”count”变量。
我们通常可以用高阶函数来替换循环和临时变量,本章后面的部分将对其继续探索。

Javascript是函数式编程语言吗?

现在还有最后一个问题我们需要问问自己,Javascript是函数式语言还是非函数式语言?

Javascript可以说是世界上最流行却最没有被理解的函数式编程语言。Javascript是一个披着C外衣的函数式编程语言。
它的语法无疑和C比较像,这意味着它使用C语言的块式语法和中缀语序。并且它是现存语言中名字起得最差劲的。
你不用去想象就可以看出来有多少人会因Javascript和Java的关系而迷惑,就好像它的名字暗示了它会是什么样的东西!
但实际上它和Java的共同点非常少。不过还真有一些要把Javascript强制弄成面向对象语言的主意,
比如Dojo、ease.js这些库曾做了大量工作试图抽象Javascript以使其适合面向对象编程。
Javascript来自于90年代那个满世界都嚷嚷着面向对象的时代,我们被告知Javascript是一个面向对象语言是因为我们希望它是这样,
但实际上它不是。

它的真实身份可以追溯到它的原型:Scheme和Lisp,两个经典的函数式编程语言。Javascript一直都是一个函数式编程语言。
它的函数是头等公民,并且可以嵌套,它具有闭包和复合函数,它允许珂理化和monad。所有这些都是函数式编程的关键。
这里另外还有一些Javascript是函数式语言的原因:

  • Javascript的词法包括了传递函数为参数的能力,具有类型推断系统,支持匿名函数、高阶函数、闭包等等。
    这些特点对构成函数式编程的结构和行为至关重要。
  • Javascript不是一个纯面向对象语言,它的多数面向对象设计模式都是通过拷贝Prototype对象来完成的,
    这是一个弱面向对象编程的模型。欧洲电脑制造商协会脚本(ECMAScript)——Javascript的正式形式和标准实现
    ——在4.2.1版本的规范里有如下陈述:

    “Javascript不具有像C++、Smalltalk、Java那样的真正的类,但是支持创建对象的构造器。
    一般来说,在基于类的面向对象语言里,状态由实例承载,方法由类承载,继承只是针对结构和行为。
    在EMACScript里,状态和方法由对象来承载,结构、行为和状态都会被继承。”

  • Javascript是一个解释型语言。Javascript的解释器(有时被称为“引擎”)非常类似于Scheme的解释器。
    它们都是动态的,都有易于组合和传输的灵活的数据类型,都把代码求值为表达式块,处理函数的方式也类似。

也就是说,Javascript的确不是一个纯函数式语言。它缺乏惰性求值和内建的不可变数据。
这是由于大多数解释器是按名调用,而不是按需调用。Javascript由于其尾调用的处理方式也不太善于处理递归。
不过所有的这些问题都可以通过一些小的注意事项来缓和。需要无穷序列和惰性求值的非严格求值可以通过一个叫Lazy.js的库来实现。
不可变量只需要简单的通过编程技巧就可以实现,不过它不是通过依赖语言层面来限制而是需要程序员自律。
尾递归消除可以通过一个叫Trampolining的方法实现。这些问题将在第六章讲解。

关于Javascript是函数式语言还是面向对象语言还是两者皆是还是两者皆非的争论一直都很多,而且这些争论还要继续下去。

最后,函数式编程是通过巧妙的变化、组合、使用函数而实现编写简洁代码的方式。而且Javascript为实现这些提供了很好的途径。
如果你真要挖掘出Javascript全部的潜能,你必须学会如何将它作为一个函数式语言来使用。

下一节 用函数工作

时间: 2024-10-22 07:27:53

[原创译书] JS函数式编程 2.1 函数式编程语言的相关文章

[原创译书] JS函数式编程 2.3 函数式程序员的工具集

?? Functional Programming in Javascript 主目录第二章 函数式编程基础上一节 与函数共舞 函数式程序员的工具集 如果你仔细看了到目前为止出现过的示例代码,你会发现这里面的一些方法不太熟悉. 它们是map().filter()和reduce()函数,它们对任何语言的函数式编程都至关重要. 它们可以让你不必使用循环和语句,写出更简洁的代码. map().filter()和reduce()函数组成了函数式程序员工具集的核心部分,这个工具集包括一系列纯的. 高阶的函

[原创译书] JS函数式编程 前言

前言 函数式编程是一种能够让你编写更聪明的代码的方式,可以减低复杂度,增强模块化. 它是一种通过灵巧地变化.组合.使用函数达到编写简洁代码的方式. Javascript提供了一个实现这些的超赞的途径.Javascript,这个Internet的脚本语言, 它的核心实际上是一个函数式语言.通过学习如何显露出它作为一个函数式语言的真实身份, 我们可以实现强大的.更易维护的以及更可靠的web应用. 通过这些,Javascript的那些怪癖和缺陷将会立刻变得清晰,并且语言本身也将会无限精彩. 学习如何使

[原创译书] JS函数式编程 3.建立函数式编程环境

?? Functional Programming in Javascript 主目录上一章 函数式编程基础 第三章 建立函数式编程环境 介绍 如果只是为了用函数式编程写应用,我们是否需要了解高级数学知识--类型理论.lambda演算和多态? 我们需要重新发明轮子吗?简单来说,这两个问题的答案都是:不需要. 在这章,我们将竭尽所能去调研所有会影响用Javascript编写函数式程序的方式,包括: 库 工具集 开发环境 编译成Javascript的函数式语言 更多 你要明白现在Javascript

[原创译书] JS函数式编程 2.2 与函数共舞

?? Functional Programming in Javascript 主目录第二章 函数式编程基础上一节 函数式编程语言 与函数共舞 有时,优雅的实现是一个函数.不是方法.不是类.不是框架.只是函数. - John Carmack,游戏<毁灭战士>首席程序员 函数式编程全都是关于如何把一个问题分解为一系列函数的.通常,函数会链在一起,互相嵌套, 来回传递,被视作头等公民.如果你使用过诸如jQuery或Node.js这样的框架,你应该用过一些这样的技术, 只不过你没有意识到. 我们从J

[原创译书] JS函数式编程 第二章总结

?? Functional Programming in Javascript 主目录第二章 函数式编程基础上一节 函数式程序员的工具集 第二章总结 为了理解函数式编程,这章覆盖了很大范围的主题.首先我们分析了一个编程语言的函数式是什么意思, 并且评估了Javascript函数式编程能力.接下来,我们用Javascript实现了一些函数式编程的核心概念, 并展示了一些Javascript内建的函数式编程函数. 尽管Javascript有一些函数式编程的工具,它函数式编程核心的大部分仍被隐藏着,并

[原创译书] JS函数式编程 2.函数式编程基础

2 函数式编程基础 ?? Functional Programming in Javascript 主目录上一章 Javascript函数式编程的力量——举个例子 现在,你已经稍稍领略了一点函数式编程能做的事情.但是到底什么是函数式编程呢? 如何来区分一个语言是否是函数式的?又如何来区分一段程序是否是函数式的呢? 在这章,我们先来看看下面的问题,这些问题覆盖了函数式编程的核心概念: 使用函数和数组实现控制流 编写纯函数.匿名函数.递归函数等等 像对象那样传递函数 利用map().filter()

[原创译书] JS函数编程 3.2 开发和生产环境

?? Functional Programming in Javascript 主目录第三章 建立函数式编程环境 开发和生产环境 环境 编程风格与应用所部署或者将要部署的环境没啥关系.但是库就有关系了. 浏览器 主要的Javascript应用还是跑在客户端的,也就是浏览器.基于浏览器的环境对于开发来说非常好, 因为浏览器无处不在,你可以在本地机器上写代码,解释器是浏览器的Javascript引擎, 所有的浏览器都有开发者终端.火狐的FireBug提供了非常有用的错误信息,并支持断点等等, 不过同

如何编写高质量的 JS 函数(3) --函数式编程[理论篇]

本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/EWSqZuujHIRyx8Eb2SSidQ作者:杨昆 [编写高质量函数系列]中, <如何编写高质量的 JS 函数(1) -- 敲山震虎篇>介绍了函数的执行机制,此篇将会从函数的命名.注释和鲁棒性方面,阐述如何通过 JavaScript 编写高质量的函数. <如何编写高质量的 JS 函数(2)-- 命名/注释/鲁棒篇>从函数的命名.注释和鲁棒性方面,阐述如何通过 JavaScri

001 java为什么需要函数式编程

一 .概述 集合是我们java程序员每天都需要的工具,没有了集合,java程序员几乎不能干任何的事情,我们每天的工作也是在对集合进行不同的操作. 尽管集合的功能已经足够强大,但是当我们面对复杂的业务问题的时候,利用原始的集合操作就会变得让人恶心. 于是在java8之中出现了lambda和stream的API,为我们以一种更加优雅的方式使用集合. 没错,就是集合,当我们现在使用NOSQL,各种日志分析,等等的大数据操作的时候,我们不可能使用原始的数据库的sql操作帮助我们完成如排序,求和,分组等操