您當前的位置:首頁 > 攝影

函數語言程式設計和麵向物件程式設計:一個淺顯的比較

作者:由 Tp Zu 發表于 攝影時間:2020-03-06

簡介

函數語言程式設計(FP)和麵向物件式程式設計(OOP)是現在比較流行的兩種的程式設計正規化。本文嘗試從兩種程式設計正規化對解決同一個問題的不同方式出發,廣義地比較一下兩種程式設計正規化的區別(有關兩種程式設計正規化的特性,比如FP的高階函式,柯里化以及OOP的繼承、覆寫、動態排程等本文不作敘述)。

總的來說,計算機程式通常由兩部分內容組成:資料和操作。在FP中,這兩者通常由值和函式 來表示;在OOP中,我們把資料和操作定義在一個

class

中,透過類屬性和類方法來表示。

首先,我們透過實現一個簡單的數學表示式處理器,來展示和比較一下FP和OOP在分解和解決問題時用到的不通方法,或者說是解決問題的的不同“哲學”。

目標

對於這樣一個數學表示式處理器,我們可以把問題分解成資料(表示式)和操作兩個部分

表示式(expression):一個表示式可以是一個數字比如

1

,也可以是數學算式比如

1+1

操作(operation):一個操作就是對錶達式的使用並返回結果,比如計算

1+1

2

, 判斷表示式中是否含有某個數字等

我們的初步設計如下:

——————————————————————————————————————

\begin{array}[b] {|c|c|}  \hline  & eval(計算表示式)& toString(轉換成字串)& hasZero(是否含有0)\\  \hline Int(整數表示式)  &  &   \\  \hline Add(加法表示式)  &&   \\  \hline Negate(取負表示式)  & &  \\  \end{array}\\

——————————————————————————————————————

具體地來講,我們需要實現上述表中每個表示式對應的每個操作地組合,也就是填滿所有的空格才能實現我們期望的功能。

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

print

a

toString

())

# Print a as its string format

>>>

‘(1 + 2)’

print

a

eval

()

toString

())

# Evaluate and print the result as its string format

>>>

‘3’

print

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

print

toString

a

))

# Convert the add expression to its string format

>>>

‘(1+2)’

print

eval

a

))

# Evaluate the result of the add expression

>>>

‘Exp’

‘Int’

3

))

print

toString

eval

a

)))

# Evaluate and print the result as its string format

>>>

‘3’

print

toString

eval

n

)))

# Evaluate and print the negated a as a string

>>>

‘-3’

* Remark:由此我們看出,FP對問題的分解是由操作驅動的,即把它分解成多個函式然後在函式內部分別處理不同的資料型別

小結

再回過頭去看下面這張表我們可以發現,如果把實現功能比作填充表中的空格的話,那麼:

——————————————————————————————————————

\begin{array}[b] {|c|c|}  \hline  & eval(計算表示式)& toString(轉換成字串)& hasZero(是否含有0)\\  \hline Int(整數表示式)  &  &   \\  \hline Add(加法表示式)  &&   \\  \hline Negate(取負表示式)  & &  \\  \end{array}\\

——————————————————————————————————————

函數語言程式設計相當於按列填充這張表格,實現一個函式相當於把所有資料變種(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

來幫助我們處理了不同資料之間的互動。實際上也就是實現了下表中行和列之間的互動

——————————————————————————————————————

\begin{array}[b] {|c|c|}  \hline  & Int& Rational\\  \hline Int  & Int add Int & Int \ add \ Rational  \\  \hline Rational  & Rational\ add \ Int  & Ratiaonal \ add \ Rational \\ \hline \end{array}\\

——————————————————————————————————————

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/

標簽: self  EXP  int  val1  eval