編程範式與函數式編程

編程範式與函數式編程

1、編程範式的分類

常見的編程範式有:函數式編程、程序編程、面向對象編程、指令式編程等。在面向對象編程的世界,程序是一系列相互做用(方法)的對象(Class Instances),而在函數式編程的世界,程序會是一個無狀態的函數組合序列。不一樣的編程語言也會提倡不一樣的「編程範型」。一些語言是專門爲某個特定的範型設計的,如Smalltalk和Java支持面向對象編程。而Haskell和Scheme則支持函數式編程。現代編程語言的發展趨勢是支持多種範型,如 C#、Java 8+、Kotlin、 Scala、ES6+ 等等。正則表達式

與成百種編程語言相比,編程範式要少得多。多數範式之間僅相差一個或幾個概念,好比函數編程範式,在加入了狀態(state)以後就變成了面向對象編程範式。算法

雖然範式有不少種,可是能夠簡單分爲三類:
圖片描述數據庫

圖片描述

1.命令式編程(Imperative programming)

計算機的硬件負責運行使用命令式的風格來寫的機器碼。計算機硬件的工做方式基本上都是命令式的。大部分的編程語言都是基於命令式的。高級語言一般都支持四種基本的語句:express

(1)運算語句
通常來講都表現了在存儲器內的數據進行運算的行爲,而後將結果存入存儲器中以便往後使用。高階命令式編程語言更能處理複雜的表達式,產生四則運算和函數計算的結合。編程

(2)循環語句
允許一些語句反覆運行數次。循環可依據一個默認的數目來決定運行這些語句的次數;或反覆運行它們,直至某些條件改變。設計模式

(3)條件分支
允許僅當某些條件成立時才運行某個區塊。不然,這個區塊中的語句會略去,而後按區塊後的語句繼續運行。數組

(4)無條件分支
允許運行順序轉移到程序的其餘部分之中。包括跳躍(在不少語言中稱爲Goto)、副程序和Procedure等。網絡

循環、條件分支和無條件分支都是控制流程。
早期的命令式編程語言,例如彙編,都是機器指令。雖然硬件的運行更容易,卻阻礙了複雜程序的設計。session

2.面向對象編程(元編程)(Object-oriented programming,OOP)

怎樣爲一個模糊不清的問題找到一個最恰當的描述(問題描述)? 抽象(Abstraction)一般是咱們用來簡化複雜的現實問題的方法。在面向對象程序編程裏,計算機程序會被設計成彼此相關的對象。對象則指的是類的實例。它將對象做爲程序的基本單元,將程序和數據封裝其中,以提升軟件的重用性、靈活性和擴展性,對象裏的程序能夠訪問及常常修改對象相關連的數據。對象包含數據(字段、屬性)與方法。數據結構

面向對象程序設計能夠看做一種在程序中包含各類獨立而又互相調用的對象的思想,這與傳統的思想恰好相反:傳統的程序設計主張將程序看做一系列函數的集合,或者直接就是一系列對計算機下達的指令。面向對象程序設計中的每個對象都應該可以接受數據、處理數據並將數據傳達給其它對象,所以它們均可以被看做一個小型的「機器」,即對象。目前已經被證明的是,面向對象程序設計推廣了程序的靈活性和可維護性,而且在大型項目設計中廣爲應用。此外,支持者聲稱面向對象程序設計要比以往的作法更加便於學習,由於它可以讓人們更簡單地設計並維護程序,使得程序更加便於分析、設計、理解。反對者在某些領域對此予以否定。

當咱們提到面向對象的時候,它不只指一種程序設計方法。它更多意義上是一種程序開發方式。在這一方面,咱們必須瞭解更多關於面向對象系統分析和麪向對象設計(Object Oriented Design,簡稱OOD)方面的知識。許多流行的編程語言是面向對象的,它們的風格就是會透由對象來創出實例。重要的面向對象編程語言包含Common Lisp、Python、C++、Objective-C、Smalltalk、Delphi、Java、Swift、C#、Perl、Ruby 與 PHP等。面向對象編程中,一般利用繼承父類,以實現代碼重用和可擴展性。

3.聲明式編程(Declarative programming)

一種編程範式,與命令式編程相對立。它描述目標的性質,讓計算機明白目標,而非具體過程。聲明式編程不用告訴計算機問題領域,從而避免隨之而來的反作用。而命令式編程則須要用算法來明確的指出每一步該怎麼作。聲明式編程一般被看作是形式邏輯的理論,把計算看作推導。聲明式編程大幅簡化了並行計算的編寫難度。

常見的聲明式編程語言有:
數據庫查詢語言(SQL,XQuery)
正則表達式
邏輯編程
函數式編程
組態管理系統等。

聲明式編程透過函數、推論規則或項重寫(term-rewriting)規則,來描述變量之間的關係。它的語言運行器(編譯器或解釋器)採用了一個固定的算法,以從這些關係產生結果。不少文本標記語言例如HTML、MXML、XAML和XSLT每每是聲明式的。函數式編程,特別是純函數式編程,嘗試最小化狀態帶來的反作用,所以被認爲是聲明式的。不過,大多數函數式編程語言,例如Scheme、Clojure、Haskell、OCaml、Standard ML和Unlambda,容許反作用的存在。

2、3種最經常使用編程範式的特色

  1. 過程式編程的核心在於模塊化,在實現過程當中使用了狀態,依賴了外部變量,致使很容易影響附近的代碼,可讀性較少,後期的維護成本也較高。
  2. 函數式編程的核心在於「避免反作用」,不改變也不依賴當前函數外的數據。結合不可變數據、函數是第一等公民等特性,使函數帶有自描述性,可讀性較高。
  3. 面向對象編程的核心在於抽象,提供清晰的對象邊界。結合封裝、集成、多態特性,下降了代碼的耦合度,提高了系統的可維護性。

不一樣的範式的出現,目的就是爲了應對不一樣的場景,但最終的目標都是提升生產力。

一、過程式編程(Procedural)

過程式編程和麪向對象編程的區別並不在因而否使用函數或者類,也就是說用到類或對象的多是過程式編程,只用函數而沒有類的也多是面向對象編程。那麼他們的區別又在哪兒呢?

面向過程實際上是最爲實際的一種思考方式,能夠說面向過程是一種基礎的方法,它考慮的是實際地實現。通常的面向過程是從上往下步步求精,因此面向過程最重要的是模塊化的思想方法。當程序規模不是很大時,面向過程的方法還會體現出一種優點。由於程序的流程很清楚,按着模塊與函數的方法能夠很好的組織。

二、函數式編程(Functional)

當談論函數式編程,會提到很是多的「函數式」特性。提到不可變數據,第一類對象以及尾調用優化,這些是幫助函數式編程的語言特徵。提到mapping(映射),reducing(概括),piplining(管道),recursing(遞歸),currying(科裏化),以及高階函數的使用,這些是用來寫函數式代碼的編程技術。提到並行,惰性計算以及肯定性,這些是有利於函數式編程的屬性。

最主要的原則是避免反作用,它不會依賴也不會改變當前函數之外的數據。

聲明式的函數,讓開發者只須要表達 「想要作什麼」,而不須要表達 「怎麼去作」,這樣就極大地簡化了開發者的工做。至於具體 「怎麼去作」,讓專門的任務協調框架去實現,這個框架能夠靈活地分配工做給不一樣的核、不一樣的計算機,而開發者沒必要關心框架背後發生了什麼。

三、面向對象編程(Object-Oriented)

並非使用類纔是面向對象編程。若是你專一於狀態改變和密封抽象,你就是在用面向對象編程。類只是幫助簡化面向對象編程的工具,並非面向對象編程的要求或指示器。封裝是一個過程,它分隔構成抽象的結構和行爲的元素。封裝的做用是分離抽象的概念接口及其實現。類只是幫助簡化面向對象編程的工具,並非面向對象編程的要求或指示器。

隨着系統愈來愈複雜,系統就會變得愈來愈容易崩潰,分而治之,解決複雜性的技巧。面對對象思想的產生是爲了讓你能更方便的理解代碼。有了那些封裝,多態,繼承,能讓你專一於部分功能,而不須要了解全局。

總結

圖片描述

命令式編程、面向對象編程、函數式編程,雖然受人追捧的時間點各不相同,可是本質上並無優劣之分。 面向對象和函數式、過程式編程也不是完成獨立和有嚴格的界限,在抽象出各個獨立的對象後,每一個對象的具體行爲實現仍是有函數式和過程式完成。

3、重點說一下函數式編程

1.函數式編程中的一些基本概念

(1)函數
函數式編程中的函數,這個術語不是指命令式編程中的函數(咱們能夠認爲C++程序中的函數本質是一段子程序Subroutine),而是指數學中的函數,即自變量的映射(一種東西和另外一種東西之間的對應關係)。也就是說,一個函數的值僅決定於函數參數的值,不依賴其餘狀態。

在函數式語言中,函數被稱爲一等函數(First-class function),與其餘數據類型同樣,做爲一等公民,處於平等地位,能夠在任何地方定義,在函數內或函數外;能夠賦值給其餘變量;能夠做爲參數,傳入另外一個函數,或者做爲別的函數的返回值。

(2)純函數
純函數是這樣一種函數,即相同的輸入,永遠會獲得相同的輸出,並且沒有任何可觀察的反作用。不依賴外部狀態,不改變外部狀態。

好比Javascript裏slice和splice,這兩個函數的做用類似。 slice符合純函數的定義是由於對相同的輸入它保證能返回相同的輸出。splice卻會嚼爛調用它的那個數組,而後再吐出來;這就會產生可觀察到的反作用,即這個數組永久地改變了。

var xs = [1,2,3,4,5];

// 純的
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]


// 不純的
xs.splice(0,3);
//=> [1,2,3]
xs.splice(0,3);
//=> [4,5]
xs.splice(0,3);
//=> []

(3)變量與表達式

純函數式編程語言中的變量也不是命令式編程語言中的變量(存儲狀態的內存單元),而是數學代數中的變量,即一個值的名稱。變量的值是不可變的(immutable),也就是說不容許像命令式編程語言中那樣可以屢次給一個變量賦值。好比說在命令式編程語言咱們寫x = x + 1。函數式語言中的條件語句,循環語句等也不是命令式編程語言中的控制語句,而是一種表達式。

「表達式」(expression)是一個單純的運算過程,老是有返回值;
「語句」(statement)是執行某種操做(更多的是邏輯語句。),沒有返回值。

函數式編程要求,只使用表達式,不使用語句。也就是說,每一步都是單純的運算,並且都有返回值。好比在Scala語言中,if else不是語句而是三元運算符,是有返回值的。嚴格意義上的函數式編程意味着不使用可變的變量,賦值,循環和其餘命令式控制結構進行編程。 固然,不少所謂的函數式編程語言並無嚴格遵循這一類的準則,只有某些純函數式編程語言,如Haskell等纔是完徹底全地依照這種準則設計的。

(4)狀態

首先要意識到,咱們的程序是擁有「狀態」的。 想想咱們調試C++程序的時候,常常會在某處設置一個斷點。程序執行斷點就暫停了,也就是說程序停留在了某一個狀態上。這個狀態包括了當前定義的所有變量,以及一些當前系統的狀態,好比打開的文件、網絡的鏈接、申請的內存等等。具體保存的信息和語言有關係。好比使用過Matlab、R之類的科學計算語言的朋友會知道,在退出程序的時候它會讓你選擇是否要保存當前的session,若是保存了,下次打開時候它會從這個session開始繼續執行,而不是清空一切重來。你以前定義了一個變量x = 1,如今這個x還在那裏,值也沒變。這個狀態就是圖靈機的紙帶。有了狀態,咱們的程序才能不斷往前推動,一步步向目標靠攏的。函數式編程不同。函數式編程強調無狀態,不是不保存狀態,而是強調將狀態鎖定在函數的內部,不依賴於外部的任何狀態。更準確一點,它是經過函數建立新的參數或返回值來保存程序的狀態的。
狀態徹底存在的棧上。

(5)函數柯里化
curry 的概念很簡單:將一個低階函數轉換爲高階函數的過程就叫柯里化。

// 柯里化以前
function add(x, y) {
    return x + y;
}
add(1, 2) // 3
// 柯里化以後
function addX(y) {
    return function (x) {
        return x + y;
    };
}
addX(2)(1) // 3

(6)函數組合(Pointfree:不使用所要處理的值,只合成運算過程)

爲了解決函數嵌套過深,洋蔥代碼:h(g(f(x))),咱們須要用到「函數組合」,咱們一塊兒來用柯里化來改他,讓多個函數像拼積木同樣。

const compose = (f, g) => (x => f(g(x)));
var first = arr => arr[0];
var reverse = arr = arr.reverse();
var last = compose(first, reverse);
last([1, 2, 3, 4, 5]); // 5

圖片描述

(7)聲明式與命令式代碼

在咱們平常業務開發中,寫的代碼絕大多數都爲命令式代碼;
咱們經過編寫一條又一條指令去讓計算機執行一些動做,這其中通常都會涉及到不少繁雜的細節。
而聲明式就要優雅不少了,咱們經過寫表達式的方式來聲明咱們想幹什麼,而不是經過一步一步的指示。

//命令式
let CEOs = [];
for (var i = 0; i < companies.length; i++) {
    CEOs.push(companies[i].CEO)
}
//聲明式
let CEOs = companies.map(c => c.CEO);

2.函數式編程的特性

(1)高階函數
高階函數就是參數爲函數或返回值爲函數的函數。有了高階函數,就能夠將複用的粒度下降到函數級別。相對於面嚮對象語言,高階函數的複用粒度更低。高階函數提供了一種函數級別上的依賴注入(或反轉控制)機制,在上面的例子裏,sum函數的邏輯依賴於注入進來的函數的邏輯。不少GoF設計模式均可以用高階函數來實現,如Visitor,Strategy,Decorator等。好比Visitor模式就能夠用集合類的map()或foreach()高階函數來替代。

(2)閉包
閉包的基礎是一等函數(First-class function)。閉包在形式上就是一個函數內部定義另外一個函數,函數的堆棧在在函數返回後並不釋放,咱們也能夠理解爲這些函數堆棧並不在棧上分配而是在堆上分配。

(3)訪問權限控制
js中的做用域

(4)延長變量生命週期
在面嚮對象語言裏,函數內的變量都是在棧上分配的,函數調用完成後,棧銷燬,變量的生命週期結束。而對象是在堆分配的,會常駐內存,除非被手動或自動回收掉。

(5)函子
Functor(函子)遵照一些特定規則的容器類型。任何具備map方法的數據結構,均可以看成函子的實現。
Functor 是一個對於函數調用的抽象,咱們賦予容器本身去調用函數的能力。把東西裝進一個容器,只留出一個接口 map 給容器外的函數,map 一個函數時,咱們讓容器本身來運行這個函數,這樣容器就能夠自由地選擇什麼時候何地如何操做這個函數。

3.函數式編程的好處

因爲命令式編程語言也能夠經過相似函數指針的方式來實現高階函數,函數式的最主要的好處主要是不變性帶來的。

(1)引用透明(Referential transparency)
引用透明(Referential transparency),指的是函數的運行不依賴於外部變量或」狀態」,只依賴於輸入的參數,任什麼時候候只要參數相同,引用函數所獲得的返回值老是相同的。其餘類型的語言,函數的返回值每每與系統狀態有關,不一樣的狀態之下,返回值是不同的。這就叫」引用不透明」,很不利於觀察和理解程序的行爲。
沒有可變的狀態,函數就是引用透明(Referential transparency)

(2)沒有反作用(No Side Effect)。(Lodash.js)
反作用(side effect),指的是函數內部與外部互動(最典型的狀況,就是修改全局變量的值),產生運算之外的其餘結果。函數式編程強調沒有」反作用」,意味着函數要保持獨立,全部功能就是返回一個新的值,沒有其餘行爲,尤爲是不得修改外部變量的值。函數即不依賴外部的狀態也不修改外部的狀態,函數調用的結果不依賴調用的時間和位置,這樣寫的代碼容易進行推理,不容易出錯。這使得單元測試和調試都更容易。
還有一個好處是,因爲函數式語言是面向數學的抽象,更接近人的語言,而不是機器語言,代碼會比較簡潔,也更容易被理解。

(3)無鎖併發
沒有反作用使得函數式編程各個獨立的部分的執行順序能夠隨意打亂,(多個線程之間)不共享狀態,不會形成資源爭用(Race condition),也就不須要用鎖來保護可變狀態,也就不會出現死鎖,這樣能夠更好地進行無鎖(lock-free)的併發操做。尤爲是在對稱多處理器(SMP)架構下可以更好地利用多個處理器(核)提供的並行處理能力。

(4)惰性求值惰性求值(lazy evaluation,也稱做call-by-need)是這樣一種技術:是在將表達式賦值給變量(或稱做綁定)時並不計算表達式的值,而在變量第一次被使用時才進行計算。這樣就能夠經過避免沒必要要的求值提高性能。(簡單例子&&)

相關文章
相關標籤/搜索