函數語言程式設計和麵向物件程式設計:一個淺顯的比較
簡介
函數語言程式設計(FP)和麵向物件式程式設計(OOP)是現在比較流行的兩種的程式設計正規化。本文嘗試從兩種程式設計正規化對解決同一個問題的不同方式出發,廣義地比較一下兩種程式設計正規化的區別(有關兩種程式設計正規化的特性,比如FP的高階函式,柯里化以及OOP的繼承、覆寫、動態排程等本文不作敘述)。
總的來說,計算機程式通常由兩部分內容組成:資料和操作。在FP中,這兩者通常由值和函式 來表示;在OOP中,我們把資料和操作定義在一個
class
中,透過類屬性和類方法來表示。
首先,我們透過實現一個簡單的數學表示式處理器,來展示和比較一下FP和OOP在分解和解決問題時用到的不通方法,或者說是解決問題的的不同“哲學”。
目標
對於這樣一個數學表示式處理器,我們可以把問題分解成資料(表示式)和操作兩個部分
表示式(expression):一個表示式可以是一個數字比如
1
,也可以是數學算式比如
1+1
操作(operation):一個操作就是對錶達式的使用並返回結果,比如計算
1+1
為
2
, 判斷表示式中是否含有某個數字等
我們的初步設計如下:
——————————————————————————————————————
——————————————————————————————————————
具體地來講,我們需要實現上述表中每個表示式對應的每個操作地組合,也就是填滿所有的空格才能實現我們期望的功能。
OOP實現
定義一個
Expression
父類, 在父類裡面定義每個操作的虛擬方法(本文采用
python
實現,所以虛擬方法可以忽略)。
為每個表示式定義一個屬於
Expression
的子類。
在每個子類裡,為每個操作定義一個方法。
父類
class
Expression
:
def
eval
(
self
):
pass
def
toString
(
self
):
pass
def
hasZero
(
self
):
pass
Int
子類
class
Int
(
Expression
):
def
__init__
(
self
,
i
):
self
。
_i
=
i
# Overrides
def
eval
(
self
):
return
Int
(
self
。
_i
)
# Overrides
def
toString
(
self
):
return
str
(
self
。
_i
)
#Overrides
def
hasZero
(
self
):
return
self
。
_i
==
0
Add
子類
class
Add
(
Expression
):
def
__init__
(
self
,
exp1
,
exp2
):
self
。
_exp1
=
exp1
self
。
_exp2
=
exp2
# Overrides
def
eval
(
self
):
val1
=
self
。
_exp1
。
eval
()
。
_i
val2
=
self
。
_exp2
。
eval
()
。
_i
return
Int
(
val1
+
val2
)
# Overrides
def
toString
(
self
):
return
“(”
+
self
。
_exp1
。
toString
()
+
“ + ”
+
self
。
_exp2
。
toString
()
+
“)”
#Overrides
def
hasZero
(
self
):
return
self
。
_exp1
。
hasZero
()
or
self
。
_exp2
。
hasZero
()
Negate
子類
class
Negate
(
Expression
):
def
__init__
(
self
,
exp
):
self
。
_exp
=
exp
# Overrides
def
eval
(
self
):
val1
=
self
。
_exp
。
eval
()
。
_i
return
Int
(
-
val1
)
# Overrides
def
toString
(
self
):
return
“-”
+
“(”
+
self
。
_exp
。
toString
()
+
“)”
#Overrides
def
hasZero
(
self
):
return
self
。
_exp
。
hasZero
()
使用示例
i
=
Int
(
1
)
# An Int-expression instance
j
=
Int
(
2
)
a
=
Add
(
i
,
j
)
# An Add-expression instance
n
=
Negate
(
a
)
(
a
。
toString
())
# Print a as its string format
>>>
‘(1 + 2)’
(
a
。
eval
()
。
toString
())
# Evaluate and print the result as its string format
>>>
‘3’
(
n
。
eval
()
。
toString
())
# Evaluate and print the negated a as a string
>>>
‘-3’
*remark:在此可見OOP對問題的分解是由資料驅動的:它把問題分解成不同的由資料主導的類然後在類裡面定義方法以實現需要的功能
*remark2:如果你瞭解惰性計算的話,可以看出我們這個表示式引擎是惰性的——只有在eval的時候才會真正開始計算
FP實現
定義一個
expression
資料型別,以及該資料型別下的各個子型別
為每個操作定義一個函式
在每個函式中,透過分支處理不同資料型別對應的操作
由於python並不完全支援函數語言程式設計正規化,因此我們無法定義單純的資料型別,但是我們可以透過tuple來模擬FP中的資料型別
具體如下:
# A expression type example
exp
=
(
‘Exp’
,
sub_exp
)
# Add-expression example
add
=
(
‘Exp’
,
(
‘Add’
,
e1
,
e2
))
# Negate-expression example
negate
=
(
‘Exp’
,
(
‘Negate’
,
e
))
# Int-expression example
integer
=
(
‘Exp’
,
(
‘Int’
,
i
))
每個資料型別的簽名儲存在
tuple
的第一個值中
(在python中模擬
type
確實比較蹩腳,感興趣的讀者可以看看下面這個純FP語言中的
type
定義,語言為
SML
)
datatype
exp
=
Int
of
int
|
Negate
of
exp
|
Add
of
exp
*
exp
定義完資料型別後,我們為每一個操作定義一個函式,並在函式內透過
pattern match
(可以理解為
if-else
條件判斷)的方式處理不同的資料型別, 實現如下:
eval
操作
def
eval
(
exp
):
if
exp
[
0
]
==
‘Exp’
:
if
exp
[
1
][
0
]
==
‘Int’
:
return
exp
elif
exp
[
1
][
0
]
==
‘Add’
:
val1
=
eval
(
exp
[
1
][
1
])
val2
=
eval
(
exp
[
1
][
2
])
return
(
‘Exp’
,
(
‘Int’
,
val1
[
1
][
1
]
+
val2
[
1
][
1
]))
elif
exp
[
1
][
0
]
==
‘Negate’
:
val
=
eval
(
exp
[
1
][
1
])
return
(
‘Exp’
,
(
‘Int’
,
-
val
[
1
][
1
]))
(程式碼中大量的索引操作是為了獲取資料的型別標籤和值,返回值的時候需要手動標上返回值的
super type
(父型別,即
Exp
)。這在一個成熟的FP語言中都是內建的,不用這麼麻煩)
toString
操作
def
toString
(
exp
):
if
exp
[
0
]
==
‘Exp’
:
if
exp
[
1
][
0
]
==
‘Int’
:
return
str
(
exp
[
1
][
1
])
elif
exp
[
1
][
0
]
==
‘Add’
:
e1
=
exp
[
1
][
1
]
e2
=
exp
[
1
][
2
]
return
“(”
+
toString
(
e1
)
+
“+”
+
toString
(
e2
)
+
“)”
elif
exp
[
1
][
0
]
==
‘Negate’
:
e
=
exp
[
1
][
1
]
return
“-”
+
“(”
+
toString
(
e
)
+
“)”
hasZero
操作
def
hasZero
(
exp
):
if
exp
[
0
]
==
‘Exp’
:
if
exp
[
1
][
0
]
==
‘Int’
:
return
exp
[
1
][
1
]
==
0
elif
exp
[
1
][
0
]
==
‘Add’
:
e1
=
exp
[
1
][
1
]
e2
=
exp
[
1
][
2
]
return
hasZero
(
e1
)
or
hasZero
(
e2
)
elif
exp
[
1
][
0
]
==
‘Negate’
:
e
=
exp
[
1
][
1
]
return
hasZero
(
e
)
使用示例
i
=
(
‘Exp’
,
(
‘Int’
,
1
))
# An integer expression definition
j
=
(
‘Exp’
,
(
‘Int’
,
2
))
a
=
(
‘Exp’
,
(
‘Add’
,
i
,
j
))
# An add expression
n
=
(
‘Exp’
,
(
‘Negate’
,
a
))
# An negate expression
(
toString
(
a
))
# Convert the add expression to its string format
>>>
‘(1+2)’
(
eval
(
a
))
# Evaluate the result of the add expression
>>>
(
‘Exp’
,
(
‘Int’
,
3
))
(
toString
(
eval
(
a
)))
# Evaluate and print the result as its string format
>>>
‘3’
(
toString
(
eval
(
n
)))
# Evaluate and print the negated a as a string
>>>
‘-3’
* Remark:由此我們看出,FP對問題的分解是由操作驅動的,即把它分解成多個函式然後在函式內部分別處理不同的資料型別
小結
再回過頭去看下面這張表我們可以發現,如果把實現功能比作填充表中的空格的話,那麼:
——————————————————————————————————————
——————————————————————————————————————
函數語言程式設計相當於按列填充這張表格,實現一個函式相當於把所有資料變種(variant)的同一操作都實現了
面向物件式程式設計相當於按行填充,實現一個物件相當於把一個數據變種的所有不同操作都實現了。
我們很難憑空地比較這兩種不同程式設計正規化地好壞,它們都是解決問題不同的正確的方式。就具體問題來說:如果我們有很多資料變種,少量的操作,函數語言程式設計可能比較好些;反之則面相物件程式設計比較好些。
在程式面對擴充套件時,函數語言程式設計允許我們在不改變現有程式碼的情況下
(自然地)
新增新的操作;面向物件程式設計則允許我們
(自然地)
新增新的資料變種。
程式的擴充套件
大部分情況下,我們寫完程式碼後,它都會面臨著功能或者資料型別的擴充套件。因此,無論我們選擇什麼樣的程式設計正規化,後續的都可能會面臨我們計劃之外的擴充套件。此時,我們可以用使用一些程式設計“技巧”來幫助我們在FP程式碼中進行資料型別擴充套件以及在OOP程式碼中進行操作的擴充套件。比如,想給
eval
函式新的引數型別,在不改變原來程式碼的情況下,我們可以:
# Pass a new function to deal with our new type。
def
eval_ext
(
f
,
exp
):
if
exp
[
0
]
==
‘Exp’
:
if
exp
[
1
][
0
]
==
‘Int’
:
return
exp
elif
exp
[
1
][
0
]
==
‘Add’
:
val1
=
eval
(
exp
[
1
][
1
])
val2
=
eval
(
exp
[
1
][
2
])
return
(
‘Exp’
,
(
‘Int’
,
val1
[
1
][
1
]
+
val2
[
1
][
1
]))
elif
exp
[
1
][
0
]
==
‘Negate’
:
val
=
eval
(
exp
[
1
][
1
])
return
(
‘Exp’
,
(
‘Int’
,
-
val
[
1
][
1
]))
else
:
return
f
(
exp
)
# Type extention。
這樣在未來我們的程式碼面臨資料型別的擴充套件時,我們把它對應的操作傳入都老的操作函式中,這樣既避免了程式碼的修改,又能新增新的操作。
那麼如果要往OOP中新增新的操作,我們可以使用OOP的
訪問者模式
,為每個類新增一個新的
accept
方法 ,例如:
class
AddExt
(
Expression
):
def
__init__
(
self
,
exp1
,
exp2
):
self
。
_exp1
=
exp1
self
。
_exp2
=
exp2
# Overrides
def
eval
(
self
):
val1
=
self
。
_exp1
。
eval
()
。
_i
val2
=
self
。
_exp2
。
eval
()
。
_i
return
Int
(
val1
+
val2
)
# Overrides
def
toString
(
self
):
return
“(”
+
self
。
_exp1
。
toString
()
+
“ + ”
+
self
。
_exp2
。
toString
()
+
“)”
#Overrides
def
hasZero
(
self
):
return
self
。
_exp1
。
hasZero
()
or
self
。
_exp2
。
hasZero
()
# Pass an object to allow new operation
def
accept
(
self
,
other
):
return
other
。
proceessAdd
(
self
)
我們添加了一個
accept
方法來接收定義了新操作的物件,利用OOP的
dynamic dispatch
特性來達到擴充套件操作的效果。
不同資料型別的互動
我們在上文中實現的算數操作只涉及了單個數據型別,比如我們
Add
的時候只考慮了
Int
和
Int
的求和,假如現在我們有一個
Rational
資料型別來表示一個分數,在
Add
的時候我們就要考慮兩種型別的資料互動了。在FP和OOP中,以
Add
為例,我們可以分別這樣做:
FP
def
eval
(
exp
):
if
exp
[
0
]
==
‘Exp’
:
if
exp
[
1
][
0
]
==
‘Int’
:
return
exp
elif
exp
[
1
][
0
]
==
‘Add’
:
val1
=
eval
(
exp
[
1
][
1
])
val2
=
eval
(
exp
[
1
][
2
])
# Use a helper function to deal with combinations
# of different datatypes
return
(
‘Exp’
,
addValues
(
val1
,
val2
))
elif
exp
[
1
][
0
]
==
‘Negate’
:
val
=
eval
(
exp
[
1
][
1
])
return
(
‘Exp’
,
(
‘Int’
,
-
val
[
1
][
1
]))
elif
:
exp
[
1
][
0
]
==
‘Rational’
:
return
exp
def
addValues
(
val1
,
val2
):
if
(
val1
[
1
][
0
]
==
‘Rational’
and
val2
[
1
][
0
]
==
‘Rational’
):
return
(
‘Rational’
,
val1
[
1
][
1
]
*
val2
[
1
][
2
]
+
val2
[
1
][
1
]
*
val1
[
1
][
2
],
val2
[
1
][
2
]
*
val1
[
1
][
2
])
elif
(
val1
[
1
][
0
]
==
‘Rational’
and
val2
[
1
][
0
]
==
‘Int’
):
return
(
‘Rational’
,
val1
[
1
][
1
]
+
val2
[
1
][
1
]
*
val1
[
1
][
2
],
val1
[
1
][
2
])
elif
(
val1
[
1
][
0
]
==
‘Int’
and
val2
[
1
][
0
]
==
‘Int’
):
return
(
‘Int’
,
val1
[
1
][
1
]
+
val2
[
1
][
1
])
elif
(
val1
[
1
][
0
]
==
‘Int’
and
val2
[
1
][
0
]
==
‘Ratitonal’
):
return
addValues
(
val2
,
val1
)
程式碼中使用了一個輔助函式來幫助
addValues
來幫助我們處理了不同資料之間的互動。實際上也就是實現了下表中行和列之間的互動
——————————————————————————————————————
——————————————————————————————————————
OOP
class
Rational
(
Expression
):
def
__init__
(
self
,
a
,
b
):
self
。
_a
=
a
self
。
_b
=
b
def
addInt
(
self
,
other
):
return
Rational
(
other
。
_i
*
self
。
_b
+
self
。
_a
,
self
。
_b
)
def
addRational
(
self
,
other
):
return
Rational
(
other
。
_a
*
self
。
_b
+
other
。
_b
*
self
。
_a
,
self
。
_b
*
other
。
_b
)
def
addValues
(
self
,
other
):
other
。
addRational
(
self
)
# Overrides
def
eval
(
self
):
return
Rational
(
self
。
_a
,
self
。
_b
)
# Overrides
def
toString
(
self
):
pass
#Overrides
def
hasZero
(
self
):
pass
class
Int
(
Expression
):
def
__init__
(
self
,
i
):
self
。
_i
=
i
def
addInt
(
self
,
other
):
return
Int
(
other
。
_i
+
self
。
_i
)
def
addRational
(
self
,
other
):
return
other
。
addInt
(
self
)
# addInt already implemented in Rational
def
addValues
(
self
,
other
):
other
。
addInt
(
self
)
# Overrides
def
eval
(
self
):
return
Int
(
self
。
_i
)
# Overrides
def
toString
(
self
):
pass
#Overrides
def
hasZero
(
self
):
pass
程式碼中我們在各個類裡面均定義了與其他類互動的
add
方法,透過
雙分派(double dispatch)
技術完成了不同類之間的互動。
在
Add
類中,我們對
eval
方法做以下修改:
class
Add
(
Expression
):
def
__init__
(
self
,
exp1
,
exp2
):
self
。
_exp1
=
exp1
self
。
_exp2
=
exp2
# Overrides
def
eval
(
self
):
return
self
。
_exp1
。
eval
()
。
add_values
(
self
。
_exp2
。
eval
())
# Overrides
def
toString
(
self
):
pass
#Overrides
def
hasZero
(
self
):
pass
我們可以看到,4中不同的
add
情況被分散到了各個類的實現中,並透過雙分派技術讓物件自己挑選出正確的程式碼。
* Remark 其實在OOP中,我們也可以透過判斷傳入物件的型別,使用if-else語句來達到同樣的效果,但是這種方式不太符合面相物件的特點(物件的互動均由方法完成)
* Remark2 在上面這種情況中,每增加一種資料型別,就要增加一些新的操作,無論OOP還是FP都不能很自然地處理這種擴充套件。但是我們可以透過上面實現來儘量減少對原始碼地改動(即只怎加邏輯,不修改已有的邏輯)
總結
在目標的分解上,OOP是資料驅動的,FP是操作驅動的;我們可以在不改變原始碼的情況下,“自然地”給OOP程式碼新增新的型別,給FP程式碼新增新的操作。
在兩種正規化面對不“自然”的擴充套件時,FP採用傳入新函式的方式處理新增型別;OOP採用訪問者設計模式來處理新增操作,來避免對原始碼的修改。
在面對不同資料型別的互動時,FP透過模式匹配的方式處理不同的資料型別組合;OOP利用雙分派技術讓類自己選擇正確的處理方法。
參考:https://courses。cs。washington。edu/courses/cse341/