首页 编译原理

原文:Which Programming Languages Are Functional?
作者:Kris Jenkins
翻译:yelbee

在讨论函数式编程的第一部分里面,我不是从学术角度,也不是从市场角度定义函数式编程,而是以一种对兼职程序员有意义的方式定义函数式编程。更重要的是,我希望,我定义了什么是副作用,以便让一个兼职程序员在被概念绕晕之前能够理解副作用的含义。

现在,让我们看看现实世界中的函数式编程语言...

扫视一番编程领域

有了副作用的知识后,我们可以根据一个给定的函数然后分析它的复杂性了。而有了一些真实世界中对函数式编程的定义,我们现在可以遨游在编程的世界中,在几乎每一个方向都提出一些深刻的见解。

函数式编程不是...

  • 不是mapreduce

尽管你会在每一门函数式语言中看到这两个函数,但这并不是使这门语言成为函数式的原因。它只是一种将事物的序列中的副作用去除的函数。

  • 不是lambda函数

你可能也会在每一门函数式语言中看到头等函数。但它只是当你开始构建一种避免副作用的语言时自然而然出现的。它是函数式语言发展到某个阶段一种必然的产物,而非促成函数式语言的根本原因。

  • 不是类型

静态类型检查是一个非常有用的工具,但它不是函数式编程的必要条件。LISP是最早的函数式编程语言,但也同时是最早的动态语言。

尽管静态类型非常有用。Haskell非常漂亮地使用它的类型系统来对抗副作用,但是它们不是构成函数式语言的要素。

说了那么多,我只想强调,函数式编程只是关于副作用的。

这对编程语言意味着什么?

JavaScript不是一门函数式编程语言

函数式语言可以帮助你尽可能地消除副作用,并在你无法控制的地方控制它们,而JavaScript不满足这个条件。事实上,很容易发现JavaScript积极鼓励副作用的地方。

最简单的例子是this。每个函数中隐藏的输入。特别神奇的是,它的意思变化得如此之快。即使是专业的JavaScript程序员也很难跟踪this当前引用的内容。从功能的角度来看,this这种神奇的使用方法,这本身就是一种设计的缺陷。

当然可以将加载函数式编程库(Functional Program Library - FP Library,比如不可变的js文件)加载进JavaScript。这使得函数式编程更加容易,但这并没有改变语言的本质。

(顺便说一下,如果你喜欢在JavaScript领域日益流行的函数库,想象一下你多么希望有一种支持函数风格的完整语言)

Java不是函数式编程语言

Java绝对不是一种函数式语言。在Java 1.8中添加lambdas并没有改变这一点。Java与函数式编程截然相反。它的核心设计原则是,代码应该被组织成一系列局部的副作用——依赖于并改变对象的局部状态的方法。

事实上,Java反对函数式编程。如果您编写的Java代码没有副作用,没有读取或更改本地对象的状态,那么您将被称为糟糕的Java程序员,因为Java不是这样写的。你的没有副作用的代码必须填上static的关键字,而事实上,确实有程序员因为写太多的static被同事们嘲讽,并被公司开除的。

我并不是说Java是错的。但关键是它对副作用的看法完全不同。Java认为将副作用限制一定的范围内是构成好代码的基石,反之,函数式编程则认为副作用是魔鬼,应该在代码中完全抹除掉它们。

你可以从一个稍微不同的角度来看。Java和函数式编程都对回应了副作用这个问题。这两种模型都将副作用视为一个问题,并做出了不同的反应。面向对象的的答案是,将它们包含在称为对象的边界内;而函数式编程的答案是,消除它们。不幸的是,实际上Java并不只是试图封装副作用,它默许它们。如果你没有以有状态对象的形式创建副作用,那么你就是一个糟糕的Java程序员。事实上,人们会因为写太过频繁的static而被解雇。

Scala面临着一项艰巨的任务

从这个角度来看,Scala是一个非常具有挑战性的命题。如果它的目标是统一面向对象函数式编程这两个世界,那么从副作用的角度来看,我们认为它试图弥合“强制副作用”和“禁止副作用”之间的鸿沟。他们的观点截然相反,我不确定他们能否调和。您当然不能仅仅通过让对象支持map函数来统一这两者。你需要更深入地了解,并调和两种对立的立场在副作用上的冲突。

我将让你来判断Scala是否成功地实现了这种协调。但如果我负责Scala的市场营销,我会把它作为一个逐步从Java的副作用世界转移到函数式编程的纯粹世界。不是试图统一它们,而是作为一个桥梁。事实上,很多人在实践中都是这么看的。

Clojure

Clojure在副作用方面采取了一种有趣的立场。它的创建者Rich Hickey说Clojure大约有“80%是函数式的”,我想我可以解释为什么会这样。从一开始,Clojure就被设计用来处理一种特定的副作用:时间。

为了说明这一点,这里有一个关于Java的笑话:

#+BEGIN_QUOTE
    What’s 5 plus 2?
    7.
    Correct. What’s 5 plus 3?
    8
    Nope. It’s 10, because we turned 5 
    into 7, remember? #+END_QUOTE

好吧,这不是一个好笑话。但关键是,在Java的世界中,值不会保持不变。我们可以合法地取一个表示5的数,调用一个函数,然后发现它不再是5了。数学告诉我们,5永远不会改变 —— 我们可以调用一个函数,赋予我们一个新的值,但我们永远不能影响5本身的性质。Java说值总是在变化的,只要它们被封装在对象边界中就可以了。

整数的情况可能看起来微不足道,但是当我们查看更大的值时,就不是这样子了。还记得文章第一部分中谈论的InboxQueue吗?InboxQueue的状态是一个随时间变化的值。我们可以说时间是InboxQueue含义的一个次要的影响原因。

Clojure非常关注时间的副作用。Rich Hickey的观点是,时间的隐藏效应意味着我们不能依靠值来维持现状;如果我们不能依赖于它,我们就不能依赖于函数的输入,因此我们不能依赖于任何东西来表现出可预测或可重复的行为。如果连值都有副作用,那么任何东西都有副作用。如果值不纯,程序中的任何东西都不能纯。

所以Clojure对时间的描述大刀阔斧。默认情况下,它的所有值都是不可变的。如果你需要更改值,Clojure提供了围绕不变值的wrappers,这些wrappers受到严格的约束:

  • 您必须通过wrappers来更改(可变的)值。
  • 您不能意外地创建一个可变值。您必须始终使用语言中的guards显式标记潜在的副作用。
  • 您不能在不知情的情况下使用可变值。您必须始终使用语言中的guards明确承认副作用的风险。
  • 当您打开一个可变值的wrappers时,返回的东西是不可变的。你可以很容易地走出依赖时间的世界,回到纯粹的世界。

就时间而言,Clojure是函数式编程语言的一个很好的例子。这种语言对时间的副作用充满敌意。默认情况下,它会在任何可能的地方消除它,在你认为必须产生副作用的地方,它会帮助您严格控制它,这样它就不会溢出到程序的其他部分。

Haskell

如果Clojure对时间怀有敌意,那么Haskell就是十足的敌意。Haskell非常讨厌副作用,并投入大量精力来控制它们。

Haskell对抗副作用的一个有趣方法是使用类型。它把所有的副作用都推到类型系统中。例如,假设您有一个getPerson函数。在Haskell中,它可能是这样的:

getPerson :: UUID -> Database Person

您可以将其理解为“接受一个UUID并根据Database的上下文中返回一个Person”。这很有趣——您可以查看Haskell函数的类型签名,并确定其中哪些涉及副作用,哪些没有涉及。你还可以保证,“这个函数不会访问文件系统,因为它没有声明这种副作用”。

同样重要的是,你可以看这样一个函数:

formatName :: Person -> String

要知道,这只是取一个Person并返回一个String,而没有其他的什么。因为如果有副作用,您会看到它们锁定在类型的声明中。

但也许最有趣的是,这个例子:

函数的声明告诉我们,formatName的这个版本包含与数据库相关的副作用。这到底是怎么回事?为什么formatName需要数据库?你的意思是,我需要设置和模拟一个数据库来测试一个名称格式化吗?这真是奇怪。

只要看看这个函数的声明,我就能看出设计上的问题。我不需要看代码,从概述中就能看出问题所在,是不是很神奇?

让我们简单地将其与Java的函数声明进行比较:

public String formatName(Person person) {..}

这相当于哪个haskel版本?如果没有看到函数的主体,就没有办法知道。它可能是单纯的版本,也可能访问数据库。或者它可能删除文件系统并返回“去你的老板!”的字样。类型的声明给你提供很少的信息,或者函数的表面应该是怎样的。

相反,Haskell的类型声明可以告诉你很多关于设计的信息。因为它们是由编译器检查的,它们告诉你一些你知道是正确的东西。这就意味着它们可以成为伟大的建筑工具。它们的表面设计探测在一个非常高的层次上,而且它们的表面编码模式也是如此。我将把functormonad这两个词从本文中去掉,但是我要说的是,高级软件模式是从高级分析开始的;而高级分析就会变得容易得多,当你有一个高级记号时。

Perl

在任何关于副作用的讨论中,Perl都值得在这里提到。它有一个神奇的参数$_,它的意思类似于“前一个调用的返回值”。它被许多核心库函数隐式地使用和更改。据我所知,这使得Perl成为惟一一种将全局副作用视为核心特性的语言。

Python

让我们快速看看Java中的一个基本的含有副作用模式的代码:

public String getName() {
  return this.name;
}

我们如何使这个调用变成纯函数呢?这是隐藏的输入,所以我们要做的就是把它提取出来:

public String getName(Person this) {
  return this.name;
}

现在getName是一个纯函数。值得注意的是,Python默认采用第二种模式。在python中,所有的对象方法都把this作为第一个参数,除了按照惯例它们把它叫做self

def getName(self):
    self.name

Mocking

Mocking框架通常做两件事。

首先,它们帮助你设置值的对象作为输入。你的语言越难设置复杂的值,你就会发现这一点越有用。但这是题外话。

第二种方法在本讨论中更有趣——它们帮助你为测试中的函数设置恰当的副作用原因,并跟踪在测试之后发生的哪些副作用结果。

从副作用的角度来看,mocks是一个标志,表明你的代码是不纯的,并且在函数式程序员的眼里,它是错误的证明。与其下载一个库来帮助我们检查冰山是否完好无损,我们应该绕着它航行。

一个测试驱动开发的Java的核心人员曾经问我如何在Clojure中进行mock。答案是,我们通常不会。我们通常将其视为需要重构代码的标志。

设计一种检测副作用的模式

如果有一本关于副作用的I-Spy书籍,那么最容易发现的两个目标就是不带参数的函数和无返回值的函数。

I-SPY系列图书是专为英国儿童编写的侦探指南,在20世纪50年代和60年代以其原始形式尤其成功,2009年米其林(Michelin)在出版间隔7年之后重新推出了这本书。

没有输入参数说明了副作用原因

当您看到一个没有参数的函数时,有两件事是正确的:要么它总是返回完全相同的值,要么它从其他的地方获得输入(例如,它有副作用)。

例如,这个函数必须总是返回相同的整数(或者它有副作用):

public Int foo() {}

没有返回值说明了副作用结果

每当你看到一个没有返回值的函数,要么它有副作用,要么没有必要调用它:

public void foo(...) {...}

根据那个函数声明,绝对没有理由调用这个函数。它不会给你任何东西。调用它的唯一理由是,咱们可以倾家荡产,完全信任它悄无声息地带来的神奇副作用。

总结和结论

对副作用的真实、直观的认识将改变你观察代码的方式。它将改变一切,从你如何看待单个功能,一直到整个系统架构。它将改变你看待编程语言、工具和技术的方式。它改变了一切。来吧,让我们今天去杀死副作用...




文章评论

captcha