函數式和面向對象編程有什么區別?

函數式編程 (Functional Programming) 和 面向對象編程 (Object Oriented Programming) 是兩個主流的編程范式,他們有各自獨特的閃光點,比如函數式編程的數據不可變惰性求值,面向對象編程的繼承多態等。這些語言特性上的區別,可以參考之前的文章,這篇文章主要從實現相同功能的角度,來對比這兩種編程范式,他們在實現上的邏輯是截然相反的

初步實現

在函數式編程中,代碼邏輯通常是按照要做什么。而在面向對象編程中,通常是把代碼邏輯抽象成 class,然后給這些 class 一些操作。這么說起來很抽象,用下面這個例子來詳細說明。

假設我們要用 函數式編程 和 面向對象編程 來分別實現下面這些功能:

eval toString hasZero
Int
Add
Negate

表格左列 Int, Add, Negate 是三個變式 (Variant),eval, toString, hasZero 是三種操作,這里要做的是填滿這個表格,分別實現三個變式的三種操作。

函數式編程實現

這里用 ML 來做函數式編程的實現,即使沒用過這門語言,應該也能讀懂大概意思。

datatype exp =
    Int    of int
  | Negate of exp
  | Add    of exp * exp

exception BadResult of string

fun add_values (v1,v2) =
    case (v1,v2) of
            (Int i, Int j) => Int (i+j)
      | _ => raise BadResult "non-values passed to add_values"

fun eval e =
    case e of
            Int _       => e
      | Negate e1   => (case eval e1 of
                          Int i => Int (~i)
      | _ => raise BadResult "non-int in negation")
      | Add(e1,e2)  => add_values (eval e1, eval e2)

fun toString e =
    case e of
        Int i           => Int.toString i
      | Negate e1   => "-(" ^ (toString e1) ^ ")"
      | Add(e1,e2)  => "("  ^ (toString e1) ^ " + " ^ (toString e2) ^ ")"

fun hasZero e =
    case e of
        Int i           => i=0
      | Negate e1   => hasZero e1
      | Add(e1,e2)  => (hasZero e1) orelse (hasZero e2)

在函數式編程中,先定義了一個數據類型 (datatype) 來表示 Int, Negate, Add,這樣定義的目的是什么呢?舉個表達式的例子:

  • Int 代表一個 int 的數據,比如 Int(2)
  • Negate 代表 Int 的負數,比如 Negate(Int(2)))
  • Add 代表兩個 Int 相加,比如 Add((Int(2), Int(3))

然后再分別實現三個操作 eval, toString, hasZero:

  • eval 是給一個表達式求值,比如給 Negate 求值,eval(Negate(Int(2))) = Int(-2) ,給 Add 求值,eval(Add(Int(2), Int(3))) = Int(5)
  • toString 是把這個表達式輸出成字符串,比如 toString(Add(Int(2), Int(3))) = "2 + 3"。
  • hasZero 是判斷表達式有沒有 0。

再看剛剛這句話函數式編程的代碼邏輯通常是按照要做什么,這里的主體是三個操作,eval, toString 和 hasZero,所以三個分別是一個函數,在函數里去實現三種變式怎么操作。

可以說,函數式編程式縱向的填滿了上面的表格。

面向對象編程

這里用 Ruby 來實現。

class Exp
end

class Value < Exp
end

class Int < Value
  attr_reader :i
  def initialize i
    @i = i
  end
  def eval # no argument because no environment
    self
  end
  def toString
    @i.to_s
  end
  def hasZero
    i==0
  end
end

class Negate < Exp
  attr_reader :e
  def initialize e
    @e = e
  end
  def eval
    Int.new(-e.eval.i) # error if e.eval has no i method
  end
  def toString
    "-(" + e.toString + ")"
  end
  def hasZero
    e.hasZero
  end
end

class Add < Exp
  attr_reader :e1, :e2
  def initialize(e1,e2)
    @e1 = e1
    @e2 = e2
  end
  def eval
    Int.new(e1.eval.i + e2.eval.i) # error if e1.eval or e2.eval has no i method
  end
  def toString
    "(" + e1.toString + " + " + e2.toString + ")"
  end
  def hasZero
    e1.hasZero || e2.hasZero
  end
end

< 在 Ruby 里是繼承的意思,class Int < Value 表示 Int 繼承了 Value,Int 是 Value 的 Subclass。

可以看到面向對象編程組織代碼的方式和之前的完全不一樣。這里把 Int, Negate, Add 抽象成了三個 class,然后分別給每個 class 加上 eval, toString, hasZero 三個方法。這也是剛剛那句話的說法 面向對象編程把代碼邏輯抽象成 class,然后給這些 class 一些操作,這里的主體是 Int, Negate, Add 這三個 class。

可以說,面向對象編程是橫向的填滿了上的表格。

通過這個對比,可以知道 函數式編程 和 面向對象編程 是兩種相反的思維模式和實現方式。這兩種方式對代碼的擴展性有什么影響呢?

擴展實現

eval toString hasZero absolute
Int
Negate
Add
Multi

在上面那個例子的基礎上,我們再加一行一列,增加 Multi 這個變式,表示乘法,增加 absolute 這個操作,作用是求絕對值。這會怎么影響我們的代碼呢?

函數式編程

在函數式編程中,要增加一個操作 absolute 很簡單,只要添加一個新的函數,不用修改之前的代碼。但是要增加 Multi 比較麻煩,要修改之前的所有函數。

面向對象編程

和函數式編程相反的,在這里增加一個 Multi 簡單,只要添加一個新的 class,但是增加 absolute 這個操作就要在之前的每一個 class 做更改。

選擇用 函數式編程 還是 面向對象編程 的一個考量因素是以后將會如何擴展代碼,對之前代碼的更改越少,出錯的概率越小。

Binary Methods

前面的對比,操作都是在一個數據類型上進行的,這里進行最后一個對比,一個函數對多個數據類型進行操作時,函數式和面向對象分別怎么實現。

Int String Rational
Int
String
Rational

這里要實現的是一個 add_values(x, y) 的操作,把兩個數據相加,但是 x, y 可能是不同的類型的。

函數式編程

函數式編程的實現相對簡單:

datatype exp =
    Int    of int
  | String of string
  | Rational of real

fun add_values (v1,v2) =
    case (v1,v2) of
                (Int i,  Int j)         => Int (i+j)
      | (Int i,  String s)      => String(Int.toString i ^ s)
      | (Int i,  Rational(j,k)) => Rational(i*k+j,k)
      | (String s,  Int i)      => String(s ^ Int.toString i) (* not commutative *)
      | (String s1, String s2)  => String(s1 ^ s2)
      | (String s,  Rational(i,j)) => String(s ^ Int.toString i ^ "/" ^ Int.toString j)
      | (Rational _, Int _)        => add_values(v2,v1)
      | (Rational(i,j), String s)  => String(Int.toString i ^ "/" ^ Int.toString j ^ s)
      | (Rational(a,b), Rational(c,d)) => Rational(a*d+b*c,b*d)
      | _ => raise BadResult "non-values passed to add_values"

這里的操作是 add_values,所以只要把所有可能的數據類型(總共9種)都列出來,就可以了。

面向對象編程:二次分派

按照上面面向對象編程的例子,我們可以這么做:

class Int < Value
    ...
  def add_values v
    if v.is_a? Int
      i + v.i
    elsif v.is_a? MyString
      i.to_s + v.i
    else
      ...
    end
  end
end

class MyString < Value
  ...
end

在 add_values 這個方法里面去做判斷,看傳入參數的類型,去做相應的操作。這種做法不是那么的 面向對象,可以有另外一種寫法:

class Int < Value
    ...
  # double-dispatch for adding values
  def add_values v # first dispatch
    v.addInt self
  end
  def addInt v # second dispatch: other is Int
    Int.new(v.i + i)
  end
  def addString v # second dispatch: other is MyString (notice order flipped)
    MyString.new(v.s + i.to_s)
  end
  def addRational v # second dispatch: other is MyRational
    MyRational.new(v.i+v.j*i,v.j)
  end
end

class MyString < Value
  ...
  # double-dispatch for adding values
  def add_values v # first dispatch
    v.addString self
  end
  def addInt v # second dispatch: other is Int (notice order is flipped)
    MyString.new(v.i.to_s + s)
  end
  def addString v # second dispatch: other is MyString (notice order flipped)
    MyString.new(v.s + s)
  end
  def addRational v # second dispatch: other is MyRational (notice order flipped)
    MyString.new(v.i.to_s + "/" + v.j.to_s + s)
  end
end
...

這里涉及到了一個概念 二次分派 (Double Dispatch),在一次方法的調用過程中,做了兩次 動態分派 (Dynamic Dispatch) 。用例子來說明

i = Int.new(1)
s = MyString.new("string")
i.add_values(s)

i.add_values(s)在調用這個方法時,實現了一次 dispatch,到 add_values 這個方法里后,做的其實是 s.addInt i,也就是去調用了 MyString 里的 addInt 這個方法,這是第二次 dispatch,所以叫做 double dispatch。

總結

函數式編程 和 面向對象編程 對比下來,我們并不能說哪一種模式更好。但是可以看出它們在思維上是截然不同的。函數式編程中側重要做什么,面向對象編程側重對象的抽象化,在有些編程語言里,比如 Java,是都可以實現的,但是要用哪種還要根據需求具體考慮。如果要了解更多 函數式編程 和 面向對象編程 的基礎概念的話,可以看看之前的這三篇文章。

推薦閱讀:
編程語言的一些基礎概念(一):靜態函數式編程
編程語言的一些基礎概念(二):動態函數式編程
編程語言的一些基礎概念(三):面向對象

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容