函數式編程 (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,是都可以實現的,但是要用哪種還要根據需求具體考慮。如果要了解更多 函數式編程 和 面向對象編程 的基礎概念的話,可以看看之前的這三篇文章。
推薦閱讀:
編程語言的一些基礎概念(一):靜態函數式編程
編程語言的一些基礎概念(二):動態函數式編程
編程語言的一些基礎概念(三):面向對象