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

我們可以這樣像這樣測試它:

 
Example

例子:

*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 把當前已經載入的文件重新載入。

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就是兩個函數合成。在這個例子中 先執行,然後執行的結果作為   參數,最後得出結果。我們也可以把這種方法應用到squaref

square x = x^2
 
Example

例子:

*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 , 代碼f . g 也表達為 "f following g."

其中  等同於  


(.)函數(函數的合成函數),將兩個函數合稱為一個函數。例如,(square . f)表示一個有一個參數的函數,先把這個參數代入函數f,然後再把這個結果代入函數square得到最後結果。相反地,(f . square)表示一個有一個參數的函數,先把這個參數代入函數square,然後再把這個結果代入函數f得到最後結果。我們可以通過一下例子中的方法進行測試:

 
Example

例子:

*Main> (square . f) 1
25
*Main> (square . f) 2
4
*Main> (f . square) 1
5
*Main> (f . square) 2
-1

在這裏,我們必須用括號把函數合成語句括起來;不然,如(square . f) 1Haskell的編譯器會認為我們嘗試把squaref 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
高級Haskell >> 類型的樂趣 >> 理論提升 >> Haskell性能


庫參考 >> 普通實務 >> 特殊任務