App下載

了解一下函數式編程,送給沉迷面向對象的程序員

猿友 2020-08-11 17:11:04 瀏覽數 (2833)
反饋

今天給大家講解一下函數式編程的小知識。函數式編程已經存在了60多年,但是到目前為止,它一直都很小眾。只有像Google這樣的改變游戲規(guī)則的企業(yè)才會依賴函數式編程,普通程序員對此幾乎一無所知。

這種情況很快就要被改變了。像JavaPython這樣的語言已經開始越來越多地開始采用函數編程,而像Haskell這樣的新語言已經完全融入了函數式編程。

簡單來說,函數式編程就是為不可變變量構建函數。相反,面向對象的編程是要具有一組相對固定的函數,而我們主要是在修改或添加新變量。

函數式編程具有非常適合諸如數據分析和機器學習之類的需求任務的特性。但是這并不意味著我們應該告別面向對象編程,轉而完全使用函數式編程。我們需要了解其中的基本原理,這樣我們就能在適當的時候使用它們。

(推薦教程:傻瓜函數式編程

一切都是為了消除副作用

要了解函數式編程,我們需要首先了解函數。 這聽起來可能很無聊,但總而言之,它很有見地。

簡單地說,函數是將輸入轉換為輸出的東西。只是事情并沒有那么簡單。思考一下,在Python中的下面這個函數的意義:

def square(x):
    return x*x

這個函數很簡單。 它需要一個變量x,可能是一個int,或者是一個 floatdouble,然后輸出該變量的平方。

再思考一下下面的這個函數:

global_list = []
def append_to_list(x):
    global_list.append(x)

乍一看,這個函數接受了一個變量 x,無論是哪種類型,由于沒有 return 語句,它什么也不返回。事實真的是這樣嗎?

如果事先沒有定義 global_list,那么這個函數就不能工作,它的輸出是相同的列表,盡管經過了修改。雖然 global_list 沒有聲明輸入,但當我們使用該函數時,它就會發(fā)生變化:

append_to_list(1)
append_to_list(2)
global_list

它返回了 [1,2],而不是空列表。這可能就是問題所在,列表確實是函數的一個輸入,雖然我們沒有明確說明。

1.不忠于函數

這些隱含的輸入,或者其他情況下的輸出,有一個官方名稱:副作用。雖然我們只列舉了一個簡單的例子,但在更復雜的程序中,這些可能會讓我們面臨真正的困難。

大家可以思考一下該如何測試 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],其他所有內容也保持不變。

但是,我們已經更改了一件事情:該代碼現在沒有副作用。

現在,當我們查看函數聲明時,能確切知道發(fā)生了什么。如果程序運行不正常,我們也可以輕松地單獨測試每個功能并查明哪個功能有問題。

2.函數式編程正在編寫純函數

具有明確聲明的輸入和輸出的函數是沒有副作用的函數,而沒有副作用的函數就是純函數。

函數編程的一個非常簡單的定義是:僅用純函數編寫程序。純函數永遠不會修改變量,只會創(chuàng)建新的變量作為輸出。

此外,對于給定輸入的純函數,我們可以得到特定的輸出。相反,不純函數可能依賴于某些全局變量。因此,如果全局變量不同,則相同的輸入變量可能導致不同的輸出。后者會讓調試和代碼維護變得更加困難。

這里有一個容易發(fā)現副作用的簡單規(guī)則:由于每個函數必須具有某種輸入和輸出,因此沒有任何輸入或輸出的函數聲明必須是不純的。如果采用函數式編程,這是你可能想要更改的第一個聲明。

函數式編程不僅是 map 和 reduce

循環(huán)不是函數式編程中的東西。首先,我們先來思考以下的Python循環(huán):

integers = [1,2,3,4,5,6]
odd_ints = []
squared_odds = []
total = 0
for i in integers:
    if i%2 ==1
        odd_ints.append(i)
for i in odd_ints:
    squared_odds.append(i*i)
for i in squared_odds:
    total += i

相較于我們要執(zhí)行的簡單操作,以上代碼明顯過長。而且也沒有起到作用,因為我們正在修改全局變量。

相反,我們可以用以下代碼替代:

from functools import reduce
integers = [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, mapreduce 如何工作,代碼也就不難理解了。

這并不意味著所有的函數代碼都使用 map、reduce 等。這也不意味著你需要函數式編程來理解 mapreduce。只是當你抽象循環(huán)時,這些函數會彈出很多。

1.Lambda函數

在談到函數式編程的歷史時,許多人都是從lambda函數的發(fā)明開始的。 盡管 lambda 是函數式編程毫無疑問的基石,但它們并不是根本原因。

Lambda 函數是可用于使程序起作用的工具。 但是,我們也可以在面向對象的編程中使用lambda

2.靜態(tài)類型

上面的示例雖然不是靜態(tài)類型的,但是它依然是函數式的。

即使靜態(tài)類型為我們的代碼增加了一層額外的安全保護,但是其函數正常也并非必不可少。 不過,這可能是一個不錯的補充。

有些編程語言的函數式編程越來越強

1.Perl

Perl 對副作用的處理方法與大多數編程語言截然不同。它包含了一個神奇的參數 $\ 。Perl確實有它的優(yōu)點,但我不會用它進行函數式編程。

(推薦教程:Perl教程

2.Java

如果你在用 Java 進行函數式編程,那我只能祝你好運了。因為你的程序有一半是由靜態(tài)關鍵字組成的,而且其他 Java 開發(fā)人員也會把你的程序視為恥辱。

這并不是說 Java 有多糟糕,而是因為它并不是為那些用函數式編程解決問題而設計的,比如數據庫管理或機器學習應用程序。

(推薦教程:Java教程

3.Scala

有趣的是:Scala 的目標是統一面向對象和函數式編程。如果你覺得這有點奇怪,那你不是一個人,因為所有人都這么覺得:函數式編程的目標是完全消除副作用,而面向對象編程是把副作用保留在對象內部。

盡管如此,很多開發(fā)人員認為 Scala 是一種幫助他們從面向對象編程過渡到函數式編程的語言。或許在未來幾年里,它們會更容易全面發(fā)揮作用。

(推薦教程:Scala教程

4.Python

Python 鼓勵函數式編程。一個事實就能看到這一點:每個函數在默認情況下至少有一個輸入self。這很像Python的禪:顯式比隱式好!

(推薦教程:python教程

5.Clojure

據它的創(chuàng)建者說,Clojure 大約有 80% 是函數式編程。默認情況下,所有值都是不可變的,就像在函數式編程中需要它們一樣。但是,我們可以通過在這些不可變的值周圍使用可變值包裝器來解決這個問題。當你打開這樣一個包裝,你得到的東西又是不變的。

(推薦教程:Clojure教程

6.Haskell

這是為數不多的純函數式和靜態(tài)類型的語言之一。雖然在開發(fā)過程中這看起來像是一個時間消耗器,但在調試程序時,Haskell會付出巨大的代價。它不像其他語言那么容易學,但絕對值得投資!

(推薦教程:Real World Haskell 中文版

大數據時代帶來了函數式編程

與面向對象編程相比,函數式編程仍然是一個新生兒。但是如果在 Python 和其他語言中包含函數式編程原理,具有不一樣的意義,那么函數式編程就有可能獲得關注。

函數式編程對于大型數據庫、并行編程和機器學習非常有用。在過去的十年里,所有這些都在蓬勃發(fā)展。

雖然面向對象代碼有著不可估量的優(yōu)點,但函數代碼的優(yōu)點卻不容忽視。只需要學習一些基本原理,就足以讓我們成為一名開發(fā)人員,并為未來做好準備。

以上就是關于函數式編程的相關介紹了,希望對大家有所幫助。

0 人點贊