[PLT] 柯里化的前生今世(十三):Weak head normal form

1. 形式系統(Formal system)

在邏輯學與數學中,一個形式系統由兩部分組成,一個形式語言加上一套推理規則

一個形式系統也許是純粹抽象地制定出來的,只是為了研究其自身。
也可能是為了描述真實現象或客觀事實而設計的。

2. λ演算(λ-caculus)

λ演算用于研究函數定義、函數應用和遞歸,它是一些形式系統的總稱,
配備不同的推理規則集,就會得到不同的演算系統。

λ演算由Alonzo Church和Stephen Cole Kleene在20世紀三十年代引入,
Church在1936年證明了,兩個λ表達式是否等價的問題,是不可判定的。
這是第一個被證明的不可判定問題,甚至早于停機問題

λ演算對函數式編程有巨大的影響。


1.1 λ項(λ-terms)

采用BNF,λ項的文法可以描述如下,

M ::= x | MM | λx.M

注:為行文簡便,這里省略了()

它表明,合法的表達式,要么是一個變量:x
要么是一個函數調用(application):MM
要么是一個函數抽象(lambda abstraction):λx.M

例如:這些表達式是合法的,x(λx.x)5λx.y

1.2 α轉換(α-conversion)

α轉換是一種推理規則,它基于以下事實,
函數的形參只是占位符,替換形參和函數體中相應的符號,所產生的新表達式與原表達式等價。

例如,經過α轉換之后,

λxy.x(xy) ≡ λuv.u(uv)

易見,α轉換是一個等價關系(自反的,對稱的,傳遞的)。

1.3 β歸約(β-reduction)

函數調用表達式,可以化簡,結果為函數體中的形參替換成實參后的表達式。
例如,(λx.x(xy))N可以一步β歸約為N(Ny)
(λx.(λy.yx)z)v可以兩步β歸約為zv
(λx.xx)(λx.xx)可以無限制的進行β歸約。

通常我們把一步或者多步β歸約,簡稱為β歸約。
如果某一λ項不可再進行β歸約,就稱該項為β范式(β-normal form)。

如果某一λ項可以β歸約為兩個不同的項,
那么,這兩項必定可以再β歸約為同一項,這種性質稱為匯聚性(confluence)。

2. 惰性求值

大多數編程語言采用的策略是嚴格求值(strict evaluation),
即,求值子表達式總是在復合表達式之前進行,
或者說,在進入函數體之前,實參需要先求值。
例如:

head [3+2, 7*5] => head [5, 35] => 5

如果采用了這種求值策略,列表的長度就必須是有限的,
調用函數head之前,必須先確定列表中的每一個元素。

Haskell的實現ghc,并沒有采用這種求值策略,它希望求值一個表達式越晚越好。
在這個例子中,ghc并不會先確定列表元素的值,
而是直接調用head,得到一個尚未被求值的列表元素3+2

而后,因為我們要在屏幕上顯示結果,所以迫使3+2必須被求值,顯示為5
這種求值方式,被稱為惰性求值(lazy)。

2.1 WHNF(weak head normal form)

data MyList a = Empty | Prepend a (MyList a)
    deriving Show

infiniteNumbers :: MyList Int
infiniteNumbers = createInfiniteNumbers 1
    where
        createInfiniteNumbers n = Prepend n (createInfiniteNumbers (n + 1))

myHead :: MyList a -> a
myHead Empty = error "empty list"
myHead (Prepend x _) = x

現在我們來計算myHead infiniteNumbers

『希望求值一個表達式越晚越好』并不是一件簡單的事情,
因為即使infiniteNumbers不事先求值,在帶入myHead之后,還是不得不求值它,
仍然會導致createInfiniteNumbers無限遞歸。

其實,ghc在求值表達式時,并不會一次性的求值到底,
而是每次只將一個表達式求值到它的WHNF(weak head normal form),
即,求值到最外層的值構造器或者λ抽象為止。

值構造器以及λ抽象內部,就不會被求值了,
未被求值的部分用占位符來表示,稱為thunk
ghc會記錄多個相同thunk的不同引用,使得這些相同thunk只會被求值一次。

2.2 求值過程

myHead infiniteNumbers

我們來看上式的求值過程:
(1)myHead infiniteNumbers這個表達式是一個thunk,由于我們要在屏幕上顯示它的值,所以不得不求值它。
(2)我們需要將上述thunk求值為WHNF,于是,將infiniteNumbers保存為另外一個新的thunk,調用函數myHead
(3)myHead會對參數進行模式匹配,因此參數不得不被求值。

(4)infiniteNumbers求值會導致createInfiniteNumbers 1被求值。因為,只需求值到WHNF,所以不會引起無限遞歸。
(5)結果為Prepend 1 (createInfiniteNumbers (1 + 1)),它是一個WHNF。其中,Prepend可被用于模式匹配,而1createInfiniteNumbers (1 + 1)都是thunk。
(6)現在myHead就可以對參數進行匹配了,myHead (Prepend x _) = x滿足匹配條件,x匹配到了1,于是myHead返回了1,注意1還是一個thunk。

(7)為了把1這個thunk顯示出來,繼續求值,結果為數字1

注:
只有infiniteNumbers的值被需要的時候,才會調用createInfiniteNumbers 1
也就是說,thunk可以不是weak head normal form,
但是如果thunk被求值,其結果一定是weak head normal form。

因此,在這個例子中,調用myHead infiniteNumbers之前,
infiniteNumbers是未求值的,
即使是,它所對應的createInfiniteNumbers 1不是一個weak head normal form。

2.4 seq

為了對求值進行控制,ghc內置了seq函數。

ghci> :t seq
ghci> seq :: a -> b -> b

它首先將第一個參數求值為WHNF,然后返回第二個參數。
例如:

ghci> let x = 1 + 2 :: Int
ghci> let y = (x, x)

ghci> let u = 1 + 2 :: Int
ghci> let v= seq u (u, u)

ghci> let f (_, _) = 0
ghci> f y
ghci> f v

ghci> :sprint x
x= _

ghci> :sprint u
u = 3

注:
:sprint是ghci提供的功能,用于顯示表達式的結果,但不會對它求值。

3. 參考

形式系統
Lambda-Calculus and Combinators
Beginning Haskell
Parallel and Concurrent Programming in Haskell
:sprint for polymorphic values
GHCi :sprint has odd/unhelpful behavior for values defined within the REPL

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容