函数式JavaScript编程指南

函数式JavaScript编程指南

简介

你是否知道JavaScript其实也是一个函数式编程语言呢?本指南将教你如何利用JavaScript的函数式特性。

要求:你应当已经对JavaScript和DOM有了一个基本的了解。

写这篇指南的目的是因为关于JavaScript编程的资料太多了但是极少的资料提到了JavaScript的函数式特性。在本指南中,我只会讲解这些基本知识而不会深入其它的函数式语言或这是Lambda算子。

你可以点击所有的例子然后你所看到的代码就会被执行,这样就可以令指南变得具有交互性。你也可以使用这个沙箱来尝试。

第一课 —— 匿名函数

我们将首先介绍匿名函数。一个匿名函数就是一个没有名字的函数。
你可以认为他们是一次性函数。当你只需要用一次某个函数式,他们就特别有用。通过使用匿名函数,没有必要把函数一直放在内存中,所以使用匿名函数更加有效率。

例Example:

下面两个函数处理同样的事情,而average在给z赋值结束之后一直保留——但匿名函数则不会。

这很自然得引出了我们下面的一节课函数作为值

第二课 – 函数作为值

事实上,我们一般在JavaScript中声明函数的方式可以看作是一个简化了的语法(也就是语法糖syntactic sugar)。

例:

下面两个表达式其实完全一样。所以左边的表达式仅仅是右边的简写。

从这里可以得出一个结论,函数是一个值就像字符串、数字或数组一样。这还出现几个问题:

我是否可以把函数作为参数传递?
可以,见下面的例子。
是否可以实时生成函数?
当然了,这是一个高级的主题,它可以通过eval函数来完成。小提示:看看本页面的源代码。

例:

这个例子演示了如何把函数作为参数传递。

第三课 – 两种方式调用函数

在JavaScript中,有两种调用函数的方式。一般的方式是把参数放在括号中,如alert(42)。另一种方式是同时把函数和参数都放在括号中,如(alert)(42)

例:

为什么函数两边的括号很重要:如果你写了括号,那么在括号中的代码就会被先计算。在计算之后,括号所在的地方就会有一个值。这个值可能是一个字符串、一个数字或一个函数。

第四课 – “短路”条件调用

现在我们将学习如何使用“短路”条件调用。使用这个方法可以缩短源代码同时代码也变得更加可读。

例:

这个语法并不是用在左表达式上,而是用在右表达式上。

第五课 – 它好在哪里

OK,现在我们已经学习了一些函数式JavaScript的内容。那么它好在哪里?函数式JavaScript编程之所以很重要有三条主要的理由:

  1. 它有助于写出模块化和可服用的代码。
  2. 它对事件处理程序非常有效。
  3. 它很有趣!

在下面的篇幅中,我会给出更多关于前两条理由的信息

1. 模块化和可复用的代码

现在你已经知道如何将函数作为值使用,那么你也应该试试!一个很好的例子是数组内建的sort方法。预定义的sort()把所有的对象转换成字符串并把他们按照词语的顺序排序。但如果我们有用户自定义的对象或者数字那么它就不是很有用了。于是这个函数可以让你给他一个进行比较的函数作为参数,如sort(compareFunction)。这个方法让我们甚至不用接触实际的sort方法。

例:

2. 事件处理程序

对事件处理程序使用函数式编程也许是最直观的函数作为值得应用了。既然这样我们马上就演示一个例子。

简单的例子:;ie

现在有一个Button类,带一个自定义的onclick行为。

练习: 为什么我们要把alert包裹在一个匿名函数中?

高级例子:

现在我们想改进我们的Button类。每一个按钮都被分配了一个值当按钮被点击时显示该值。首先我们调整我们的类:

下面你也许要尝试写下面的代码:

如果你执行它你就会发现提示框中间是空的。为什么会这样呢?其实原因在于JavaScript的可见性规则。当onclick函数被执行时this指向的是按钮的DOM节点而非自定义的按钮对象。

我们如何解决这个问题? 使用函数式编程:

这种情况下执行该匿名函数会将v绑定到this.value上。

沙箱

在此处输入你的代码,并按下计算.


更多信息

下面是关于函数式JavaScript编程的一些有趣的链接:

展望

本节给大家展示一下JavaScript的未来。一个非常振奋人心的JavaScript特性——E4X,一个JavaScript中直接的XML支持。

Currying的翻译

函数Currying,是我所一直不能确定的英文翻译之一 ,另外还有一个Web的翻译。

函数Currying的意思就是将参数和函数关联起来,变成一个新的函数。比如一个二元参数f a b

当给出f 1的时候,应该返回什么呢?一般的语言中,要么是将b设为一个默认值,要么就是直接出错,而当有了Currying,那么f
1其实返回一个函数g x = f 1 x。
当然这个描述也不够好。

其实其命名是为了纪念一个逻辑学家 Haskell Curry 的――函数式语言Haskell也是为纪念这个人,当然这个东西并不是他第一个发现。

既然作为姓名,数学中可能不会进行翻译,就叫做Curry化,或者柯里化,不知道还有什么更好的翻译方法,能较为直接地体现其真正的含义。

JavaScript = C + Lisp

作者: William Taysom

原文地址:http://www.jadetower.org/muses/archives/000307.html

翻译:ShiningRay

我在过去的几周内一直在写JavaScript代码——使用我们的对话框系统来个性化Mozilla。假设你要求:“嘿,电脑,我要教你如何在Amazon.com上找书。首先你象这样进入Amazon,然后在这里输入你要的书的名字。点击“Go”然后……”我的困难在于对Mozilla编码使我的对话框系统可以“看”浏览器中正在进行什么然后自己可以执行这些动作。

由于Mozilla中较高的层次是用JavaScript实现的。所以我一直在废寝忘食研究它(我的Rhino book里面全是我做的书签)我写的越多,我越觉得它像Lisp。

考虑以下代码:

这里SemTree是一个对象,它允许你从一个HTML DOM 树中选出某些你感兴趣的节点,去掉那些你不感兴趣的节点。(根本上说,这是一个TreeWalker 类的包装器。)若要建立一个 SemTree ,你要给出一个接受器。一个接受器只是一个判断给定节点是否能被接受的一个函数:

一旦有了一些基本的接受器和筛选器,很容易就可以定义组合筛选器──一种将筛选器以特殊形式组合起来的函数:

当它被调用的时候,以接受器作为参数,acceptAny返回一个新的接受器,可以接受只要是 disjuncts 能接受的那些给定节点 n。所以,semanticAccepter 中出现的acceptAny 能接受文本、链接、表单和链接中的图像。相反地,acceptOnlyIf只能接受被所有接受器组件接受的节点。acceptOnlyIf的定义类似于acceptAny

acceptOnlyIfacceptAny如此相似让我纳闷是否有一个通用的方法可以将任一个组合器(像否定、短路与、短路或)变成一个组合筛选器(象acceptNotacceptOnlyIfacceptAny)。的确有,但JavaScript不胜任这个任务。要完成这个,我们需要更强大的武器。

将函数的能力定义得和单子的功能一样微小

通过提供第一类型函数,JavaScript迈出了进入更广阔世界的第一步。而这个世界中最具影响的便是Haskell和它的一些变体。由于接受器告诉我们是否要接受一个节点,它们应该是一个从节点到布尔值的函数。在Haskell中,我们这样写:

这样,否定是一个布尔到另一布尔,合取和析取是从一个布尔的列表到一个单个布尔值,这样写:

组合接受器有类似的形式:

我们给semanticAccepter下的定义和JavaScript版的类似:

我们怎样定义一个类似acceptOnlyIf的组合器?Haskell没有像一些语言中的命令结构。取代的是递归:

由于某些原因,短变量名是Haskell中的标准。n是一个节点,a是一个接受器,as是一个接受器的列表。(以’s’结尾是代表某个变量是列表的标准形式。)你上面看到的定义是正确的,但我不常用这个方法。我会使用一个优美的小函数叫map

当传了一个函数和一个列表,map返回对列表中的每个元素执行函数后生成的列表。有了map后,acceptOnlyIf
就可以这样定义。

这里语法 (\a -> a n) 基本意思和JavaScript下面的一样:

整个acceptOnlyIf的定义本质上说明了,“给出一个节点n,找出每个接受器对n的值,然后返回这些值的合取值(和值AND)。”有了这种定义函数的优美方法之后,它们之间的相似之处立刻显现出来了:

这样,泛化就是一些琐碎的事了:

现在我们是否可以更进一步了呢?我们是否可以也将 not搞成 acceptNot呢?andnot的主要区别在于 and参数是一个布尔值的列表,而not只能针对单个布尔值。要更进一步泛化 liftCombiner,我们必须:

  1. 找出可以描绘出基本值和列表的共同特性的结构。
  2. 将这个结构应用到合成组合器的问题中.

Haskell 正好有我们所需要的。它称为单子Monad.

什么是单子?

之前就有人问过这个问题。单子到处可见。大多数结构和过程/进程像数据类型、函数、对象、、异常、I/O、副作用、同步、事务、分析器和编程语言,都可以接受单独的、原子的操作。组对(Pair)、元组(tuple)、列表、树、图——这些数据结构都有一个单子级的解释,是常常不止一个。由于是单子的东西太多了,所以很难对它们进行描述。不过我还是可以给你一个数学上的定义。但是正如“A continuation is the rest of the computation ”所说的,给出单子的定义只有在你已经对它有了一些感性的认识才有用。否则直接给出定义只会混淆你的观点。那先让我们研究一下这个谦虚的列表作为我们受到单子启发的途径。

检验结构有很多方法。其中一个方法是查看各部分是如何一起工作来组成整体的。当以这种方法分析列表的时候,我们发现它们是连接的解码:一个列表要么是空的要么是一个由一个head和一个tail组成的Cons(一种构造函数,返回一个新的列表)。head是一个列表的元素,tail则是列表的其它部分。

若要定义列表[1, 2, 3],我们这样写:To define the list [1, 2, 3], we write:

Haskell中的列表就是这样工作的。除了用[]代表 Nil和用: 代表 Cons。逗号可以用来分隔条目,可以写成这样:

链表有一个很长很有名的历史。不幸的是分解材料并不会显露出单子。

另一种分析列表的方法是通过研究它是如何和其它东西相关、进行交互的。Haskell提供了“class”机制根据和它们相关联的方法来定义对象。类似于抽象数据类型和接口:

这是一个看似完美的列表类,但它几乎没有从分解材料中抽象出什么东西。所有的方法,除了 map,都是特定于列表的逻辑结构的,map抓住了一个较抽象的概念。它将一个函数作为参数,然后返回一个被函数处理过新的列表。回忆一下 map 对定义 acceptAny acceptOnlyIf 起了多大的帮助?这很明确是个值得研究的函数。

还有哪些其它函数对列表作为一个独立于它特定实现和形态的数据结构来说是至关重要的呢?好吧,还应该有一个方法 unit来把一个单独的元素放入一个列表,而且我们还需要一个 join 来将一个列表的列表组成一个长的列表。这个类定义像这样:

以下是链表的实现:

这些方法一目了然:unit将参数放入列表,map对列表中的每个元素执行函数,join将一个列表的列表连成一串。让我们确定一下串连接符(++)的定义吧:

这些函数都十分简单而且十分有用。它们确实是列表的标准成分,但就好比火药一样,如果你把它们正确地组合起来,你就能引爆一些东西:一个单子。我们等不及这个式子了:

就是它了。两个函数:return将一些东西放入单子中,同时(>>=),称为“绑定”,将一个单子变成另一个。我们立即来看看单子到底能做什么。不过首先为了明确起见,我们先将我们的列表实现扩展成应用一个单子:

return定义和 unit一样:将它自己放入一个列表中。绑定 (>>=)将mapjoin 组合成一个操作。首先你将函数k映射到列表 ls ,然后由于 f返回一个列表的列表,你再用 join 将结果组成单个列表。

但单子这东西有什么好处呢?设想你想要将一个列表中的所有值和另一个列表中的值相加来产生一个大的列表:

JavaScript代码应该像:

应用了单子我们可以这样写:

代码看来很简练,但没什么特别的。幸运的是,由于单子是如此有用,所以更好的语法形式也出现很久了。我们应该这样写:

这称为“do notation做标记”为了明显起见。还有另一种变体,我个人喜欢叫“列表包含语法”或者“我无法相信这没有成为一个学说”:

不论哪种方法,都是所有的单子之上的一种较好语法。这种较好语法也许看上去像某种命令语言或什么蛇的东西(指Python语言──译注)。但我不接受其它替代语法,在Haskell中任何单子都能使用两者之一。

我们看看一个列表单子是如何工作的,但我们仅有的单子是 LinkedList。对于not,我们只需要一个直接变换值的单子就行了:一个一致单子。它不会对值做特殊的改变,仅仅直接返回值:

现在我们终于可以完整地定义 liftCombiner了:

现在“让组合器能工作在所有接受器上”的这个想法已经不难实现了:

最后的思考

今天我们看了如何处理组合的接受器(从节点到布尔的函数),组合器可以是任何单子。结果接受器也是单子。你认为是否有一种方法,可以让组合器组合任意单子和其它单子(除了接受器之外)?如果有,怎样做?如果没有,单子之间要有怎样的关联才能这样?

简介延续“Continuation”

作者: Denys Duchier

翻译: Shining Ray @ Nirvana Studio

写本文旨在回应comp.lang.python新闻组中关于延续的讨论。


对于call/cc(call with current continuation)的情结和关于他的操作解释粗糙的细节内容,至今一直掩盖了延续的简洁和优雅。在本文中,我想用两个方式来纠正这个问题:

  • 首先用一个简单且直观的方式展示延续的概念。
  • 第二通过提供_可运行的_Python代码,来描述如何使用延续而不用call/cc来实现搜索引擎。

我将展示:

  1. 一个针对命题公式确定性检查工具。
  2. 一个基本的带回溯和裁剪的prolog引擎。

我希望这能帮助那些对于这个论题有些迷惑的人搞清这个问题。我还想指出上面讲到的两个程序是我在今天晚上花了几个小时写的,他们应该可以给你关于下面将要讲述的技术的能力的一些衡量尺度。


1. 简化延续

习惯上一般来说,一个函数返回一个值是这样的:

这隐含了它的值返回的地方。而延续的想法是通过添加一个延续参数来明确要返回的地方。函数不“返回”值,而是通过把值作为一个参数传递给延续“继续”处理值。在基于延续的程序中,以上函数foo变成了:

从这个角度看,函数从不使用返回“return”。取代的是它“继续”。正因为这个原因,延续有时候会被描述为“带参数的goto”。

上面描述的想法是。更精确地说,它是CPS(Continuation Passing Style,延续传送风格)的一个初步代码转换。基本的想法是给每个函数添加一个额外的“延续”参数,并进一步转化函数体以便不用返回他的值,而是把值传送给这个额外的延续参数。

在foo函数的例子中已经大体描述了这个概念。然而,更确切的,要注意CPS转换同时展开了所有的非lambda算子的嵌套表达式(换句话说,就是他显式地连接了所有子表达式的计算)。让我们看一个例子:

在延续传送的观点中,甚至像*+之类的基本操作符都要带一个额外的延续参数。我们通过以下定义进行模拟:

现在,CPS可以把上面的baz函数转换成:

换句话说,2*x的计算现在使用了一个延续来接收结果v并使用它来计算 v+y并最终把这个结果传给总的延续c

当在这个上下文中理解了call/cc之后,它就不再神秘了。它只是一个特殊的手段,能让我们的双手去接触那个不可见的由CPS引入的额外的延续参数,并让我们像程序中其他函数值那样使用它。思考call/cc(f),其中f需要接受当前延续作为参数。其实说到底,call/cc(f)可以通过CPS转换成call_cc(f,c),而c就是CPS所引进的延续参数同时call_cc可以按照以下方式定义:

即,f的普通参数和由CPS引入的额外的延续参数都是当前的延续c

还有一些细节,不过以上部分是基础。

CPS转换是很多针对函数式语言的编译器的基础。它的缺点是它引入了很多lambda算子(即闭包),同时,必要的是,编译器要尽可能多得把他们优化出去。在这方面,Scheme的Rabbit编译器的Steele,T的Orbit编译器的Kelsey etal. 和SML/NJ编译器的Apple有着很多的研究。有一个优点是,如果lambda表达式是你唯一的控制结构而且你把他们优化到了极限,那么你同时也优化了所有的控制结构。

然而应该注意的是,也正如很多人已经注意到的,由于编译器的工作恰是常常要把CPS引入的东西删除,所以有些人对将CPS转换作为编译过程基础的价值提出了质疑。

2. 将延续作为一个普通的编程技术

你可能已经注意到一些人对于不可思议难懂的延续的应用极其狂热。但事实上还是有着很多“非不可思议”的延续的应用,他们也不要求call/cc的存在。你可以在Python中书写延续传递程序,或者在任何支持闭包的部分形式和有自动垃圾收集的语言中写。

我最了解的应用程序是关注于“搜索”。它和正在进行的关于迭代子的讨论有很大的关系。很多年以前,我从我以前的导师Drew McDermott那里学习了这个技术,我会在下面讲述它。然而,我得指出,和Tim的特性记述相反,生成器(generator,在Drew的观点上)不需要以“类栈方式”进行表现;尽管很少有提出不这样用的 :-)

这个概念是通过传递两个延续来进行搜索:

  1. 一个成功延续,用于进一步进行搜索
  2. 一个失败延续,用于回溯到第一个更早的选择点

用Python来表达它,常常是以下面的形式出现:

“info”是在搜索中传送的一些信息。“yes”是成功延续,“no”是失败延续。“yes”以当前的“info”状态和当前的失败延续作为参数。而“no”则没有参数。

如果self.check(info)为真,则Foo对象满足了搜索条件。

一个有两个Foo“one”和“two”属性的类Baz。定义一个Baz对象来满足搜索条件,只要“one”属性满足它或者“two”属性满足(换句话说就是一个Baz对象是一种析取式)。我们通过调用“one”属性的搜索方法来表达,同时还给他传递一个尝试“two”属性的失败延续。

很明显,在上面的内容中,由于Python缺乏对于闭包的真正支持,使得要按照函数式语言中简洁和优雅的写法书写有些困难。

3. 检查命题公式的确定新

命题逻辑的公式如下:

它表示

p,q,r 是可以被赋以真值得命题变量。你可以证明不管你给p,q,r赋什么值,上面这个公式总是为真的。看一个更加简单的公式会更清楚,如(p | !p)也就是“p或非p”。这样一个公式是所谓的“确定的”:它总是为真,无论你如何解释他的变量。

下面的Python程序实现了针对命题公式的确定性检验工具,它使用了前面描述的延续传送风格。这个程序仅仅只是要描述一个例子而已。对于这种人物有更加有效的方法。然而,我相信它可以很好地表达关于使用延续传送实现搜索的一个通用的想法。

两个程序(确定性检查工具和前面提到的prolog引擎)都可以在下面的URL中获得: