本篇文章是啟發自 The Expression Problem and its solutions,當中談論的是在撰寫程式的過程中,所採用的程式典範不同,會在程式表達上遇到不同程度的困難,稱之為表達問題(expression problem)。

物件導向語言中的表達問題

以原文中的例子,作者想以物件導向語言來撰寫一個簡單的 expression evaluator。基本上採用直譯器模式(interpreter pattern),這邊我把程式碼以 python 重寫。

1
2
3
4
5
class Expr:
def tostring(self):
return 0
def eval(self):
return 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Constant(Expr):
def __init__(self, value):
self.__value = value
return self

def tostring(self):
return str(self.__value)

def eval(self):
return self.__value

class BinaryPlus(Expr):
def __init__(self, lhs, rhs):
self.__lhs = lhs
self.__rhs = rhs
return self

def tostring(self):
return self.__lhs.tostring() + " + " + self.__rhs.tostring()

def eval(self):
return self.__lhs.eval() + self.__rhs.eval()

我們先定義了一個 Expr,我們希望提供 tostringeval 兩種操作。我們後續定義了常數 Constant,也讓常數實作這兩種操作。

如果我們想要擴充 Expr 或是 Constant 的時候該怎麼做呢?更精確地說,如果我們想要新增新的操作到既有的類別上,我們該怎麼做?一般情況下可能就是直接在原有的類別上加上新的方法。不過這樣其實違反了軟體工程原則,開放封閉原則(open-closed principle),我們應該要對舊有的類別、方法、介面等等程式碼修改保持封閉,也就是不能去修改既有程式碼,我們應該對新增程式碼保持開放,也就是允許新增程式碼。

我們可以發現在物件導向語言當中,新增類別是容易的,但是新增方法是困難的。這是在物件導向語言中所遇到的表達問題。

函數式語言中的表達問題

以 Haskell 為例,撰寫以上的程式(這邊直接引用原文)。

1
2
3
4
5
6
7
8
9
10
11
12
data Expr = Constant Double
| BinaryPlus Expr Expr

stringify :: Expr -> String
stringify(Constant c) = show c
stringify(BinaryPlus lhs rhs) = stringify lhs
++ " + "
++ stringify rhs

evaluate :: Expr -> Double
evaluate(Constant c) = c
evaluate(BinaryPlus lhs rhs) = evaluate lhs + evaluate rhs

在 Haskell 中,要新增方法是容易的,但是如果要新增型別的話,就得動到 data 的定義。在函數式語言中,新增型別是困難的。這是在函數式語言中所遇到的表達問題。

Julia 語言中的表達問題?

文章中有提到 Clojure 採用的是 multi-methods,他可以對應到 Julia 的多重分派(multiple dispatch)。多重分派可以好好地處理表達問題,讓新增型別及方法都是簡單的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract type Expr end

tostring(::Expr) = 0
eval(::Expr) = 0

struct Constant{T}
value::T
end

tostring(::Constant) = String(c.value)
eval(c::Constant) = c.value

struct BinaryPlus
lhs::Expr
rhs::Expr
end

tostring(b::BinaryPlus) = tostring(b.lhs) * " + " * tostring(b.rhs)
eval(b::BinaryPlus) = eval(b.lhs) + eval(b.rhs)

不過他文末也有提到一個關鍵是 open method,也就是將方法定義在類別之外,如此一來,新增方法就會是簡單的。所以真正解決表達問題的並不是多重分派的機制,而是 open method 的設計。