程序员为何与函数式编程“坠入爱河”?

81540398 2020-09-04

本文转载自公众号“读芯术”(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和其他语言中加入函数式编程原理意味着什么的话,那就是函数式编程正越来越受到关注。这完全说得通:函数式编程对于大型数据库、并行编程和机器学习大有裨益。而在过去十年间,这些迎来了蓬勃发展。

相关推荐

86447318 / 0评论 2019-10-23