Haskell/更進一步
Haskell文件
[編輯]到現在,我們已經多次使用了GHC解釋器。它的確是一個有用的調試工具。但是接下來的課程如果我們直接輸入所有表達式是麻煩的。所以現在我們開始寫第一個Haskell源文件。
在你最喜愛的編輯器中打開一個文件Varfun.hs(hs是Haskell的簡寫)然後把下面的定義粘貼進去。Haskell使用縮進和空格來決定函數(及其它表達式)的開始和結束,所以確保沒有前置的空格並且縮進是正確的,否則GHC將報錯。
area r = pi * r^2
(為避免你的疑慮,pi
事實上已經被Haskell定義,不需要在這裡包含它了)。現在轉到你保存文件的目錄,打開ghci,然後使用 :load(或者簡寫為 :l):
Prelude> :load Varfun.hs Compiling Main ( Varfun.hs, interpreted ) Ok, modules loaded: Main. *Main>
如果ghci給出一個錯誤,"Could not find module 'Varfun.hs'"(「找不到模塊'Varfun.hs'」),那麼使用:cd來改變當前目錄到包含Varfun.hs的目錄:
Prelude> :cd c:\myDirectory Prelude> :load Varfun.hs Compiling Main ( Varfun.hs, interpreted ) Ok, modules loaded: Main. *Main>
現在你可以執行文件中的函數:
*Main> area 5 78.53981633974483
如果你修改了文件,只需使用:reload(簡寫為 :r)來重新載入文件。
註解
GHC也可以用作編譯器。就是說你可以用GHC來把你的Haskell文件轉換為一個獨立的可執行程序。詳情見其文檔。 |
你將注意到直接在ghci輸入語句與從文件載入有一些不同。這些不同現在看來可能非常隨意,但它們事實上是十分明智的範圍的結果,其餘的,我們保證將晚點解釋。
沒有let
[編輯]初學者要注意,源文件里不要像這樣寫
let x = 3 let y = 2 let area r = pi * r ^ 2
而是要像這樣寫
x = 3 y = 2 area r = pi * r ^ 2
關鍵字let
事實上在Haskell中用的很多,但這裡不強求。在這一章討論let
綁定的用法後,我們將看得更遠。
不能定義同一個量兩次
[編輯]先前,解釋器高興地允許我們像這樣寫
Prelude> let r = 5 Prelude> r 5 Prelude> let r = 2 Prelude> r 2
另一方面,在源文件里像這樣寫不對
--这不对 r = 5 r = 2
像我們先前提及的那樣,變量不能改變,並且當你寫一個源文件時甚至更關鍵。這裡有一個漂亮的暗示。它意味着:
順序不重要
[編輯]你聲明變量的順序不重要。例如,下面的代碼片段可以得到完全同樣的結果:
y = x * 2 x = 3 |
x = 3 y = x * 2 |
這是Haskell和其它函數式編程語言的一個獨特的特性。變量不可改變的事實意味着我們隨意選擇以任何順序寫代碼(但是這也是我們不能聲明一個量一次以上的原因--否則那將是模稜兩可的的)。
練習 |
---|
把你在上一章寫的作業保存在一個Haskell文件中。在GHCi中載入這個文件並用一些參數測試裡面的函數 |
關於函數的更多特性
[編輯]當我們開始使用源文件工作而不只是在解釋器中敲一些代碼時,定義多個子函數會讓工作變得簡單。 讓我們開始見識Haskell函數的威力吧。
條件表達式
[編輯]if / then / else
[編輯]Haskell支持標準的條件表達式。我們可以定義一個參數小於時返回、參數等於時返回、參數大於時返回的函數。事實上,這個函數已經存在(被稱為符號(signum)函數),但是讓我們定義一個我們自己的,把它叫做mySignum
。
mySignum x = if x < 0 then -1 else if x > 0 then 1 else 0
我們可以這樣像這樣測試它:
例子:
*Main> mySignum 5 1 *Main> mySignum 0 0 *Main> mySignum (5-10) -1 *Main> mySignum (-1) -1
注意最後一個測試「-1」兩邊的括弧是必需的;如果沒有,系統將認為你正試圖將值「mySignum」減去「1」,而這是錯誤的。
Haskell中的結構與大多數其它編程語言非常相似;無論如何,你必須同時有一個then和一個else子句。在執行條件語句後如果它的值為True
,接着會執行then部分;在執行條件語句後如果它的值為False
,接着會執行else部分。
你可以把程序修改到文件里然後重新裝載到GHCI中,除了可以用命令:l Varfun.hs 重新裝載外,你還可以通過更快捷更簡單的方法:reload 或 :r 把當前已經載入的文件重新載入。
case
[編輯]Haskell跟很多語言一樣提供了 case 結構用於組合多個條件語句。(case其實有更強大的功能 -- 詳情可以參考 模式匹配).
假設我們要定義一個這樣的函數,如果它的參數為它會輸出;如果它的參數為它會輸出;如果它的參數為它會輸出;如果它的參數為其它,它會輸出。如果使用if 語句我們會得到一個可讀性很差的函數。但我們可以用case語句寫出如下形式可讀性更強的函數:
f x = case x of 0 -> 1 1 -> 5 2 -> 2 _ -> (-1)
在這個程序中,我們定義了函數f
它有一個參數x
,它檢查x
的值。如果它的值符合條件那麼f
的值為,如果它的值符合條件那麼f
的值為,如此類推。如果x
不符合之前任何列出的值那麼f
的值為("_"
為「通配符」表示任何值都可以符合這個條件)
注意在這裡縮進是非常重要的。Haskell 使用一個叫「layout"的布局系統對程序的代碼進行維護(Python 語言也使用一個相似的系統)。這個布局系統允許我們可以不需要像C,Java語言那樣加分號跟花括號來對代碼段進行分割。
縮進
[編輯]更多了解請到 縮進。 有人不喜歡使用使用縮進的布局方法,而使用分號跟花括號的方法。如果使用這種方法,以上的程序可以寫成以下的形式:
f x = case x of { 0 -> 1 ; 1 -> 5 ; 2 -> 2 ; _ -> -1 }
當然, 如果你使用分號跟花括號的布局方法, 你可以更隨意地編寫你的代碼。以下的方式是完全可以的。
f x = case x of { 0 -> 1 ; 1 -> 5 ; 2 -> 2 ; _ -> -1 }
但是用這種方法有時候代碼可讀性是很差的。(譬如在以上情況下)
為不同參數定義一個函數
[編輯]函數也可以通過分段定義的方法進行定義,也就是說你可以為不同個參數定義同一個函數的不同版本。例如,以上的函數f
可以寫成一下方式
f 0 = 1 f 1 = 5 f 2 = 2 f _ = -1
就跟以上的case
語句一樣,函數的執行是與定義的順序有關的。如果我們把最後的一行移到最前面,那麼無論參數是什麼,函數f
的值都會是-1
,無論是0 ,1 ,2 (大部分編譯器會對這種參數定義重合發出警告)。如果我們不使用f _ = -1
,當函數遇到 0 ,1 ,2 以外的參數時會拋出一個錯誤(大部分編譯器也會會發出警告)。這種函數分段定義的方法是非常常用的,而且會經常在這個教程裡面使用。以上的方法跟case
語句實質上是等價的 —— 函數分段定義將被翻譯成case
語句
函數合成
[編輯]複雜的函數可以通過簡單的函數相互合成進行構建。函數合成就是把一個函數的結果作為另一個函數的參數。其實我們曾經在起步那一章見過,5*4+3
就是兩個函數合成。在這個例子中先執行,然後執行的結果作為 參數,最後得出結果。我們也可以把這種方法應用到square
與 f
:
square x = x^2
例子:
*Main> square (f 1) 25 *Main> square (f 2) 4 *Main> f (square 1) 5 *Main> f (square 2) -1
每一個函數的結果都是顯而易見的。在例子的第一句中括號是非常必要的;不然,解釋器認為你要嘗試得到square f
的值,但這顯然沒有意義。函數的這種合成方式普遍存在於其它編程語言中。在Haskell中有另外一種更數學化的表達方法:(.
) 點函數。點函數源於數學中的()符號。
註解
在數學裡我們用表達式 表達 "f following g." 在Haskell , 代碼 其中 等同於 。 |
(.
)函數(函數的合成函數),將兩個函數合稱為一個函數。例如,(square . f)
表示一個有一個參數的函數,先把這個參數代入函數f
,然後再把這個結果代入函數square
得到最後結果。相反地,(f . square)
表示一個有一個參數的函數,先把這個參數代入函數square
,然後再把這個結果代入函數f
得到最後結果。我們可以通過一下例子中的方法進行測試:
例子:
*Main> (square . f) 1 25 *Main> (square . f) 2 4 *Main> (f . square) 1 5 *Main> (f . square) 2 -1
在這裡,我們必須用括號把函數合成語句括起來;不然,如(square . f) 1
Haskell的編譯器會認為我們嘗試把square
與f 1
的結果合成起來,但是這是沒有意義的因為f 1
甚至不是一個函數。
現在我們可以稍稍停下來看看在Prelude中已經定義的函數,這是十分明智的。因為很有可能把已經在Prelude中定義了的函數,自己卻不經意再定義一遍(我已經不記得我這樣做了多少遍了),這樣浪費掉了很多時間。
let綁定
[編輯]我們經常在函數中聲明局部變量。 如果你記得中學數學課的內容,這裡有一個例子, 一元二次方程可以用下列等式求解
我們將它轉換為函數來得到:的兩個值
roots a b c = ((-b + sqrt(b*b - 4*a*c)) / (2*a), (-b - sqrt(b*b - 4*a*c)) / (2*a))
請注意我們的定義中有一些冗餘,不如數學定義優美。在函數的定義中我們重複了sqrt(b*b - 4*a*c)
。 用Haskell的局部綁定(local binding)可以解決這個問題。也就是說我們可以在函數中定義只能在本函數中使用的值。 我們來創建sqrt(b*b-4*a*c)
的局部綁定disc
,並在函數中替換sqrt(b*b - 4*a*c)
我們使用了let/in來聲明disc
:
roots a b c = let disc = sqrt (b*b - 4*a*c) in ((-b + disc) / (2*a), (-b - disc) / (2*a))
在let語句中可以同時聲明多個值,只需注意讓它們有相同的縮進就可以:
roots a b c = let disc = sqrt (b*b - 4*a*c) twice_a = 2*a in ((-b + disc) / twice_a, (-b - disc) / twice_a)
更進一步 |
習題解答 |
Haskell基礎 |
起步 >> 變量和函數 >> 列表和元組 >> 更進一步 >> 類型基礎 >> 簡單的輸入輸出 >> 類型聲明 |
Haskell |
Haskell基礎
>> 初級Haskell
>> Haskell進階
>> Monads
|