本文转载自公众号“读芯术”(ID:AI_Discovery)。
函数式编程发展至今已有60年的历史,但是截至目前,它仍然算是比较小众。尽管像Google这样的大公司依赖于函数式编程的关键概念,但是普通程序员对此几乎一无所知。
这种情况即将改变了。不仅是Java或Python这样的语言越来越多地采用了函数式编程的概念,类似Haskell这样的新语言也正在完全实现函数式编程。
简单来说,函数式编程就是为不可变变量构建函数。与之相反,面向对象的编程则是有一组相对固定的函数,而用户主要是修改或添加新变量。
由于函数式编程的特性,它非常适合完成诸如数据分析和机器学习之类的需求任务。但是这并不意味着用户要告别面向对象的编程,转而完全使用函数式编程。但用户需要了解其基本原理,以便在适当的时候使用它们以发挥优势。
一切都是为了消除副作用
要了解函数式编程,首先需要了解函数。函数是将输入转换为输出的东西,它并不总是这么简单。下面看一个Python中的函数:
- def square(x):
- return x*x
这个函数很简单。它需要一个变量 x,或者是一个int,又或者是float或double,然后输出该变量的平方。
现在再思考这个函数:
- lobal_list = []def append_to_list(x):
- global_list.append(x)
乍一看,该函数看起来像是接受了一个任意类型的变量x,并且由于没有 return 语句,它不会返回任何值。
请等一下!如果未事先定义global_list,那么该函数将不起作用,并且在经过修改后仍输出相同的列表。尽管global_list从未被视为函数的输入,但使用函数时它也会发生改变:
- append_to_list(1)
- append_to_list(2)
- global_list
它将返回[1,2]而不是一个空列表。即使我们对此并不明确,但这表明该列表确实是该函数的输入。这种不明确可能会造成问题。
图源:GitHub
不忠实于函数
这些隐含的输入,或在其他情况下的输出,有一个官方的名称:side effects(副作用)。虽然本文所举的只是一个简单的示例,但是在更复杂的程序中,这些副作用可能会导致真正的困难。
请思考一下如何测试append_to_list:用户不仅需要阅读第一行并使用任意的x来测试函数,还需要阅读整个定义,理解其作用,定义global_list并且以这种方式进行测试。当需要处理带有数千行代码的程序时,此示例中的简单操作可能很快就会变得乏味无趣。
有一个简单的解决方法:忠于函数认定为输入的内容。
- newlist = []def append_to_list2(x, some_list):
- some_list.append(x)append_to_list2(1,newlist)
- append_to_list2(2,newlist)
- newlist
它并没有做出太大的改变。输出仍然是[1,2],并且其他所有内容也保持不变。但是有一样改变了:该代码现在摆脱了副作用。
现在,当查看函数声明时,用户能确切地知道发生了什么。因此,如果程序运行不正常,用户也可以轻而易举地单独测试每个功能,并查明哪个功能有问题
函数式编程正在编写纯函数
没有副作用的函数是指其输入和输出都具有明确的声明,而没有副作用的功能就是纯函数。
函数式编程一个非常简单的定义:仅用纯函数编写程序。纯函数永远不会修改变量,而只会创建新的变量作为输出。(笔者在上面的示例中稍微“作弊”了一下:它遵循函数式编程的原则,但仍使用全局列表。用户可以找到更好的示例,但这只是基本原则。)
此外,对于给定输入的纯函数,可以得到特定的输出。相反,不纯函数则依赖于一些全局变量。因此,如果全局变量不同,则相同的输入变量可能导致不同的输出。不纯函数会使代码的调试和维护变得更加困难。
有一个更容易发现副作用的小窍门:由于每个函数都必须具有某种输入和输出,因此没有任何输入或输出的函数声明一定是不纯的。如果采用函数式编程,这些则可能是第一批需要的更改声明。
图源:unsplash
函数式编程不仅只有Map和reduce
函数式编程中不包含循环结构(Loops),请看下面这些Python中的循环:
- integers = [1,2,3,4,5,6]
- odd_ints = []
- squared_odds = []
- total = 0for i in integers:
- if i%2 ==1
- odd_ints.append(i)for i inodd_ints:
- squared_odds.append(i*i)for i insquared_odds:
- total += i
相较于我们要执行的简单操作,以上代码明显过长。而且由于修改全局变量,它也不够有效。我们可以用以下代码替代:
- from functools import reduceintegers = [1,2,3,4,5,6]
- odd_ints = filter(lambda n: n % 2 == 1, integers)
- squared_odds = map(lambda n: n * n, odd_ints)
- total = reduce(lambda acc, n: acc + n, squared_odds)
这是完整的函数。因为不需要迭代一个数组的许多元素,所以它更短也更快。而且,一旦了解了 filter、map和reduce 如何工作,代码也就容易理解了。但这并不意味着所有函数代码都使用map、reduce 等。这也不意味着需要借助函数式编程来理解map 和 reduce,这些函数只是在抽象循环时弹出很多。
- Lambda functions:谈及函数式编程的发展史时,许多人都会先提及lambda函数的发明。尽管,lambda毫无疑问是函数式编程的基石,但这并不是根本原因。Lambda函数是可使程序发挥作用的工具。但是,lambda也可用于面向对象的编程。
- Static typing:上面的示例不属于静态输入,而是函数式的。即使静态类型为代码增加了一层额外的安全保护,但也并非一定要其函数化,不过这可能会是锦上添花。
一些语言对函数式编程更加友好
图源:unsplash
(1) Perl
Perl对于副作用的处理方法与大多数编程语言截然不同。它包含一个神奇的参数 $_,这使得处理副作用成为Perl核心功能之一。尽管Perl确实有其优点,但作者不会尝试使用它进行函数式编程。
(2) Java
如果要用Java编写函数式代码的话,只能自求多福了。因为该程序的一半不仅将都是static 关键字,而且其他大多数Java开发人员也会将此程序视为耻辱。
(3) Scala
Scala是一个很有趣的语言:它的目标是统一面向对象和函数式编程。很多人都觉得这很奇怪,因为函数式编程旨在彻底消除副作用,而面向对象的编程则试图将副作用保留在对象内部。
话虽如此,许多开发人员将Scala视为一种可以帮助他们从面向对象编程过渡到函数式编程语言,这可能会帮助他们在未来几年更容易完全过渡到函数式编程。
(4) Python
Python积极鼓励使用函数式编程。下列事实证明了这一点:每个函数在默认情况下都有至少有一个输入self。这就像是Python之禅:显式比隐式好!
(5) Clojure
根据其创建者的说法,Clojure的函数化达到80%。默认情况下,正如在函数式编程中所需要的,它的所有值都是不可变的。但是,可以通过对这些不可变值使用可变值包装类来解决此问题。当打开这样的包装类,可变值将再次不可变。
(6) Haskell
这是极少数纯函数式和静态类型的语言之一。尽管在开发过程中可能会耗费大量时间,但在调试程序时这些付出都会获得巨大回报。它不像其他语言那样容易学习,但是绝对值得花时间学习。
图源:unsplash
与面向对象的编程相比,函数式编程仍然小众。但是,如果说在Python和其他语言中加入函数式编程原理意味着什么的话,那就是函数式编程正越来越受到关注。这完全说得通:函数式编程对于大型数据库、并行编程和机器学习大有裨益。而在过去十年间,这些迎来了蓬勃发展。
虽然面向对象编程有着不可估量的优点,但函数代码的优点也不容忽视。只需要学习一些基本原理,就足以让用户成为一名开发人员,并为未来做好准备。