跳轉到內容

程式語言簡介/評估策略

來自華夏公益教科書

評估策略

[編輯 | 編輯原始碼]

程式語言採用的引數評估策略定義了在函式呼叫期間何時評估引數。主要有兩種策略:嚴格評估和惰性評估。

嚴格評估

[編輯 | 編輯原始碼]

嚴格評估策略是在將引數傳遞給函式之前完全評估引數。兩種最常見的評估策略:按值呼叫和按引用呼叫,都屬於此類。

按值呼叫: 按值呼叫策略是指將實際引數的內容複製到形式引數中。在形式引數中執行的狀態更改不會反映回實際引數。以下在 C 中實現的交換函式就是一個著名的此類行為的例子

void swap(int x, int y) {
  int aux = x;
  x = y;
  y = aux;;
}
int main() {
  int a = 2;
  int b = 3;
  printf("%d, %d\n", a, b);
  swap(a, b);
  printf("%d, %d\n", a, b);
}

一旦呼叫 swap 函式,變數 a 和 b 的內容分別被複制到形式引數 x 和 y 中。在 swap 函式體中發生的資料交換隻影響形式引數,而不是實際引數。換句話說,在這個程式中,swap 的呼叫是無害的。為了規避這種語義,C 語言允許我們使用指標傳遞一個位置的地址,而不是它的內容。因此,下面的函式按預期交換了兩個變數的內容

void swap(int *x, int *y) {
  int aux = *x;
  *x = *y;
  *y = aux;
}
int main() {
  int a = 2;
  int b = 3;
  printf("%d, %d\n", a, b);
  swap(&a, &b);
  printf("%d, %d\n", a, b);
}

按值呼叫策略在程式語言中非常常見。它是 C、Java、Python 甚至 C++ 中的首選策略,儘管 C++ 也支援按引用呼叫。

按引用呼叫: 在按值呼叫策略中,我們將實際引數的內容複製到形式引數中,而在按引用呼叫策略中,我們將實際引數的地址複製到形式引數中。一些語言實現了按引用呼叫策略。C++ 就是其中之一。以下程式重新實現了 swap 函式,使用按引用呼叫策略

void swap(int &x, int &y) {
  int aux = x;
  x = y;
  y = aux;
}
int main() {
  int a = 2;
  int b = 3;
  printf("%d, %d\n", a, b);
  swap(a, b);
  printf("%d, %d\n", a, b);
}

在 C++ 中,按引用傳遞引數是 語法糖,用於使用指標。如果我們仔細研究一下 g++(C++ 編譯器)為上面的 swap 函式和帶有指標的 swap 函式生成的彙編程式碼,我們會發現它們是完全相同的。

如果傳遞給函式的資料結構的大小很大,按引用呼叫可能比按值呼叫更快。然而,這種策略在目前的主流語言中並不存在,除了 C++。按引用傳遞引數可能會導致難以理解的程式。例如,下面的函式也實現了 swap 函式;但是,它結合了三個 異或 操作來避免需要輔助變數。

void xor_swap(int &x, int &y) {
  x = x ^ y;
  y = x ^ y;
  x = x ^ y;
}

如果形式引數 x 和 y 引用了同一個位置,這個函式可能會導致意想不到的結果。例如,以下程式使用 xor_swap 實現,將實際引數置零,而不是保留其值

int main() {
  int a = 2;
  int b = 3;
  printf("%d, %d\n", a, b);
  xor_swap(a, a);
  printf("%d, %d\n", a, b);
}

惰性評估

[編輯 | 編輯原始碼]

嚴格評估策略強制在將實際引數傳遞給被呼叫函式之前對其進行評估。為了說明這一點,以下在 Python 中實現的程式將迴圈。

def andF(a, b):
  if not a:
    return True
  else:
    return b

def g(x):
  if g(x):
    return True
  else:
    return False

f = andF(False, g(3))

有一些引數傳遞策略不需要在將引數傳遞給被呼叫函式之前對其進行評估。這些策略被稱為惰性。三種最著名的惰性策略是按宏展開呼叫按名呼叫按需呼叫

按宏展開呼叫: 許多程式語言,包括 C、Lisp 和 Scheme,為開發者提供了一種機制,可以在核心語言語法中新增新的語法,稱為 。宏由一個宏 預處理器擴充套件為程式碼。這些宏可能包含引數,這些引數在預處理器生成的最終程式碼中被複制。例如,以下 C 程式透過宏實現了 swap 函式

#define SWAP(X,Y) {int temp=X; X=Y; Y=temp;}
int main() {
  int a = 2;
  int b = 3;
  printf("%d, %d\n", a, b);
  SWAP(a, b);
  printf("%d, %d\n", a, b);
}

這個宏實現了一個有效的 swap 例程。預處理後的程式將類似於以下程式碼。因為宏體被直接複製到呼叫程式的文字中,所以它是在該程式的上下文中操作的。換句話說,宏將直接引用它接收的變數名,而不是它們的值。

int main() {
  int a = 2;
  int b = 3;
  printf("%d, %d\n", a, b);
  { int tmp = (a); (a) = (b); (b) = tmp; };
  printf("%d, %d\n", a, b);
}

傳遞給宏作為引數的表示式會在每次它們在宏體中使用時進行評估。如果引數從未使用,則它根本不會被評估。例如,以下程式將使變數 b 自增兩次

#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))
int main() {
  int a = 2, b = 3;
  int c = MAX(a, b++);
  printf("a = %d, b = %d, c = %d\n", a, b, c);
}

宏有一個問題,叫做變數捕獲。如果一個宏定義了一個在呼叫者環境中已經定義的變數 v,而 v 被作為引數傳遞給宏,那麼宏體將無法區分 v 的一個出現和另一個出現。例如,以下程式有一個定義變數 temp 的宏。main 中的呼叫會導致這個函式中定義的變數 temp 被宏體中的定義捕獲。

#define SWAP(X,Y) {int temp=X; X=Y; Y=temp;}
int main() {
  int a = 2;
  int temp = 17;
  printf("%d, temp = %d\n", a, temp);
  SWAP(a, temp);
  printf("%d, temp = %d\n", a, temp);
}

一旦這個程式被 C 預處理器擴充套件,我們將得到以下程式碼。這個程式無法交換變數 temp 和 a 的值

int main() {
  int a = 2;
  int temp = 17;
  printf("%d, temp = %d\n", a, temp);
  {int temp=a; a=temp; temp=temp;};
  printf("%d, temp = %d\n", a, temp);
}

有一些惰性評估策略可以避免變數捕獲問題。兩種最著名的技術是按名呼叫和按需呼叫。

按名呼叫: 在這種評估策略中,實際引數只會在函式內部使用時才進行評估;但是,這種評估使用的是呼叫者的上下文。例如,在下面的例子中,取自 韋伯的書,我們有一個返回整數 6 的函式 g。在函式 f 中,第一個賦值,例如 b = 5,將 5 儲存到變數 i 中。第二個賦值,b = a,讀取 i 的值(當前為 5),並對其加 1。然後,這個值被儲存到 i 中。

void f(by-name int a, by-name int b) {
  b=5;
  b=a;
}
int g() {
  int i = 3;
  f(i+1,i);
  return i;
}

很少有語言實現按名呼叫評估策略。其中最著名的語言是 AlgolSimula 是 Algol 的直接後代,也實現了按名呼叫,正如我們在這個 例子 中看到的。按名呼叫總是會導致引數的評估,即使這個引數被多次使用。這種行為在 引用透明 的語言中可能會造成浪費,因為在這些語言中,變數是不可變的。有一種評估策略可以解決這個問題:按需呼叫

按需呼叫: 在這種評估策略中,引數只會在使用時才進行評估。然而,一旦第一次評估發生,它的結果就會被快取,因此對引數的進一步使用不需要重新評估。這種機制提供以下三個保證

  • 表示式只有在呼叫函式需要其結果時才進行評估;
  • 表示式只會被呼叫函式需要評估的程度進行評估;
  • 表示式絕不會被評估超過一次,稱為應用順序評估。

Haskell 是一種以使用按需呼叫而聞名的語言。這種評估策略是語言設計者用來保持 Haskell 為純函式式語言的關鍵特徵。例如,按需呼叫允許語言將輸入通道模擬為無限列表,該列表只需要讀取資料時才進行評估。例如,以下程式計算 斐波那契數列 的第 n 項。然而,生成這個序列的函式 fib 沒有終止條件!

fib m n = m : (fib n (m+n))

getIt [] _ = 0
getIt (x:xs) 1 = x
getIt (x:xs) n = getIt xs (n-1)

getN n = getIt (fib 0 1) n

getIt 函式只擴充套件 fib 生成的列表,只要讀取它的第 n 個元素就足夠了。例如,以下是一系列計算斐波那契數列第 4 項的呼叫

getIt (fib 0 1) 4
= getIt (0 : fib 1 1) 4

getIt (fib 1 1) 3
= getIt (1 : fib 1 2) 3

getIt (fib 1 2) 2
= getIt (1 : fib 2 3) 2

getIt (fib 2 3) 1
= getIt (2 : fib 3 5) 1
= 2

引數匹配

華夏公益教科書