第三章 探討 AspectFun 的程式重用性與擴充性
3.4 相關程式重用與擴充議題探討
國
立 政 治 大 學
‧
N a tio na
l C h engchi U ni ve rs it y
_ -> proceed e} in
plus@advice around{eval}(e :: Plus a b) = case e of
{Plus e1 e2 -> eval e1 + eval e2;
_ -> proceed e} in
在此程式中可以發現 advice 與 type class 在擴充函數時的差異性,使用 advice 避免了 type class 複雜的相依關係,advice 沒有 class 間的繼承關係,使用 advice 在擴充函數時更直覺簡易,且 advice 與 type class 的最大差異在於 advice 可針對某 特定型態的函數引數重複定義進行擴充,例如我們想要知道在每次 eval If 運算式時執 行的是 then 或者 else 部分的程式,我們可額外定義一個 advice log
log@advice around {eval} (e :: If a b c) = case e of {
(If e1 e2 e3) -> if (iszero $ eval e1)
then {println “eval else”; proceed e}
else {println “eval then”; proceed e};
_ -> proceed e} in
而 type class 針對每一種型態只能定義一個對應的操作,一旦需要增加新的功能只能修 改舊有的程式碼。
3.4 相關程式重用與擴充議題探討
在函數式語言中,遞迴函數佔有相當重要的地位,我們經常會使用遞迴函數來處理遞迴 宣告的資料型態,例如常被用來實作檔案系統的 Tree,如下列範例所示
data Tree a = Node (Tree a) (Tree a) | Leaf a csum (Leaf i) = i
| csum (Node (l, r)) = csum l + csum r
csum 用來計算 Tree 中所有 Leaf 的總合。為了實現程式碼重用,我們藉由 csum 函數來
‧ 國
立 政 治 大 學
‧
N a tio na
l C h engchi U ni ve rs it y
定義一個新函數 csumEven :
csumEven t @(Leaf i) = let val x = csum t
in if x mod 2 = 0 then x else 0
| csumEven t = csum t
t1 = Leaf 2
t2 = Node (Leaf 4) (Leaf 5)
csumEven 用來計算 Tree 中 Leaf 為偶數值的總合,然而實際執行 csumEven 函數並無法 正確的計算 tree 中所有偶數值的總合。
csumEven t1 -- output 2.
csumEven t2 -- output 9 rather than 4..
因為當引數值為 Node 時,csumEven 會呼叫 csum 函數處理 Node 的部分,而 csum 函數 接著遞迴執行 csum 而非 csumEven,所以導致 csumEven 的結果錯誤。針對遞迴函數的 程式碼重用問題,研究[15]提出使用函數式語言模擬物件導向語言的 dynamic dispatch,
將函數定義中使用到遞迴呼叫的部分保留先不決定,直到使用時才決定實際被遞迴呼叫 為何。可擴充的遞迴程式如下 :
sum this (Leaf i) = i
| sum this (Node (l, r)) = this l + this r
sumEven this t @(Leaf i) = let val x = sum this t
in if x mod 2 = 0 then x else 0
| sumEven this t = sum this t
fsumEven t = sumEven fsumEven t
‧ 國
立 政 治 大 學
‧
N a tio na
l C h engchi U ni ve rs it y
在程式中,可重用的遞迴函數多了一個額外的參數 this,所有預期在日後會被繼續重用 的遞迴函數皆必頇在函數一開始定義的時候就預留此 this 參數,但針對那些已存在的且 不可被重用的遞迴函數,便無法在不修改它的狀況下達到重用程式碼的目的。
由於遞迴函數 csum 不能重用的問題在於它每次遞迴呼叫時必非預期的對象 csumEven,若我們可在 csumEven 執行的期間動態改變 csum 遞迴呼叫的對象,
就可不修改 csum 定義而實現程式碼重用的目的。在剖面導向程式語言中我們常 會使用 cflow 來控制剖面模組影響的範圍,所以我們可以藉由 cflow 來修正 csumEven 錯誤的結果
even@advice around {csum+cflow(csumEven)} (arg) = case arg of { Leaf i -> csumEven arg;
_ -> proceed arg } in csumEven t2 -- output correct value 4
csum t2 -- output 9, advice even do not affect the call to csum
此程式中,當 csumEven 執行時 advice even 會攔截所有 csum 函數呼叫,若引數 值為 Leaf 則改變函數呼叫的對象,而原先錯誤的 csumEven 也因為剖面模組的影 響修正為正確的結果。
‧ 國
立 政 治 大 學
‧
N a tio na
l C h engchi U ni ve rs it y
第四章
以 AspectFun 輔助實現泛型程式設計
此章節將介紹函數式語言上的泛型程式設計相關研究,並闡述在 AspectFun 上如何輔 助實現泛型程式設計的最初構想與遭遇問題,最後說明本研究為了以 AspectFun 輔助 實現泛型程式設計所進行的擴充及修改的實作細節。
4.1 泛型程式設計
4.1.1 重載函數(overloaded function)
在函數式語言中,程式設計者除了可定義用來操作某特定型態的函數如 eval 函數 : eval :: Expr -> Int
也常會使用一些重載函數,所謂的重載函數是指該函數可用來操作程式中多種資料型態 的集合,例如在函數式語言中經常被使用的 show、eq 函數,show 函數可用來將多種型 態的數值轉為字串方式呈現,eq 函數是用來比較兩個相同型態的數值是否相等。在 Haskell 語言中,程式設計者通常是使用 type class 機制來實現重載函數的定義與擴充,
針對該重載函數欲處理的新型態去定義函數如何處理。
重載函數的缺點是一旦程式擴充了新的型態,對於 eq 這類會用在程式中大多數型態的 函數,程式設計者就必頇隨著型態增加而擴充函數定義,若程式中有 n 個需求如同 eq 的函數,當程式新增了一種型態,我們便必頇進行 n 次擴充,這樣的程式設計方法雖然 可以解決程式需求,但在修改程式碼的過程中可能會造成新的問題,這種顯然不是個有 效率的程式設計方法。
在 Haskell 中提供了 deriving 機制可用來解決部分的重載函數擴充問題,若程式設 計者自行定義的資料型態欲被 show 函數操作,可在該資料型態定義加上 deriving Show,如 :
‧
重複進行類似的工作。Haskell 的 deriving 機制雖然看似解決重載函數的擴充問題,但其 實 deriving 機制只適用於部分語言內建的重載函數如 Eq、Show、Read、Ord 等,針對 程式設計者自行定義的重載函數並無法使用 deriving 機制,也就是程式設計者仍然要自 行處理函數擴充的問題,例如我們欲使用自定義的 pretty 函數用來取代 show 函數:class Pretty a where pretty :: a String instance Pretty Int where
pretty i = int2String i
instance Pretty a => Pretty [a] where pretty [] = “”
‧ 國
立 政 治 大 學
‧
N a tio na
l C h engchi U ni ve rs it y
言上支援泛型程式設計的作法。它的作法主要概念如圖 4.1 所示,程式中額外使用一中 介資料型態 Spine a,而所有泛型函數只需定義如何去操作 Spine a 此中介資料型態,而 其他的資料型態如 Int、Char、List 等則藉由轉換函數 toSpine 被轉換為中介資料型態表 示,每當新增一種資料型態時,程式設計者只需要提供此新的資料型態如何轉換 Spine a 的轉換定義,即擴充重載函數 toSpine,而原來定義好的所有泛型函數不需要重新被修 改或擴充。
圖 4. 1 : Spine 泛型作法概念
在 Hinze 教授作法中所提及的中介資料型態 Spine a,是將一般函數式語言的資料型 態宣告的結構抽象化,由於一般函數式語言的資料型態定義通常是 :
data TypeName = ConstructorName arg1 arg2 arg3 arg4 …
每個資料建構函數都會有建構函數名稱以及建構函數所需的引數值,針對資料型態定義 在語法結構上的相似性因此可以被對應到 Spine a 表示,在 Spine a 中,資料建構函數名 稱以 Con 表示,若該建構函數需要引數,則使用 App 表示將建構函數 apply 至引數上,
而語言內建的基本資料型態(primitive type)如 Int、Char 則視為無引數的資料建構函數,
‧
Existential type,Existential type 的目的是為了將型態資訊隱藏起來,在一般的資 料型態定義中,我們可以將建構函數所需的引數型態參數化 :data Tree a = Node (Tree a) (Tree a) | Leaf a
其中 a 是一型態變數,型態參數化的好處是我們不需針對各種型態的 Tree 作個別定義,
當建構函數被使用時,型態變數 a 便會依據引數型態而被綁定為某個特定型態,例如 (Leaf 5)我們可知此其型態為攜帶整數的 Tree。Existential type 是指一型態變數只 出現在資料型態定義的右邊(只在建構函數定義部分)
data Tree =Node (Tree) (Tree)
|forall b. Show b => Leaf b
當一個型態變數為 Existential type 時,型態資訊就會被隱藏,因此我們只知(Leaf 5) 型態為 Tree 但不知其攜帶的數值型態為何,以 Existential type 定義的 Tree 可以混 合攜帶各種不同型態的值如(Node (Leaf 2) (Leaf True)),這是一個合乎型態安全的 Tree。在 Haskell 中我們可以對型態變數加上部分限制,例如 Show b 即是限制型態變 數 b 是任何可被 show 函數操作的型態。在 Spine a 定義中,由於 App 是表示建構函數 名稱 apply 至引數,在非空串列的 Spine 表示中,型態變數 b 可能是 x :: a 或 xs :: [a],
因此為了確保此編碼方式可行在 App 定義中必頇使用 Existential type,在後續章節會
‧ 國
立 政 治 大 學
‧
N a tio na
l C h engchi U ni ve rs it y
說明 Existential type 對此研究造成的問題。
4.2 實現泛型程式設計所面臨的問題
在 Hinze 教授提出的泛型方法中期望將所有的重載函數都可以泛型函數替代,然而此泛 型方法被發現並無法確保所有的泛型函數的計算結果都如同程式設計者預期,其中 strings 函數便是一個代表性問題指出此泛型作法的問題。strings 函數問題是指我們希望 設計一泛型函數 strings,它可用來蒐集其引數中所有被使用的字串值如
strings “abc”
expected output [“abc”]
strings 5
expected output [ ] strings [“abc”, “def”]
expected output [“abc”, “def”]
實際執行的結果發現,當引數為一字串串列時,strings 函數的回傳值並非如同預期結果 是其引數,而是一空串列。以下我們將分別以 type class 機制與 AspectFun 的剖面機制說 明其實作方式與問題點。
4.2.1. 以 type classes 機制輔助實現泛型程式設計的實作與問題
在 Haskell 中,我們首先嘗試以 type class 機制去實現 Hinze 教授所提出的泛型方法,
由於 toSpine 函數是此方法中必需的轉換函數,藉由 type classes 我們很容易便可定義出 此重載函數並可對其進行擴充 :
class ToSpine a where toSpine :: a -> Spine a instance ToSpine Int where toSpine i = Con (Descr 0)
‧
instance ToSpine Char where toSpine c = Con (Descr „a‟)
instance ToSpine a => ToSpine [a] where toSpine [ ] = Con (Descr [ ])
toSpine (x : xs) = (App (App (Con (Descr (:))) x) xs)
在上述程式碼中可見,toSpine 函數目前可用來轉換 Int、Char、List 此三種資料型態,
在將 toSpine 函數欲操作的資料型態各別定義好後,我們便可以 type class 輔助去定義一 個泛型函數 strings,由於 strings 函數的作用是用來擷取所有資料型態中出現的字串值,
若資料型態為字串則回傳包含該字串的串列,因此我們需要針對引數為 String 型態執行 額外處理
class Strings a where
strings ::ToSpine a => a→[String]
instance Strings String where strings x = [x]
instance Strings a where
strings x = strings‟ (toSpine x)
strings‟ :: Spine a→[String]
strings‟ (Con c) = [ ]
‧
示中的”abc”與”def”字串值,然而在 strings 遞迴處理的過程中,並沒有正確呼叫 String ->[String]此型態的 strings 函數,卻是呼叫了 a -> [String]型態的 strings 函數,因此字串 值”abc”與”def”皆會被視為串列處理,造成 strings 輸出的結果不正確。
4-2-2 以 AspectFun 輔助實現泛型程式設計的實作與問題
基於在 Hasekll 中使用 type classes 輔助實現的泛型作法的錯誤結果,我們試圖使用 AspectFun 剖面機制去解決此問題,並認為以剖面實現的泛型方法相較於 type class 更為 直覺,並且可以避免函數擴充時的修改問題,然而 AspectFun 也發生了相同的結果錯誤 問題 :
strings :: a -> [String]
strings x = strings‟ (toSpine x) in strings‟:: Spine a -> [String]
strings‟ (Con c) = [ ]
strings‟ (App f x) = strings‟ f ++ strings x in
n@advice around {strings} (arg :: String) = [arg] in
在上述程式片段中,我們以 type-scoped advice n 處理當引數型態為 String 時的例外工 作。在 AspectFun 中,我們期望每次 strings 被呼叫時皆會先觸發 advice n 判斷引數型態,
但實際執行的情況在 strings‟定義中呼叫 strings 時並沒有觸發 advice n,所以在 AspectFun 上也發生 type class 的問題。
歸納兩者的原因,我們發現造成問題的原因是由於 Spine a 定義中使用了 Existential
歸納兩者的原因,我們發現造成問題的原因是由於 Spine a 定義中使用了 Existential