• 沒有找到結果。

第二章 背景介紹

2.2 AspectFun 介紹

2.2.4 data+

抽象時(general),此 advice 便會織入至函數中,一旦函數被呼叫時 advice 便會被觸 發。

2.2.3 Cflow pointcut

AspectFun 也提供了類似 AspectJ 的 cflow 功能,cflow 是指程式執行時流程控制,其 目的近似於 type-scoped advice,限制程式在某個特定的執行流程下才會觸發 advice,

如 :

AspectFun 提供了相當具有彈性的 cflow 機制與語法,我們也可改寫原來的 pointcut 定義為 f – cflow(g),限制 advice 在 f 被呼叫時且 g 不被執行的狀況下才會觸發。

2.2.4 data+

基於對舊有程式的擴充需求,除了一般函數式語言的資料型態宣告方式,AspectFun 提

‧ 國

立 政 治 大 學

N a tio na

l C h engchi U ni ve rs it y

供了一特殊的資料型態宣告方式 data+,data+的目的在減少程式設計者在擴充新的資料 型態定義時直接修改舊有資料定義的程式碼 :

data Shape = Circle Int in

drawShape :: Shape -> ( )

drawShape (Circle r) = drawCircle r in

在上述範例中可見,程式碼中定義了一資料型態 Shape 及函數 drawShape,drawShape 可將不同的 Shape 輸出至螢幕顯示,由於目前只定義了一種 Shape 為 Circle,當程式設 計者想要對原有的程式繼續擴充時,使用 data+可以避免程式設計者直接修改舊有的資 料型態定義 :

data+ Shape = …| Square Int Int in

由於原有的資料型態 Shape 現在以被擴充增加了 Square,原有的函數定義也必頇同樣跟 著擴充,否則便會造成程式在執行時發生錯誤,在 AspectFun 中可以藉由宣告 advice 來 達到擴充函數定義的目的 :

square@advice around {drawShape} (arg) = case arg of

Square w h -> drawSquare w h _ -> proceed arg in 2.2.5 AspectFun 的織入系統介紹

AspectFun 是一個靜態織入的剖面導向語言,靜態織入是指程式在編譯過程中就要決定 advice 是否要被觸發,例如一個 AspectFun 程式 :

n@advice around {f} (arg) = proceed arg in f x = x in

g x = f x in (g 5, g „a‟)

n proceed arg = proceed arg f x = x

g x = (n f) x

main = do putStrLn $ show $ (g 5, g „a‟)

其中 advice n 將會被轉換為一高階函數宣告,proceed 是高階函數的參數,目的在讓 advice 定義中所使用的 proceed 保留字可以被代換為 pointcut 中的函數,arg 參數的作用類似 proceed。在原先的 AspectFun 程式中,每次呼叫函數 f 將會觸發 advice n,即 n 會織入 至 f 中,在轉換後的 Haskell 程式碼的函數 g 定義中,((n f) x)即是表示 n 織入至 f 中,

而這就是 AspectFun 用來實現靜態織入的程式碼轉換方法。

除了一般的 advice 宣告,我們知道 AspectFun 也提供了 type-scoped advice 宣告控制 advice 的觸發條件,由於 type-scoped advice 必頇依據函數引數的實際型態判斷織入與 否,因此在編譯過程中無法直接將所有的函數 f 呼叫一律轉換為(n f) ,如 :

n@advice around {f} (arg :: Char) = proceed arg in f x = x in

g x = f x in (g 5, g „a‟)

在上述程式中,由於 g 的型態是 a -> a,而 advice n 只有當引數為 Char 型態時才會被觸 發,在推導至 g 時,我們只知道 g 的引數是某個任意型態 a,無法判斷此時呼叫 f 是否 會觸發 advice,因此我們需要型態資訊來輔助判斷函數是否要被織入,而 predicate 就是 AspectFun 在輔助用來判斷 advice 是否織入的額外型態資訊。

當程式中使用 type-scoped advice 時,由於某些函數定義的型態較 type-scoped advice 定義的型態較為抽象,在推導至此函數無法立即判斷織入與否,此時函數會形成 predicate,直到推導至其他函數或程式主體確定型態資訊足夠時,才判斷是否織入。在 前述程式中,由於 f 的型態是 a -> a,暫時無法判斷 n 是否織入,此時 f 會形成 predicate,

而 g 的定義中會呼叫 f,因此 g 會帶著 predicate f,直到推導至程式主體時確定 f 的引數

‧ 國

立 政 治 大 學

N a tio na

l C h engchi U ni ve rs it y

值是為 Char 型態,才對 f 進行織入。此 AspectFun 程式最後會被轉換為下列可執行的 Haskell 程式碼 :

n proceed arg = proceed arg f x = x

g df x = df x

main = do putStrLn $ show $ (g f 5 , g (n f) „a‟)

其中 g 帶著 predicate f,因此原來的函數 g 定義會被轉換為 g df x = df x,在推導至程式 主體中的 g „a‟時知道 f 會觸發 advice n,便傳遞織入過的 f 函數(n f);在程式主體中的 g 5 不符合 n 被觸發的條件,因此只傳遞未被織入的 f。

3.1 The Expression Problem

對於各程式語言不同的程式建構方法,我們需要一個公認有效的衡量標準(benchmark) 來評估該程式語言程式建構機制的優劣,而 Expression Problem 就是個經常被用來衡量 程式語言建構機制優劣的問題。Expression Problem 是指在實作一個程式語言時,因為 data 及 operation 兩個方向在擴充的過程中會因為程式語言的擴充機制受限,導致程式設 計者需要修改舊有程式碼的問題。Expression Problem 所要解決的問題在於:

 擴充新的 data / operation 時,舊有的程式碼不需修改和重新編譯, 新的程式 碼不會重複舊有程式碼。

 程式碼在通過編譯器的型態推導系統後,在執行期間(runtime)不會產生型態錯 誤。

在物件導向語言中,我們透過繼承便可輕易擴充新的 data,而擴充新的 operation 卻需要修改所有相關的類別(class),而 Visitor Pattern 的作法是將類別中的方法( Method) 抽離至外部成為 Visitor 介面的一種設計方法,針對需要新增的 operation 程式設計者只 需定義新的類別並實作 Visitor 介面即可擴充 operation,在 data 不變動的前提下使用 visitor pattern 有助於程式的模組化建構,但使用 Visitor Pattern 也相對限制了物件導向語 言擴充 data 的簡易性;而函數式語言則與物件導向語言相反,擴充 operation 容易但擴 充 data 時則需要修改原有程式碼,如下列範例程式中:

module Lang where data Expr = Num Int

|Plus Expr Expr

eval (Num i) = i

eval (Plus e1 e2) = (eval e1) + (eval e2)

module Main where import Lang

toString (Num i) = inttostring i

toString (Plus e1 e2) = toString e1 ++ “ + ” ++ toString e2

3.2 Open datatype / Open function

針對函數式語言因擴充 data 時造成已存在程式必頇修改的問題,相關研究[14]提出了 open datatype 及 open function 概念,data 與 function 若被宣告為 open,程式設計者可在 不修改原有模組的情況下擴充已存在的 data 與 function 的定義,但此方法需要修改 Haskell 語言才可實現 open 機制。在此研究中也提及另一種實作方式試圖以 Haskell 現 有的程式機制解決 data 擴充問題,它的作法是將 Expr 中的每個資料建構函數(data constructor)提升為獨立的資料型態(datatype):

‧ 國

立 政 治 大 學

N a tio na

l C h engchi U ni ve rs it y

data Plus a b = Plus a b

由於 Num 與 Plus 經過改寫後已成為不同的型態,原有的 eval 函數也必頇將函數型態放 寬為 a  Int,在 Haskell 中的 type class 機制提供程式設計者定義一組多載(overloading) 函數,將 Num 及 Plus 宣告為 class Eval 的 instance,因此 eval 可以操作這兩種不同型態 的資料

class Eval a where eval :: a -> Int

instance Eval Num where eval (Num i) = i

instance (Eval a, Eval b) => Eval (Plus a b) where eval (Plus a b) = eval a + eval b

每當需要增加新的 operation 時,只需要定義此 operation 的 class 及其 instances class ToString a where

toString :: a -> String instance ToString Num where

…(略)

instance (ToString a, ToString b) => ToString (Plus a b) where

…(略)

使用此實作方式雖可解決 data 擴充問題,且透過定義新的 instance 也避免了在 data 擴充 後相關函數定義需要重新修改的狀況

data If a b = If a b b

instance (Eval a, Eval b) => Eval (If a b) where eval (if e1 e2 e3) = …(略)

‧ 國

立 政 治 大 學

N a tio na

l C h engchi U ni ve rs it y

3.3 AspectFun 的程式擴充機制在 Expression Problem 的應用

我們已知函數式語言在處理 Expression Problem 時的兩個問題點

 當 data 需要擴充時,data 定義的程式碼部分就需要被修改

 在 data 擴充後,用來操作該 data 的相關函數也需要進行修改

在 3.2 節中提到了使用個別獨立的 datatype 及 type classes 克服函數式語言不易增加 data 的困難,我們希望藉由 AspectFun 解決 Expression Problem。

針對資料型態 Expr 的部分,我們參考研究[14]中所提出的資料建構函數提升為獨立 的資料型 :

data Num = Num Int in

data Plus = Plus a b in

針對函數擴充部分,在 Haskell 中我們知道使用 type class 機制可以擴充新的函數,

然而 AspectFun 中並不支援 type classes 機制,我們無法使用 type class 來擴充函數定義。

若將 type classes 中的 class 視為物件導向語言中的抽象類別(abstract class),且將 instances 視為繼承此抽象類別的子類別(subclass)並實作其父類別所定義的抽象方法(abstract method),我們引用此繼承概念並透過 AspectFun 提供的 type-scoped advice 機制,模擬 這物件導向語言常見的實作方法。首先將 eval 函數定義為 undefined

eval :: a -> Int eval e = undefined in

此未定義的 eval 函數可視為是一抽象類別,undefined 為 AspectFun 內建的函數,此時 若呼叫 eval 函數將會產生執行錯誤(runtime error)。接著透過 advice 來擴充 eval 函 數的定義,num 與 plus advices 在此處可視為是繼承 eval 的兩個子類別

num@advice around{eval}(e :: Num) = case e of {Num i -> i;

‧ 國

立 政 治 大 學

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 來修正

就可不修改 csum 定義而實現程式碼重用的目的。在剖面導向程式語言中我們常 會使用 cflow 來控制剖面模組影響的範圍,所以我們可以藉由 cflow 來修正

相關文件