haskell 求列表最大值_Haskell和自然数之基础篇
對自然數的理解,是隨著自己的成長而不斷深入的。在小學的時候覺得很自然就理解了,很自然就用起來了,加、減、乘和整除很自然就學會了,感覺沒有什么障礙。到了初中的某一天,突然想到一個問題:1 + 1為什么就是等于2呢?沒有理由的就指定了是2,沒有推導和證明的過程,感覺很不自然。于是自己思考了好幾個月,覺得似乎想通了,寫了一篇文章,然后被一些同學嘲笑了。現在也想不起來當時寫的是什么了,那篇文章也不知道遺失到哪里去了,不過應該還是沒有寫清楚究竟為什么1 + 1等于2,要不然我是不會忘記寫的是什么的。于是這個令人疑惑的問題一直困擾著我,一直到參加工作,也依然時不時會惦記著這個問題。
直到我學習了Haskell,看到一篇關于自然數的表示文章,用Haskell清晰的定義了自然數,定義了自然數的加法和乘法。我終于明白了1 + 1為什么就是等于2,這個從自然數的定義和加法的定義很自然就可以推導得到了,證明起來很容易。在這之后,又看了皮亞諾公理的自然數定義,對自然數的定義更加清楚了。在這之前,我聽說過皮亞諾公理,但是并不感興趣,還感受不到自然數公理化的意義,所以并沒有去看。
大概兩個月前,我收到了劉新宇的新書《同構--編程中的數學》,看到了這本書中對自然數的論述,然后又重溫了丘奇數的概念。覺得可以寫點關于自然數的東西了。這是一個系列,有兩篇文章:第一篇講自然數和丘奇數的基礎概念和構造,以及在其上的基本運算,第二篇講自然數的變換,結合F-Alg來講如何消除自然數的結構得到其他的類型的值。
好了,讓我們從一無所知的狀態來開始了解什么是自然數吧。我們最早了解自然數是從數數開始的,當我們不知道桌上一堆東西有多少個時,最簡單的辦法就是數一數有多少個。數一下手指頭是1 個,數兩下手指頭是2 個,數三下手指頭是3 個,這樣一直數下去,直到數完了這堆東西。于是我們就得到了一系列的數:1, 2, 3, ...,這些數和數手指頭的次數的對應關系如下。
1 : 數一下手指頭當桌上沒有東西的時候,我們就不用數手指頭了,因為什么都沒有,所以什么也不用做。這個時候我們用0 個來表示桌上東西的數量。于是有下面這個新的數和數手指頭的次數的對應關系。
0 : 什么也不做因為我們一無所知,就像幼兒園的小朋友一樣,還弄不明白兩下、三下是怎么來的,是什么意思。我們再來看一遍我們數數的過程,一開始是什么也不做,然后將一個東西擺到桌子的另一邊,做一次數手指頭的動作,再將一個東西擺到桌子的另一邊,再做一次數手指頭的動作,這樣每將一個東西擺到桌子的另一邊,我們都接著做一次數手指頭的動作,直到把桌子上的東西數完。于是我們就得到一個數手指頭的動作的序列,這個數手指頭動作的序列的次數就是東西的個數。因此有下面的數和數手指頭的動作序列的對應關系。
0 : 什么也不做我們把上面的什么也不做用O 來表示,把數手指頭這個動作用S 來表示,于是上面的數和數手指頭的動作序列的對應關系就變成了下面這樣。
0 : O我們可以這樣認為,最開始存在一個自然數O,然后我們開始做一個數手指頭的動作,就得到一個新的自然數S O,再做一個數手指頭的動作,又得到一個新的自然數S (S O)。再繼續下去,我們就得到了自然數的序列:O, S O, S (S O), S (S (S O), ...。我們用N 來表示所有自然數的集合,也就是自然數類型,這樣每做一次數手指頭的動作,我們就得到了一個自然數n ∈ N 的后繼S n,這也是一個自然數。我們可以使用Haskell來定義自然數:
data于是我們就得到了所有的自然數。我們可以通過皮亞諾公理來驗證這一點,皮亞諾公理的表述如下:
1. 0 是自然數。
2. 每個自然數都有它的下一個自然數,稱為它的后繼。
3. 0 不是任何自然數的后繼。
4. 不同的自然數有不同的后繼數。
5. 如果自然數的某個子集包含 0,并且其中每個元素都有后繼元素。那么這個子集就是全體自然數。
這里得到自然數的后繼用動作S 來表示,也可把S 看成是自然數集合上的自函數。皮亞諾公理確保了0(也就是我們前面用O來表示的數)是第一個自然數,然后通過不停的獲取自然數的后繼,我們就得到了所有的自然數。其中皮亞諾公理的第4條確保了S 是一個單射,第5條確保了S 是一個滿射,因此S 是一個自同構(這一點在下一篇中會用到)。
另外第5條公理還有如下的等價描述:
任意關于自然數的命題,如果證明了它對自然數 0 是對的,又假定它對自然數 n 為真時,可以證明它對 n的后繼n′ 也真,那么命題對所有自然數都真。這保證了數學歸納法的正確性,使得自然數上可以有歸納函數。因此也叫歸納公理。
我們有了嚴格定義的自然數,現在可以在這個定義的基礎上定義加法、乘法和冪運算了。我們使用Haskell來定義這些運算:
-- | 先定義幾個基本函數,用于給后面的運算定義使用自然數上的歸納函數的定義是對于自然數n ,對其進行歸納的初始值是z ,每一個歸納步調用函數step 。自然數n 是由幾個S 構造的,我們就以z 為參數遞歸調用幾次step 函數。比如當自然數n 的值是S (S (S O)時,這是由3 個S 構造的,于是我們就遞歸調用3 次step 函數,于是結果是step (step (step z)) 。
對于加法,我們是這樣定義的,給定一個自然數m,加上一個值為S (S O)的自然數n 時,其結果等于值為S (S m)的自然數。用歸納函數來定義就相當于初始值z 是m ,step 是S 也就是succN ,我們對加數n 做歸納法,也就是自然數n 中由幾個S 構造的,我們就遞歸調用幾步S 。于是有了結果的值是S (S m)。
當m 的值是S O 也就是1 ,n 的值也是S O 即1 時,我們有(S O) + (S O) = S (S O),根據上面的數的對應關系,我們有1 + 1 = 2 。完成了證明,解決了我多年來的疑問。
對于乘法,兩個自然數m 和n 相乘,就相當于把n 個m 加起來。用歸納函數來定義就相當于初始值是O,step 是(m +) 也就是plus m ,我們對乘數n 做歸納法。于是乘以一個值為S (S O)的自然數,我們就遞歸調用兩步遞歸步plus m,于是得到結果值是plus m (plus m O),就是m + m。
對于冪運算,則和乘法類似,自然數m 的n 次冪的值就等于把n 個m 乘起來。用歸納函數來定義就相當于初始值是S O,step 是(m *) 也就是mult m ,我們對冪數n 做歸納法。于是求m 的一個值為S (S O) 的n 次冪的自然數的值,我們就遞歸調用兩步遞歸步plus m,于是得到結果值是mult m (mult m (S O)),就是m * m。
減法的定義比較復雜,因為自然數沒有負數,因此需要比較兩個數的大小來實現減法,這個放到后面一起來定義。
用皮亞諾公理來定義的自然數只是自然數表示的一種形式,我們可以用其他同構的形式來定義和表示自然數。接下來我們將使用列表和函數來表示自然數。
- 用列表表示自然數
列表是包含了同類型元素的一種數據類型,多個同類型的數據列在一起,就組成了列表。當列表內的元素的類型是 () 時,列表就只剩下長度信息了,我們可以用其來表示自然數。
Haskell中列表的定義如下:
data列表的類型是[a],當列表內的元素的類型也就是a 為() 時,我們有一個特殊的列表類型[()]。這個列表類型的元素是無具體信息的,其有用的信息就是列表的長度,因此我們可以使用列表類型[()]來表示自然數。比如用[(), (), ()] 來表示皮亞諾形式的自然數S (S (S O))。列表表示的自然數和數的關系如下:
0我們使用Haskell來定義如下的用列表類型[()] 表示的自然數運算。
-- | 先定義幾個基本函數,用于給后面的運算定義使用列表上的歸納函數的定義是對于列表n,對其進行歸納的初始值是z ,每一個歸納步調用函數step 。列表n 是由元素組成的,我們就以z 為參數遞歸調用幾次step 函數。比如當列表n 的值是[(), (), ()] 時,這是由3 個元素組成的,于是我們就遞歸調用3 次step 函數,于是結果是step (step (step z)) 。
對于加法,直接用兩個列表的連接運算來定義。即將兩個列表m 和n 連接起來就實現了列表的加法。
對于乘法,兩個列表m 和n 相乘,就相當于把n 個m 加起來。用歸納函數來定義就相當于初始值是[],step 是(m +) 也就是plus m ,我們對列表n 做歸納法。于是乘以一個值為[(), ()] 的列表,我們就遞歸調用兩步遞歸步plus m,于是得到結果值是plus m (plus m []),就是將兩個列表m 連接起來。
對于冪運算,則和乘法類似,列表m 的n 次冪的值就等于把n 個m 乘起來。用歸納函數來定義就相當于初始值是[()],step 是(m *) 也就是mult m ,我們對冪數n 做歸納法。于是求m 的一個值為[(), ()] 的n 次冪的自然數的值,我們就遞歸調用兩步遞歸步plus m,于是得到結果值是mult m (mult m (S O)),就是m * m。
- 用函數表示自然數(丘奇數)
在純函數編程語言中(比如Haskell),函數也是一個值,因此我們也可以用函數來表示自然數。這種表述方式時阿隆佐.丘奇發明的,因此也叫丘奇數。
我們知道,函數是可以組合起來,即我們可以把函數f 和g 組合起來得到g . f ,這里運算符 . 就是組合運算(按普遍的定義,是反序的)。那我們把相同的兩個函數f 組合起來就得到了f . f,把三個函數f 組合起來就得到了f . f . f,以此類推,我們就可以得到n 個函數f 的組合f . f . f . f ...。我們可以用函數f 的組合來表示自然數,由幾個函數f 組合的函數就表示自然數幾,比如用f 表示S O ,用f . f 表示S (S O) ,用f . f . f 表示S (S (S O))。至于自然數O ,則用函數id 來表示。丘奇數和數的對應關系如下:
0于是就有了如下使用Haskell來實現的自然數的函數表示的定義和基本運算。
-- | 用函數來表示自然數的數據類型,就是給定一個函數f得到多個函數f的組合函數。我們用一個新的數據類型Church來定義丘奇數,這實際上就是以函數f 為參數得到多個函數f 組合的函數的lambda函數的封裝類型,其本質就是一個lambda函數,這個lambda函數的返回結果是多個函數f的組合。
當類型Church的lambda的函數參數是(+1) 時,如果這個丘奇數表示的是自然數S (S (S O)),那lambda函數返回的結果是(+1) . (+1) . (+1),也是一個函數,將這個函數應用到參數0,我們得到了3。可以看到類型Church(丘奇數)本身的定義就是歸納的,因此其歸納函數iter 的實現就是將歸納步step 直接作為參數傳遞給類型Church的lambda函數,然后將結果函數應用到初始值z ,就得到了歸納函數iter 的結果。
因為丘奇數本身的定義就是歸納的,所以我們就不需要用歸納法來實現加法了,直接用Church本身的定義來實現加法就可以了。比如當丘奇數m 的值為Church (f -> f . f . f),丘奇數n 的值為Church (f -> f . f) 時,m 加上n 的丘奇數的lambda函數返回的結果是(f . f . f . f . f),也就是Church (f -> (f . f) . (f . f . f)),因此加法就是由函數的組合運算來實現。
類似的,丘奇數的乘法也使用其本身的定義來實現。當丘奇數m 的值為Church (f -> f . f . f),丘奇數n 的值為Church (f -> f . f) 時,m 乘以n 的丘奇數的lambda函數返回的結果是(g -> g . g) (f . f . f),得到Church ((g -> g . g) . (f -> f . f . f)),結果是Church (f -> (f . f . f) . (f . f . f)),因此乘法就是由丘奇數的lambda函數的組合來實現的。
最后,丘奇數的冪運算也可以使用其本身的定義來實現。當丘奇數m 的值為Church (f -> f . f . f),丘奇數n 的值為Church (f -> f . f) 時,m 的n 次冪的丘奇數的lambda函數返回的結果是(g -> g . g) (h -> h . h . h),得到Church (f -> ((g -> g . g) (h -> h . h . h)) f),將g 替換為(h -> h . h . h) 有Church (f -> ((h -> h . h . h) . (h -> h . h . h)) f),結果是Church (f -> (f . f . f) . (f . f . f) . (f . f . f)),因此冪運算就是將一個丘奇數的lambda函數應用到另一丘奇數的lambda函數的方式來實現的。
丘奇數和前面兩個自然數表示形式所不同的是丘奇數的前驅的實現比較難,不像皮亞諾形式的和列表形式的那么簡單直觀。
我們在前面已經說過,丘奇數的前驅就是從由n 個函數f 組合成的函數中去除一個函數f ,變為由n-1 個函數f 組合成的函數。比如丘奇數的lambda 返回結果是f . f . f,這個丘奇數的前驅的lambda 返回的結果是f . f。最簡單的實現就是找到函數f 的反函數
,于是有 . f . f .f 等于f . f。但是我們沒有辦法在Haskell中找到任意一個函數的反函數,看來這個實現方式是行不通的。那既然我們做不到逆轉世界,那停止世界是可以的,我們可以使用const x 函數來停止世界,將x -> f -> f x 用為x -> f -> x 即x -> const x 來替換,就去除了一次函數f 的作用,相當于沒有調用過函數f 。順著這個思路,我們于是有了如下這個丘奇數前驅的實現。-- 前驅函數,從n個函數f的組合得到n-1個函數f的組合 predChurch n = Church $ f x -> runChurch n (g h -> h (g f)) (const x) id具體的證明就留給聰明的讀者吧。
- 自然數的減法和整除的實現
有了自然數的前驅函數,我們就可以實現減法了。前面說過,自然數沒有負數,所以我們需要可以比較兩個自然數,當自然數m 小于自然數n 時,m - n 的結果是0 。
我們可以將自然數實現為Eq 和Ord 類型類的實例,就可以比較兩個自然數了。Haskell的實現如下:
instance我們通過自然數的前驅來實現減法,皮亞諾形式的自然數減法實現如下所示:
minus列表形式的自然數減法實現如下所示:
minus丘奇數的減法實現如下所示:
minus自然數的整除就是通過減法來實現的,具體如下所示:
divide列表形式的自然數和丘奇數使用類似的方式,具體實現就留給讀者了。
至此,我們從最開始的一無所知的狀態一步一步的定義了什么是自然數,然后定義了其上的加、減、乘、整除和冪運算這些基本操作,證明了1 + 1 = 2 這個命題。我們還介紹了自然數的其他兩種同構形式的自然數定義,即列表表示的自然數和丘奇數。
有興趣的讀者可以等待下一篇:Haskell和自然數之代數篇。
參考鏈接:
總結
以上是生活随笔為你收集整理的haskell 求列表最大值_Haskell和自然数之基础篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 可以装linux的路由器,[转载]lin
- 下一篇: mysql运维机制_《MySQL运维内参