函數(shù)式和面向?qū)ο缶幊逃惺裁磪^(qū)別骚露?

函數(shù)式編程 (Functional Programming) 和 面向?qū)ο缶幊?(Object Oriented Programming) 是兩個(gè)主流的編程范式棘幸,他們有各自獨(dú)特的閃光點(diǎn),比如函數(shù)式編程的數(shù)據(jù)不可變吨悍、惰性求值蹋嵌,面向?qū)ο缶幊痰?strong>繼承爆雹、多態(tài)等。這些語言特性上的區(qū)別菇晃,可以參考之前的文章磺送,這篇文章主要從實(shí)現(xiàn)相同功能的角度,來對(duì)比這兩種編程范式馅袁,他們?cè)趯?shí)現(xiàn)上的邏輯是截然相反的荒辕。

初步實(shí)現(xiàn)

在函數(shù)式編程中抵窒,代碼邏輯通常是按照要做什么弛针。而在面向?qū)ο缶幊讨欣罨剩ǔJ前汛a邏輯抽象成 class削茁,然后給這些 class 一些操作。這么說起來很抽象茧跋,用下面這個(gè)例子來詳細(xì)說明。

假設(shè)我們要用 函數(shù)式編程 和 面向?qū)ο缶幊?來分別實(shí)現(xiàn)下面這些功能:

eval toString hasZero
Int
Add
Negate

表格左列 Int, Add, Negate 是三個(gè)變式 (Variant)厌衔,eval, toString, hasZero 是三種操作璧帝,這里要做的是填滿這個(gè)表格,分別實(shí)現(xiàn)三個(gè)變式的三種操作睬隶。

函數(shù)式編程實(shí)現(xiàn)

這里用 ML 來做函數(shù)式編程的實(shí)現(xiàn),即使沒用過這門語言,應(yīng)該也能讀懂大概意思变勇。

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)

在函數(shù)式編程中,先定義了一個(gè)數(shù)據(jù)類型 (datatype) 來表示 Int, Negate, Add链患,這樣定義的目的是什么呢纲仍?舉個(gè)表達(dá)式的例子:

  • Int 代表一個(gè) int 的數(shù)據(jù),比如 Int(2)
  • Negate 代表 Int 的負(fù)數(shù)贸毕,比如 Negate(Int(2)))
  • Add 代表兩個(gè) Int 相加郑叠,比如 Add((Int(2), Int(3))

然后再分別實(shí)現(xiàn)三個(gè)操作 eval, toString, hasZero:

  • eval 是給一個(gè)表達(dá)式求值,比如給 Negate 求值明棍,eval(Negate(Int(2))) = Int(-2) 乡革,給 Add 求值,eval(Add(Int(2), Int(3))) = Int(5)
  • toString 是把這個(gè)表達(dá)式輸出成字符串击蹲,比如 toString(Add(Int(2), Int(3))) = "2 + 3"署拟。
  • hasZero 是判斷表達(dá)式有沒有 0。

再看剛剛這句話函數(shù)式編程的代碼邏輯通常是按照要做什么歌豺,這里的主體是三個(gè)操作推穷,eval, toString 和 hasZero,所以三個(gè)分別是一個(gè)函數(shù)类咧,在函數(shù)里去實(shí)現(xiàn)三種變式怎么操作馒铃。

可以說蟹腾,函數(shù)式編程式縱向的填滿了上面的表格。

面向?qū)ο缶幊?/h3>

這里用 Ruby 來實(shí)現(xiàn)区宇。

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议谷。

可以看到面向?qū)ο缶幊探M織代碼的方式和之前的完全不一樣炉爆。這里把 Int, Negate, Add 抽象成了三個(gè) class,然后分別給每個(gè) class 加上 eval, toString, hasZero 三個(gè)方法卧晓。這也是剛剛那句話的說法 面向?qū)ο缶幊贪汛a邏輯抽象成 class芬首,然后給這些 class 一些操作,這里的主體是 Int, Negate, Add 這三個(gè) class逼裆。

可以說郁稍,面向?qū)ο缶幊淌菣M向的填滿了上的表格。

通過這個(gè)對(duì)比胜宇,可以知道 函數(shù)式編程 和 面向?qū)ο缶幊?是兩種相反的思維模式和實(shí)現(xiàn)方式耀怜。這兩種方式對(duì)代碼的擴(kuò)展性有什么影響呢?

擴(kuò)展實(shí)現(xiàn)

eval toString hasZero absolute
Int
Negate
Add
Multi

在上面那個(gè)例子的基礎(chǔ)上桐愉,我們?cè)偌右恍幸涣胁破疲黾?Multi 這個(gè)變式,表示乘法仅财,增加 absolute 這個(gè)操作狈究,作用是求絕對(duì)值。這會(huì)怎么影響我們的代碼呢盏求?

函數(shù)式編程

在函數(shù)式編程中,要增加一個(gè)操作 absolute 很簡(jiǎn)單亿眠,只要添加一個(gè)新的函數(shù)碎罚,不用修改之前的代碼。但是要增加 Multi 比較麻煩纳像,要修改之前的所有函數(shù)荆烈。

面向?qū)ο缶幊?/h3>

和函數(shù)式編程相反的,在這里增加一個(gè) Multi 簡(jiǎn)單竟趾,只要添加一個(gè)新的 class憔购,但是增加 absolute 這個(gè)操作就要在之前的每一個(gè) class 做更改。

選擇用 函數(shù)式編程 還是 面向?qū)ο缶幊?的一個(gè)考量因素是以后將會(huì)如何擴(kuò)展代碼岔帽,對(duì)之前代碼的更改越少玫鸟,出錯(cuò)的概率越小。

Binary Methods

前面的對(duì)比犀勒,操作都是在一個(gè)數(shù)據(jù)類型上進(jìn)行的屎飘,這里進(jìn)行最后一個(gè)對(duì)比妥曲,一個(gè)函數(shù)對(duì)多個(gè)數(shù)據(jù)類型進(jìn)行操作時(shí),函數(shù)式和面向?qū)ο蠓謩e怎么實(shí)現(xiàn)钦购。

Int String Rational
Int
String
Rational

這里要實(shí)現(xiàn)的是一個(gè) add_values(x, y) 的操作檐盟,把兩個(gè)數(shù)據(jù)相加,但是 x, y 可能是不同的類型的押桃。

函數(shù)式編程

函數(shù)式編程的實(shí)現(xiàn)相對(duì)簡(jiǎn)單:

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葵萎,所以只要把所有可能的數(shù)據(jù)類型(總共9種)都列出來,就可以了唱凯。

面向?qū)ο缶幊蹋憾畏峙?/h3>

按照上面面向?qū)ο缶幊痰睦幽八蓿覀兛梢赃@么做:

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 這個(gè)方法里面去做判斷,看傳入?yún)?shù)的類型波丰,去做相應(yīng)的操作壳坪。這種做法不是那么的 面向?qū)ο螅梢杂辛硗庖环N寫法:

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
...

這里涉及到了一個(gè)概念 二次分派 (Double Dispatch)掰烟,在一次方法的調(diào)用過程中爽蝴,做了兩次 動(dòng)態(tài)分派 (Dynamic Dispatch) 。用例子來說明

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

i.add_values(s)在調(diào)用這個(gè)方法時(shí)纫骑,實(shí)現(xiàn)了一次 dispatch蝎亚,到 add_values 這個(gè)方法里后,做的其實(shí)是 s.addInt i先馆,也就是去調(diào)用了 MyString 里的 addInt 這個(gè)方法发框,這是第二次 dispatch,所以叫做 double dispatch煤墙。

總結(jié)

函數(shù)式編程 和 面向?qū)ο缶幊?對(duì)比下來梅惯,我們并不能說哪一種模式更好。但是可以看出它們?cè)谒季S上是截然不同的仿野。函數(shù)式編程中側(cè)重要做什么铣减,面向?qū)ο缶幊虃?cè)重對(duì)象的抽象化,在有些編程語言里脚作,比如 Java葫哗,是都可以實(shí)現(xiàn)的,但是要用哪種還要根據(jù)需求具體考慮球涛。如果要了解更多 函數(shù)式編程 和 面向?qū)ο缶幊?的基礎(chǔ)概念的話劣针,可以看看之前的這三篇文章。

推薦閱讀:
編程語言的一些基礎(chǔ)概念(一):靜態(tài)函數(shù)式編程
編程語言的一些基礎(chǔ)概念(二):動(dòng)態(tài)函數(shù)式編程
編程語言的一些基礎(chǔ)概念(三):面向?qū)ο?/a>

  • 序言:七十年代末亿扁,一起剝皮案震驚了整個(gè)濱河市捺典,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌魏烫,老刑警劉巖辣苏,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肝箱,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡稀蟋,警方通過查閱死者的電腦和手機(jī)煌张,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來退客,“玉大人骏融,你說我怎么就攤上這事∶瓤瘢” “怎么了档玻?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)茫藏。 經(jīng)常有香客問我误趴,道長(zhǎng),這世上最難降的妖魔是什么务傲? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任凉当,我火速辦了婚禮,結(jié)果婚禮上售葡,老公的妹妹穿的比我還像新娘看杭。我一直安慰自己,他們只是感情好挟伙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布楼雹。 她就那樣靜靜地躺著,像睡著了一般尖阔。 火紅的嫁衣襯著肌膚如雪贮缅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天诺祸,我揣著相機(jī)與錄音携悯,去河邊找鬼。 笑死筷笨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的龟劲。 我是一名探鬼主播胃夏,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼昌跌!你這毒婦竟也來了仰禀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤矮湘,失蹤者是張志新(化名)和其女友劉穎荧嵌,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體三圆,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡悬嗓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年污呼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片包竹。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡燕酷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出周瞎,到底是詐尸還是另有隱情苗缩,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布声诸,位于F島的核電站酱讶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏彼乌。R本人自食惡果不足惜泻肯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望囤攀。 院中可真熱鬧软免,春花似錦、人聲如沸焚挠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蝌衔。三九已至榛泛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間噩斟,已是汗流浹背曹锨。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留剃允,地道東北人沛简。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像斥废,于是被迫代替她去往敵國(guó)和親椒楣。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容