首頁 > theory > model > > 正文

再談編程范式—程序語言背后的思想

點擊:

編程范式是程序語言背后的思想。代表了程序設計者認為程序應該如何被構建和執行的看法。常見的編程范式有:命令式、過程式、說明式、面向對象、函數式等。本文這些進行講解,并總結其優劣。


編程范式

托馬斯.庫爾提出“科學的革命”的范式論后,Robert Floyd在1979年圖靈獎的頒獎演說中使用了編程范式一詞。編程范式一般包括三個方面,以OOP為例:

  1,學科的邏輯體系——規則范式:如 類/對象、繼承、動態綁定、方法改寫、對象替換等等機制。

  2,心理認知因素——心理范式:按照面向對象編程之父Alan Kay的觀點,“計算就是模擬”。OO范式極其重視隱喻(metaphor)的價值,通過擬人化,按照自然的方式模擬自然。

  3,自然觀/世界觀——觀念范式:強調程序的組織技術,視程序為松散耦合的對象/類的組合,以繼承機制將類組織成一個層次結構,把程序運行視為相互服務的對象之間的對話。

簡單來說,編程范式是程序員看待程序應該具有的觀點代表了程序設計者認為程序應該如何被構建和執行的看法

常見的編程范式有:命令式、過程式、說明式、面向對象、函數式、泛型編程等。

Imperative—命令式||過程式

  馮諾依曼 機器語言、匯編語言 BASIC COBOL C Ada FORTRAN Fortran ,

  腳本式 Perl Python PHP,把用其他語言開發的獨立程序作為部件“粘到一起”

  面向對象 Smalltalk C++ Java,將計算建立在獨立的對象的相互作用至上。每個對象有其自身的內部狀態,以及管理自身狀態的可執行子程序

Declarative—說明式||聲明式

  函數式  Lisp ML Haskell ,程序被看作是一種從輸入到輸出的函數

  數據流  ld Val,語言將計算看成在一些基本的功能結點之間流動的信息流。結點由輸入單詞的到達觸發,能夠并發操作

  邏輯式  Prolog,設法根據一集邏輯規則找出滿足某些特定關系的值

  基于模板的 XSLT xml html,

需要提醒的是:編程范式是編程語言的一種分類方式,它并不針對某種編程語言。就編程語言而言,一種語言可以適用多種編程范式


一些編程語言是專門為某種特定范式設計的,例如C語言是過程式編程語言;Smalltalk和Java是較純粹的面向對象編程語言;Haskell是純粹的函數式編程語言。另外一些編程語言和編程范式的關系并不一一對應,如Python,Scala,Groovy都支持面向對象和一定程度上的函數式編程。C++是多范式編程語言成功的典范。C++支持和C語言一樣的過程式編程范式,同時也支持面向對象編程范式,STL(Standard Template Library)使C++具有了泛型編程能力。支持多種范式可能是C++直到現在仍然具有強大的生命力的原因之一。

Swift是一門典型的多范式編程語言,即支持面向對象編程范式,也支持函數式編程范式,同時還支持泛型編程。Swift支持多種編程范式是由其創造目標決定的。Swift創造的初衷就是提供一門實用的工業語言。不同于Haskell這類出自大學和研究機構的學術性質的編程語言。蘋果推出Swift時就帶著著明確的商業目的:Mac OS和iOS系統的主要編程語言Objective-C已顯老態,Swift將使得蘋果系統的開發者擁有一門更現代的編程語言,從而促進蘋果整個生態圈的良性發展。

命令式編程:

命令式編程的主要思想是關注計算機執行的步驟,即一步一步告訴計算機先做什么再做什么

從本質上講,它是“馮.諾依曼機”運行機制的抽象,它的編程思想方式源于計算機指令的順序排列

(也就是說:過程化語言模擬的是計算機機器的系統構造,而并不是基于語言的使用者的個人能力和傾向。這一點我們應該都很清楚,比如我們最早曾經使用過的單片機的匯編語言。)

不管你用的是 C, C++ 還是 C#, Java, Javascript, BASIC, Python, Ruby 等等,你都可以以這個方式寫。

程序流程圖是命令式語言進行程序編寫的有效輔助手段

命令式語言特別適合解決線性(或者說按部就班)的算法問題。它強調“自上而下(自頂向下)”“精益求精”的設計方式。這種方式非常類似我們的工作和生活方式,因為我們的日常活動都是按部就班的順序進行的。 

命令式語言趨向于開發運行較快且對系統資源利用率較高的程序。命令式語言非常的靈活并強大,同時有許多經典應用范例,這使得程序員可以用它來解決多種問題。 

命令式語言的不足之處就是它不適合某些種類問題的解決,例如那些非結構化的具有復雜算法的問題。問題出現在,命令式語言必須對一個算法加以詳盡的說明,并且其中還要包括執行這些指令或語句的順序。實際上,給那些非結構化的具有復雜算法的問題給出詳盡的算法是極其困難的。 

廣泛引起爭議和討論的地方是:無條件分支,或goto語句,它是大多數過程式編程語言的組成部分,反對者聲稱:goto語句可能被無限地濫用;它給程序設計提供了制造混 亂的機會。目前達成的共識是將它保留在大多數語言中,對于它所具有的危險性,應該通過程序設計的規定將其最小化。 

命令式對實際事物處理一般可以拆分為以下兩種模式:

  • 流程驅動:類似 一般就是主動輪詢 在干活中還要分心 主動去找活干  這樣有空余的時間也完全浪費掉了

    采用警覺式者主動去輪詢 ( polling),行為取決于自身的觀察判斷,是流程驅動的,符合常規的流程驅動式編程 ( Flow-Driven Programming)的模式。

  • 事件驅動:類似  比如公司有一個oa系統 你干完活的時候只需要看下oa系統有沒分配給你活 沒有可以干自己的事  不用擔心還有其他事沒干完

    采用托付式者被動等通知 (notification),行為取決于外來的突發事件,是事件驅動 的,符合事件驅動式編程 ( Event-Driven Programming,簡稱 EDP)的模式。


事件驅動編程

其實,基于事件驅動的程序設計在圖形用戶界面(GUI)出現很久前就已經被應用于程序設計中,可是只有當圖形用戶界面廣泛流行時,它才逐漸形演變為一種廣泛使用的程序設計模式。 

在過程式的程序設計中,代碼本身就給出了程序執行的順序,盡管執行順序可能會受到程序輸入數據的影響。

在事件驅動的程序設計中,程序中的許多部分可能在完全不可預料的時刻被執行。往往這些程序的執行是由用戶與正在執行的程序的互動激發所致。 

  • 事件:就是通知某個特定的事情已經發生(事件發生具有隨機性)。 

  • 事件與輪詢:輪詢的行為是不斷地觀察和判斷,是一種無休止的行為方式。而事件是靜靜地等待事情的發生。事實上,在Windows出現之前,采用鼠標輸入字符模式的PC應用程序必須進行串行輪詢,并以這種方式來查詢和響應不同的用戶操做。 

  • 事件處理器:是對事件做出響應時所執行的一段程序代碼。事件處理器使得程序能夠對于用戶的行為做出反映。 

事件驅動常常用于用戶與程序的交互,通過圖形用戶接口(鼠標、鍵盤、觸摸板)進行交互式的互動。當然,也可以用于異常的處理和響應用戶自定義的事件等等。

事件的異常處理比用戶交互更復雜。 

事件驅動不僅僅局限在GUI編程應用。但是實現事件驅動我們還需要考慮更多的實際問題,如:事件定義事件觸發、事件轉化事件合并事件排隊事件分派事件處理事件連帶等等。

其實,到目前為止,我們還沒有找到有關純事件驅動編程的語言和類似的開發環境。所有關于事件驅動的資料都是基于GUI事件的。 

屬于事件驅動的編程語言有:VB、C#、Java(Java Swing的GUI)等。它們所涉及的事件絕大多數都是GUI事件。 

此種程化范式要求程序員用按部就班的算法看待每個問題。很顯然,并不是每個問題都適合這種過程化的思維方式。這也就導致了其它程序設計范式出現,包括我們現在介紹的面向對象的程序設計范式。 


從編程的發展史來談面向對象的出現。當軟件還非常簡單的時候,我們只需要面向過程編程

定義函數

函數一 函數二 函數三 函數四

定義數據

數據一 數據二 數據三 數據四

最后各種函數,數據的操作。

當軟件發展起來后,我們的軟件變得越來越大,代碼量越來越多,復雜度遠超Hello World的時候,我們的編寫就有麻煩了:函數和數據會定義得非常多,面臨兩個問題。首先是命名沖突,英文單詞也就那么幾個,可能寫著寫著取名時就沒合適的短詞用了,為了避免沖突,只能把函數名取得越來越長。然后是代碼重復,我們可以用函數里面調用函數的方法,但是函數調函數(比如一個功能多個方法(函數),幾個功能混用方法)不便于維護。


面向對象程序

面向對象程序設計(Object-oriented programming OOP)是種通過類、方法、對象和消息傳遞,來支持面向對象的程序設計范式對象則指的是類的實例。它將對象作為程序的基本單元,將程序和數據封裝其中,以提高軟件的重用性、靈活性和擴展性,對象里的程序可以訪問及經常修改對象相關連的數據。在面向對象程序編程里,程序會被設計成彼此相關的對象

面向對象程序設計可以看作一種在程序中包含各種獨立而又互相調用的對象的思想,這與傳統的思想剛好相反:傳統的程序設計主張將程序看作一系列函數的集合,或者直接就是一系列對計算機下達的指令。面向對象程序設計中的每一個對象都應該能夠接受數據、處理數據并將數據傳達給其它對象,因此它們都可以被看作一個小型的“機器”,即對象。即把事情交給最適合的對象去做

面向對象和面向過程的區別最直觀的比喻就如:搖(狗尾巴) 狗.搖尾巴()的區別。

面向對象編程的三個基本概念:

  • 封裝,面向對象程序設計隱藏了某一方法的具體執行步驟,取而代之的是通過消息傳遞機制傳送消息給它。經過深入的思考,做出良好的抽象,給出“完整且最小”的接口,并使得內部細節可以對外隱藏

  • 繼承,在某種情況下,一個類會有“子類”。子類比原本的類(稱為父類)要更加具體化;

  • 多態,指由繼承而產生的相關的不同的類,其對象對同一消息會做出不同的響應;

使用面向對象編程語言,易于構建軟件模型。因為,對象很類似乎很容易和現實世界上的所有事物和概念。

面向對象通過接口

  • 類,類是相似對象的集合。物以類聚——就是說明。每個對象都是其類中的一個實體。類中的對象可以接受相同的消息。換句話說:類包含和描述了“具有共同特性(數據元素)和共同行為(功能)”的一組對象。

  • 接口,每個對象都有接口。接口不是類,而是對符合接口需求的類所作的一套規范。接口說明類應該做什么但不指定如何作的方法。一個類可以有一個或多個接口。 

  • 方法,方法決定了某個對象究竟能夠接受什么樣的消息。面向對象的設計有時也會簡單地歸納為“將消息發送給對象”。 


面向對象技術一方面借鑒了哲學、心理學、生物學的思考方式,另一方面,它是建立在其他編程技術之上的,是以前的編程思想的自然產物。

如果說結構化軟件設計是將函數式編程技術應用到命令式語言中進行程序設計,面向對象編程不過是將函數式模型應用到命令式程序中的另一途徑,此時,模塊進步為對象,過程龜縮到class的成員方法中。OOP的很多技術——抽象數據類型、信息隱藏、接口與實現分離、對象生成功能、消息傳遞機制等等,很多東西就是結構化軟件設計所擁有的、或者在其他編程語言中單獨出現。但只有在面向對象語言中,他們才共同出現,以一種獨特的合作方式互相協作、互相補充。


從上面可以看到,如果按照面向過程的方法去設計汽車,汽車廠商需要采購一大堆零件,然后研究如何調試、調用這一大堆零件以完成一個功能。但是如果采用面向對象的方法去設計汽車,那么汽車廠商可以采用外包的方式交給專業的制動系統廠商來設計,只需要約定需要開放哪些public方法,輸入什么輸出什么就可以了。


在知乎kevin zou總結的三種面向對象方式:

靜態函數包對象

將功能有聯系的一批函數放在一起封裝成一個類。這種類可以完全沒有內部數據,也可以有數據。當有數據時,這些數據充當的其實就是配置(配置對于一個設計優秀的對象,是透明的,對象本身內部的函數根本不知道有配置這個東西,它只知道它需要的每一個數據在它new之后就已經存在this里了,隨取隨用。配置的給予或獲取方式,是構建對象(new)時才需要去考慮的)這種對象的特點是,它的每一個函數(或方法)對這些數據都是只讀的,所以不管方法有無被調用,被誰調用,被調用多少次,它也不會改變它的狀態。

領域模型對象

這個概念是相對于傳統的面向數據庫的系統分析和設計而言的。數據庫雖然只用了外鍵就描述了復雜的大千世界,但軟件開發的難點在于適應變化,并且能夠安全地修改。關系模型看似簡單,但它卻像一張蜘蛛網一樣將所有table和欄位包在一塊,牽一發而動全身,讓你在修改時如履薄冰,一不小心就會顧此失彼,bug此起彼伏。而OO的封裝特性則剛好可以用來解決這個問題。將業務數據整理成一個個獨立的對象,讓它們的數據只能被自己訪問。留給外界的基本上只是一些接口(方法),數據除非萬不得已,一個都不會公開。外界只能向它發送消息,它自己則通過修改自身數據來響應這種消息。這種對象與第一種對象剛好相反,它一定有數據,而且它的每一個函數存在的目的就是修改自己的數據。且每一次修改都是粗粒度的,每一次修改后,對象也還是處在valid狀態。推薦閱讀《領域模型淺析》,《領域模型,你真的理解的了嗎?

順便拓展下:領域驅動設計(Domain-Driven Design)-貧血模型-領域模型-充血模型

臨時對象

其它用來解決過程式開發時,超多的變量,超復雜的流程而整理出來的小對象,。這些對象一起協作,最后完成一個傳統成千上萬行的過程式代碼才能完成的功能。例如現在要連接sql server執行查詢語句并取得結果返回。不使用任何類庫和工具,所有步驟都自己進行,例如解析協議,socket網絡連接,數據包收發等。這時候從頭到尾用一個個函數來完成,絕對沒有先劃分出一個個職責分明的對象,讓各對象協作完成這件事情來得更簡單。

 

但編程實踐表明,并不是任何東西成為對象都是一件好事情。舉一個Java中的蹩足的例子:Java中只有對象才能作為參數傳入函數(當然還有原始類型primitive type)。所以為了將函數傳遞給另外一個函數,你需要將函數包裹在一個對象中,通常會用一個匿名類,因為這個類不會有其他作用,只是為了讓Java的一切皆為對象的設計高興。

Java擁有純粹的面向對象概念。它從設計之初,就希望以一切皆為對象的純對象模型來為世界建模。但發展到現在,Java中加入了越來越多非對象的東西。引入了閉包,從而獲得了函數式編程中的一級函數;引入泛型,從而獲得了參數化的類型。這可能暗示了,這個世界是如此得豐富多彩,使用單一模式為世界建模并不會成功


聲明式編程:

聲明式編程是以數據結構的形式來表達程序執行的邏輯。它的主要思想是告訴計算機應該做什么,但不指定具體要怎么做

SQL 語句就是最明顯的一種聲明式編程的例子,例如:

SELECT * FROM collection WHERE num > 5

除了 SQL,網頁編程中用到的 HTML 和 CSS 也都屬于聲明式編程。

通過觀察聲明式編程的代碼我們可以發現它有一個特點是它不需要創建變量用來存儲數據

另一個特點是它不包含循環控制的代碼如 for, while

函數式編程和聲明式編程是有所關聯的,因為他們思想是一致的:即只關注做什么而不是怎么做。但函數式編程不僅僅局限于聲明式編程。

函數式編程

函數式編程(functional programming)或稱函數程序設計、泛函編程,是一種編程范式,它將計算機運算視為函數運算,并且避免使用程序狀態以及易變對象。其中,λ演算(lambda calculus)為該語言最重要的基礎。而且,λ演算的函數可以接受函數當作輸入(引數)和輸出(傳出值)。

函數式編程關心類型(代數結構)之間的關系,命令式編程關心解決問題的步驟。函數式編程中的lambda可以看成是兩個類型之間的關系,一個輸入類型和一個輸出類型。lambda演算就是給lambda表達式一個輸入類型的值,則可以得到一個輸出類型的值,這是一個計算,計算過程滿足  -等價和  -規約。函數式編程的思維就是如何將這個關系組合起來,用數學的構造主義將其構造出你設計的程序

比起命令式編程,函數式編程更加強調程序執行的結果而非執行的過程倡導利用若干簡單的執行單元讓計算結果不斷漸進,逐層推導復雜的運算,而不是設計一個復雜的執行過程


命令式編程是面向計算機硬件的抽象,有變量(對應著存儲單元),賦值語句(獲取,存儲指令),表達式(內存引用和算術運算)和控制語句(跳轉指令),一句話,命令式程序就是一個馮諾依曼機的指令序列。

函數式編程是面向數學的抽象,將計算描述為一種表達式求值,一句話,函數式程序就是一個表達式

函數式編程最重要的特點是“函數第一位”,即函數可以出現在任何地方,比如你可以把函數作為參數傳遞給另一個函數,不僅如此你還可以將函數作為返回值。

函數式編程的本質

函數式編程中的函數這個術語不是指計算機中的函數(實際上是Subroutine),而是指數學中的函數,即自變量的映射。也就是說一個函數的值僅決定于函數參數的值,不依賴其他狀態。比如sqrt(x)函數計算x的平方根,只要x不變,不論什么時候調用,調用幾次,值都是不變的。

在函數式語言中,函數作為一等公民,可以在任何地方定義,在函數內或函數外,可以作為函數的參數和返回值,可以對函數進行組合。

純函數式編程語言中的變量也不是命令式編程語言中的變量,即存儲狀態的單元,而是代數中的變量,即一個值的名稱變量的值是不可變的(immutable),也就是說不允許像命令式編程語言中那樣多次給一個變量賦值。比如說在命令式編程語言我們寫“x = x + 1”,這依賴可變狀態的事實,拿給程序員看說是對的,但拿給數學家看,卻被認為這個等式為假。

函數式語言的如條件語句,循環語句也不是命令式編程語言中的控制語句,而是函數的語法糖,比如在Scala語言中,if else不是語句而是三元運算符,是有返回值的。

嚴格意義上的函數式編程意味著不使用可變的變量,賦值,循環和其他命令式控制結構進行編程

從理論上說,函數式語言也不是通過馮諾伊曼體系結構的機器上運行的,而是通過λ演算來運行的,就是通過變量替換的方式進行,變量替換為其值或表達式,函數也替換為其表達式,并根據運算符進行計算。λ演算是圖靈完全(Turing completeness)的,但是大多數情況,函數式程序還是被編譯成(馮諾依曼機的)機器語言的指令執行的


函數式編程的特性

  • 函數是"一等公民":函數優先,和其他數據類型一樣。

  • 只用"表達式",不用"語句":通過表達式(expression)計算過程得到一個返回值,而不是通過一個語句(statement)修改某一個狀態。

  • 無副作用:不污染變量,同一個輸入永遠得到同一個數據。

  • 不可變性:前面一提到,不修改變量,返回一個新的值。

由于變量值是不可變的,對于值的操作并不是修改原來的值,而是修改新產生的值,原來的值保持不便。

通常來說,算法都有遞推(iterative)遞歸(recursive兩種定義。

由于變量不可變,純函數編程語言無法實現循環,這是因為For循環使用可變的狀態作為計數器,而While循環DoWhile循環需要可變的狀態作為跳出循環的條件。因此在函數式語言里就只能使用遞歸來解決迭代問題,這使得函數式編程嚴重依賴遞歸。

函數式語言當然還少不了以下特性:
  • 高階函數(Higher-order function):就是參數為函數或返回值為函數的函數。有了高階函數,就可以將復用的粒度降低到函數級別,相對于面向對象語言,復用的粒度更低。

  • 偏應用函數(Partially Applied Functions):一個函數接收一個有多個參數的函數,返回一個需要較少參數的函數。偏函數將一到多個參數在內部固定,然后返回新函數,返回的函數接收剩余的參數完成函數的應用。

  • 柯里化(Currying):輸入一個有多個參數的函數, 返回一個只接收單個參數的函數。

  • 閉包(Closure):閉包就是有權訪問另一個函數作用域中變量的函數.閉包的三個特性:1.閉包是定義在函數中的函數 。2.閉包能訪問包含函數的變量。3.即使包含函數執行完了, 被閉包引用的變量也得不到釋放。具體參看《閑話閉包


函數式編程的好處 

由于命令式編程語言也可以通過類似函數指針的方式來實現高階函數,函數式的最主要的好處主要是不可變性帶來的。沒有可變的狀態,函數就是引用透明(Referential transparency)的和沒有副作用(No Side Effect

函數即不依賴外部的狀態也不修改外部的狀態,函數調用的結果不依賴調用的時間和位置,這樣寫的代碼容易進行推理,不容易出錯。這使得單元測試和調試都更容易。

由于(多個線程之間)不共享狀態,不會造成資源爭用(Race condition),也就不需要用來保護可變狀態,也就不會出現死鎖,這樣可以更好地并發起來,尤其是在對稱多處理器(SMP)架構下能夠更好地利用多個處理器(核)提供的并行處理能力。

我覺得函數編程的好處就不用管js里面該死的this指向

函數式編程語言還提供惰性求值-Lazy evaluation,也稱作call-by-need,是在將表達式賦值給變量(或稱作綁定)時并不計算表達式的值,而在變量第一次被使用時才進行計算。這樣就可以通過避免不必要的求值提升性能。

函數式編程語言一般還提供強大的模式匹配(Pattern Match)功能。在函數式編程語言中可以定義代數數據類型(Algebraic data type),通過組合已有的數據類型形成新的數據類型,如在Scala中提供case class,代數數據類型的值可以通過模式匹配進行分析。

函數式編程天生親和單元測(特別是黑盒測試),因為FP關注就是輸入與輸出。反觀Java或者C++,僅僅檢查函數的返回值是不夠的:代碼可能修改外部狀態值,因此我們還需要驗證這些外部的狀態值的正確性。在FP語言中呢,就完全不需要。

調試查錯方面,因為FP程序中的錯誤不依賴于之前運行過的不相關的代碼。而在一個指令式程序中,一個bug可能有時能重現而有些時候又不能。因為這些函數的運行依賴于某些外部狀態, 而這些外部狀態又需要由某些與這個bug完全不相關的代碼通過某個特別的執行流程才能修改。在FP中這種情況完全不存在:如果一個函數的返回值出錯了,它一直都會出錯,無論你之前運行了什么代碼。而整個程序就是函數接龍。

推薦閱讀《傻瓜函數式編程

泛型編程

泛型編程是另外一個有趣的話題。泛型為程語言提供了更高層級的抽象,即參數化類型。換句話說,就是把一個原本特定于某個類型的算法或類當中的類型信息抽象出來。這個抽象出來的概念在C++的STL(Standard Template Library)中就是模版(Template)。STL展示了泛型編程的強大之處,一出現就成為了C++的強大武器。除C++之外,C#,Java,Haskell等編程語言都引入了泛型概念。


泛型編程是一個稍微局部一些的概念,它僅僅涉及如何更抽象地處理類型,即參數化類型。這并不足以支撐起一門語言的核心概念。我們不會聽到一個編程語言是純泛型編程的,而沒有其他編程范式。但正因為泛型并不會改變程序語言的核心,所以在大多數時候,它可以很好的融入到其他的編程方式中。C++,Scala,Haskell這些風格迥異的編程語言都支持泛型。泛型編程提供了更高的抽象層次,這意味著更強的表達能力。這對大部分編程語言來說都是一道美味佐餐美酒。


在Swift中,泛型得到廣泛使用,許多Swift標準庫是通過泛型代碼構建出來的。例如Swift的數組和字典類型都是泛型集。這樣的例子在Swift中隨處可見。


參考文章:

編程范式[ 程序員的編程世界觀 ]

漢諾塔——各種編程范式的解決 

編程范式:命令式編程(Imperative)、聲明式編程(Declarative)和函數式編程(Functional)

神奇的λ演算 https://www.cnblogs.com/dragonpig/archive/2010/01/26/1657052.html

編程語言范式 http://www.cnblogs.com/lisperl/archive/2011/11/20/2256165.html 

λ 演算學習 https://www.cnblogs.com/kirohuji/p/7080876.html

函數式編程漫談 https://cloud.tencent.com/developer/article/1190773

此文大多是本文給出的鏈接文字提煉總結,如果不妥之處,請到本站留言,告知,拜謝!