什么编程语言是函数式的?

文章目录
  1. 1. 扫视一番编程领域
    1. 1.1. 函数式编程不是…
  2. 2. 这对编程语言意味着什么?
    1. 2.1. JavaScript不是一门函数式编程语言
    2. 2.2. Java不是函数式编程语言
    3. 2.3. Scala面临着一项艰巨的任务
    4. 2.4. Clojure
  3. 3. Haskell
  4. 4. Perl
  5. 5. Python
  6. 6. Mocking
  7. 7. 设计一种检测副作用的模式
    1. 7.1. 没有输入参数说明了副作用原因
    2. 7.2. 没有返回值说明了副作用结果
  8. 8. 总结和结论

原文: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的笑话:

1
2
3
4
5
6
7
#+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中的一个基本的含有副作用模式的代码:

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

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

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

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

1
2
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(…) {…}

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

总结和结论

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