數學和編程中「函數」的概念相同在哪裡,不同在哪裡?

特別因為中間橫著一門"函數式編程"


數學上的函數沒有副作用,不依賴於外部參數,結果是確定的,在程序設計中叫做「純函數」。

數學上的函數是這樣的:

f : 參數→函數值

程序中函數是這樣的:

f : (參數,外部變數)→(函數值,副作用)

另一方面,數學上的函數注重映射本身,而編程的注意力則主要在運算過程。


較長,不喜者請直接跳至最後一段看結論。

函數(本文指單值函數)用集合論的定義,就是一個集合到一個集合的二元關係(嚴格定義不做表述),大概有這樣的兩個特性:

  • 確定性,對於定義域中一個確定的元素,在值域中是否有元素和它對應、那個元素跟它對應是一個確定的,換句話說,就是有確定的輸入一定有確定的輸出;

  • 單值性,即定義域中的一個元素只對應值域中的一個元素,也就是如果f(a)=b成立,那麼f(a)=c一定不成立;

而編程中的「函數」概念,則有很大的不同了,在英文中也有很多不同的叫法。在BASIC中叫做subroutine,在Pascal中叫做procedure和function,在C中只有function,在Java裡面叫做method。

當他叫做subroutine(子過程或者子程序)的時候,不管怎麼樣,都是非常恰當的,因為這表述的是一個計算機問題,程序跳轉到了另外一段程序,對某一個特定的問題進行了處理。而Pascal中,有了對procedure(過程)和function的區分。function是有返回值的,就比較接近數學上的描述;而procedure是沒有返回值的,如同C裡面的返回值是void類型的函數。

不過,不管是Pascal還是C,以及我們熟知的Python、Ruby、Lua等語言,他們的函數都和數學意義上的函數來得不一樣,他們能夠滿足單值性(特別的,類似Ruby中a,b = f(x)的,我們也說只返回了一個值),但是卻不一定能夠滿足確定性。

不能滿足確定性,就是因為第一次調用「函數」計算f(x) = a,那麼下一次調用則可能f(x) = b。也就是所,這個「函數」不是無狀態的,是有記憶功能的。而數學上的函數是沒有狀態的,是沒有記憶的。這種因為「函數」的有了狀態,而出現了不確定性,一般叫做副作用(side-effect)。在面向對象的編程裡面,類的成員函數,基本上不太可能每一次調用都返回同樣的值,他們的值還要取決於其他的成員變數。所以,很多面向對象的語言,類裡面的「函數」都稱作「方法(method)」。

再然後,就是有一種叫做函數式編程語言的事物,他們認為,這種沒有副作用的真正的函數,對於我們編程是更有利的,能夠提高正確率,更好更快的完成編程。所以就在編程語言中引入這種思想。不過實踐中,如果沒有狀態的改變可能會很難完成一些編程任務,比如類似LISP這樣的函數式編程語言也需要setf之類的語句來改變和保存狀態。號稱純函數式編程的Haskell我不清楚具體的語法,不過能操作鏈表應該也會涉及到改變和保存狀態的操作,還請熟悉Haskell的高手賜教。

綜上所述,數學中的函數和編程中的函數還是有不一樣的,而這個最主要的不一樣來自於函數的確定性(或者是副作用)。函數式編程則是儘力的消除副作用對程序帶來的影響。另外,我個人一直認為,那個我們稱作函數的「function」的詞,翻譯做「功能」更為恰當。


居然在人人網看到關於問題不錯的一些說法, 邪了門了.. 看 SICP 的同學進:

http://zhan.renren.com/lichedu?tagId=1968from=template

如果要問現代數學最重要的概念是什麼,那毫無疑問就是函數了,或者更確切地說,是映射。泛函這個詞,或許對非數學系的同學來說有些陌生,但如果寫成英語 functional, 看起來就眼熟多了。狹隘一點地說,泛函就是以函數為參數,返回值是數值的一類函數。看到這裡相信不少同學都發現了,這就是在很多語言中被稱為高階函數(high-order function)的那個東西。泛函在數學中是如此普遍的概念,現代數學幾乎無處不會用到。數學家們很自然地在集合上添加運算,構造空間;從一個空間映射到另一個空間,創造泛函。對泛函做變換,構造泛函的泛函等等。

為什麼我要在這裡提到數學和泛函?因為在我看來, lisp 是一門以表達數學為己任的語言。正如 SICP 中希望表達的一種觀點:語言會影響思維。如果數學推理過程中最頻繁應用到的泛函,在計算機語言中卻沒有對應的表達,換言之數學思維不能很自然地表述為計算機語言的話,那麼計算機對於數學研究的意義就顯得很可疑了,畢竟那時候的計算機可不是用來玩大菠蘿3的。所以這裡就有了兩撥人,務實的一撥人開發出了 fortran ,力主解決數值計算;務虛的一撥人則創造了 lisp ,試圖一舉解決符號計算的難題。在 John McCarthy 所作的 history of lisp 中這樣寫到:

「Then mathematical neatness became a goal and led to pruning some features from the core of the language.(保證數學上的簡潔性成為我們的目標,並因此拒絕了將一些特性加入到語言核心中。)

This was partly motivated by esthetic reasons and partly by the belief that it would be easier to devise techniques for proving programs correct if the semantics were compact and without exceptions.(這部分是基於美學上的考慮,部分是因為我們相信,緊湊而沒有特例的語法才更有可能設計出一種從數學上證明程序正確的方法。) 」


題主既然提到函數式編程,前邊的回答有些壓根沒提函數式編程,有些則只提這個,未免有些偏差。編程概念里的「函數」一詞也有很多種意思(見文末),和數學上的函數不盡相同。我嘗試做一個比較完善的回答。

代碼/程序(靜態的存在,以可讀的文本或者二進位文件表示)是一種表示特定時空中的特定規律的方法,而進程(被執行的程序)是程序所表示的規律在時間軸上的展開。可以類比一下傅立葉變換(編程是時域-&>頻域,執行程序是頻域-&>時域)。程序為了表示某個規律,就需要對該規律所存在的時空進行抽象。

範式大概可以這麼分類:

  • 函數式編程:對數學空間的抽象
  • 非函數式編程
    • 面向過程:對時間軸上發生的事件的抽象(但是這個時間軸不是嚴格向前的,是可以呈現單向、扭曲和分岔三種狀態的,對應順序結構、循環結構和選擇結構)
    • 面向對象:對現實空間,或者類似現實空間的空間的抽象,注重實體(物體)自身的功能性和物體之間的相互作用
    • 面向數據:對數據的模式(資料庫的內模式外模式邏輯模式那個模式,即存儲和調用方式)空間的抽象

說的詳細一點。如果把計算機看成一個黑盒子,不去研究內部工作原理的話,我們對計算機的使用限於輸入、輸出設備。(輸入設備產生的效果*時間) (叉乘,下同)可以抽象成 A={在 x1 時間將 y1 電信號輸入計算機},同理 (輸出設備產生的效果*時間) 是 B={在 x2 時間將 y2 電信號轉換成人類或其他系統可感知的信號 y2"}。程序就是一個用 A 作為自變數, B 作為應變數的映射關係 f(x1, x2, y1, y2, t)。那麼和數學上的函數類似,如何描述這個 f 就有多種方法(列表法,圖像法,函數法等等),這些方法就被我們稱為編程範式。

關於函數一詞的意思:在函數式編程裡面函數和數學上的函數意義大致相同;非函數式編程的話,函數一般指的是一段可以被重複插入在不同位置的代碼段(宏,macro)。如果假設編譯器可以完全展開所有代碼成線性(即去除所有非選擇結構的跳轉)的話,函數就不存在了。


計算機的函數是「可計算的」

而數學中很多函數都是「不可計算的」

可計算性又是另外的問題了,記得應該是等價於停機問題的


兩種函數在重入的時候,表現不一樣


(不完全保證正確性,僅供參考)

個人理解,函數其實就代表著一種映射關係,提供輸入得到輸出,這一點在數學和編程中都是一樣的。

但是,要說到不同之處,前面的答案有提到,數學中的函數不一定可計算。除此之外,個人認為,首先,計算機中的函數,如果屬於數學中的函數則必須是良定義的(就算看起來結果飄忽不定其實也只是有隱含的輸入參數的影響,不存在同時有多套輸出的情況),但是數學中的函數可以不是良定義的(然而通常非良定義的函數基本沒什麼卵用貌似)。(良定義的意思是,對於每一個定義域內的輸入有且只有一個輸出,不會一個輸入好幾個輸出這樣。)(題外話,C#里的事件感覺不像是良定義的,因為給事件掛上好幾個帶返回值的函數的時候事件的返回值就會莫名其妙。雖然說關於這一點的處理機制是固定的,因此也勉強算良定義了,但是,說實話這個跟數學裡非良定義的函數的實用性應該差不多,如果是想要獲取返回值的話。)

除此之外,計算機中的「函數」可能根本就不是函數。因為,計算機中的函數可以沒有輸入(裡面只有一條返回某個常量的公式,不一定是好的做法但是你可以這麼做),可以沒有輸出(函數體是空的),可以有兩個返回值(fork函數),可以根本就不會返回(守護線程,以及各種主循環之類的),總之,因為編程時寫下的嚴格來說其實是過程而不是數學中的函數,因此所謂的「函數」也就可以寫出各種不是函數的東西了。

至於函數式編程,前面有答案提到數學中函數的無副作用性。在這裡補充下個人見解。首先,前面有提到計算機函數的輸出可以有不確定性,這一點我認為不對,因為,你完全可以把計算機函數的各種「狀態」也視為輸入的一部分看待(也就是說有隱含的輸入),至於輸出自動覆蓋「狀態」的行為,並不會影響計算機函數在輸入固定的情況下輸出也會固定這一點。(你也可以在數學證明裡隨便亂賦值,但是會不會被罵就不關我事了。)(簡單來說就是,計算機函數也是「無副作用」的,但是從表面上看是「有副作用」的。)

那麼為什麼還要提出函數式編程呢?主要是為了簡化問題。在不使用「狀態」,不改變輸入的實參的情況下,debug起來會更方便,因為函數吃了哪些輸入吐了哪些輸出會一目了然,不存在暗中搞鬼的行為。因此,函數式編程會推崇使用「不可變數」,也就是定義的時候賦值,賦值完就不可以再重新賦值的「變數」。如此一來,你就沒法使用引用參數之類的奇怪的東西,函數不會去修改任何輸入也不會去偷偷寫某些狀態,只會吐出一個新的輸出。(舉個例子,假如有一個立方體對象,其中有一個方法是將立方體的邊長加倍,面向對象的做法可能是void DoubleSideLength(){this.SideLength*=2;},面向函數的做法則會更像是Cube DoubleSideLength(){return new Cube(sideLength: this.SideLength*2);},也就是說原來的對象沒有改變,返回的是一個新的對象。)當然,這只是函數式編程的特點, 並不是說一定是解決問題的最佳方法。

除此之外,函數式編程還模仿數學中的函數,大量採用匹配和遞歸。(大致對應數學中的分段函數和循環函數)。匹配基本上就是if和switch的套路,不細說了,遞歸的話是為了替代循環結構。

另外,一般來說不必擔心效率問題,因為函數式編程下的函數寫得即使在表面上也沒有副作用,編譯器是可以做懶調用和尾遞歸優化的。懶調用的意思是,用到某個值的時候才計算,那麼很多多餘的計算就不會執行,當然編譯器也可以像內聯函數那樣把調用展開來,節省調用的開銷;尾遞歸優化基本上是把遞歸自動優化成while貌似,不是特別懂,不過基本上是這個套路。


數學上的定義很明確:函數是表示一個集合A到另一個集合B的映射關係

編程中的函數貌似沒有明確的定義,可以理解為:函數是一段可以調用的子程序。

相同之處是他們都可以有確定的輸入得到確定的輸出結果。

不從之處是,數學上的函數表示的是映射關係,而程序中的函數重點在可調用。


程序設計里,函數通常也稱過程。通俗點,數學上,函數是相同的輸入得到相同輸出;程序里,同一函數可以有不同過程。比如:f(x)=2x和h(x)=x+x,這是相同的函數,計算過程不同。答案來源:某國外公開課視頻上看到的,名字忘記了。


確實有兩種看上去很像的描述函數的方式,而且不完全一樣。

一種是描述計算過程來確定輸入輸出,進而確定函數;

一種是通過邏輯來定義函數這個二元關係,通常在數學中使用。

把它們分別稱作計算函數邏輯函數

一些邏輯函數並不能轉化為計算函數,最簡單的,實數域中的等值函數(f(x)=x),不能表示為計算函數,因為不是所有實數都可以在計算機中表達;

相應的,計算函數f x = f (x-1)也不能轉換為一個邏輯函數,由於沒有歸納基,顯然你不知道x對應哪個f x。

但是,這並不意味著數學不可以描述計算函數,程序不可以描述邏輯函數,只不過通常都是這麼用而已,最可怕的還是這兩種表達方式經常混用……另外,這兩種函數之間是有轉換方法的,具體參見遞歸論和domain theory。


不同意幾個高票答案關於計算機「函數」副作用的觀點,因為對於依賴成員變數的方法,應該將其視為一族函數,或者視為函數本身的超參數。所以,我的觀點是計算機的函數和數學上沒有不同,只不過是一些超參數是否需要顯式輸入和隱式輸入的問題。


利用百度,以我編程入門,高數看到門的水平,寫出了這些話。歡迎指出錯誤之處。

數學函數的經典定義是:

在某變化過程中有兩個變數x,y,按照某個對應法則,對於給定的x,有唯一確定的y與之對應,那麼y就叫做x的函數。其中x叫自變數,y叫因變數。

對於返回值不為空的計算機函數:

計算機的函數,是一個固定的一個程序段,或稱其為一個子程序,它在可以實現固定運算功能的同時,還帶有一個入口和一個出口,所謂的入口,就是函數所帶的各個參數,我們可以通過這個入口,把函數的參數值代入子程序,供計算機處理;所謂出口,就是指函數的函數值,在計算機求得之後,由此口帶回給調用它的程序。

這兩者是很相似的,都是由一些數據經過一些處理得到另一些數據。

而編程的函數,還有返回值為空的,這種函數則只是為了封裝一系列語句,用來執行某個操作,這就跟數學上的有所不同了。


目前計算機的能力等價於圖靈機,計算機是圖靈機的一種物理實現;

能夠被圖靈機表示的屬於可計算函數從名字上看自然存在不可計算函數

所以計算機中的函數從能力上看包含於數學中的函數;

從抽象層面看:計算機中的任何函數(任意的代碼段)都是函數

不同點:

計算機是圖靈機的一種物理實現;計算機語言可以看作是人與機器的介面;

這種介面連接了邏輯(數學中的函數) 和一個物理機器,所以同時擁有純邏輯和物理屬性兩方面性質

也就是說計算機中的函數比數學中的函數多一層物理屬性。


本質上沒有任何區別。

數學上的函數(映射)與程序中的函數都是一樣的系統:帶一個輸入介面和輸出介面的盒子系統。

這個系統,接收一個輸入值(數學函數中的自變數、程序中的參數;若輸入值為向量,則稱為多元函數),在盒子中進行處理(數學函數中的函數表達式、程序中的函數體),然後吐一個輸出值出來(數學函數中的因變數、程序中的返回值;若輸出值為常量,則稱為常值函數)。

在數學上,如果輸入值與輸出值都是實數,則是通常的(實)函數;若輸入值和輸出值都是複數,則稱其為複函數;若輸入值不是數,則稱為泛函;若輸入值和輸出值是集合,則稱其為集值函數。


推薦閱讀:

任意一條曲線是否可以找到方程或者函數來描繪?
若f(x)=-k f""(x), k>0, 那麼f(x)一定是正弦曲線嗎?
js自執行函數前加個分號是什麼意思?
什麼軟體能畫形如z=3x+4xy這種函數的圖像?
形狀如下圖的函數滿足f "(x) = f(2x),如何得到這個函數的具體形式?

TAG:編程語言 | 數學 | 編程 | 函數式編程 | 函數 |