Haskell/變量和函數

(本章的所有例子都可以寫進Haskell原始碼文件並利用GHC或Hugs加載進行求值運算。請注意不要輸入開頭的提示符,如果代碼前面有提示符,可以在GHCi等環境中輸入;如果沒有,則寫進文件再運行。)

變量 編輯

我們已經了解如何把GHCi當作計算器來使用,當然只是極其簡短的例子。對於更長的計算以及Haskell程序而言,需要對運算的中間結果進行追蹤。

中間結果可以存入變量,通過名字來訪問。一個變量包含一個,當使用變數時,變數內的值會被代入。舉個例子,請考慮以下情況

ghci> 3.1416 * 5^2
78.53999999999999

在前一章節中,我們知道了如何做像加法和減法這樣的算術運算。提問:半徑是5厘米的圓的面積是多少?別擔心,你並不是錯看了幾何課本。這個圓的面積是 ,其中 是半徑(5厘米), 簡單地說就是 。那麼,讓我們在GHCi中試一下吧:

   ___         ___ _
  / _ \ /\  /\/ __(_)
 / /_\// /_/ / /  | |      GHC Interactive, version 6.4.1, for Haskell 98.
/ /_\\/ __  / /___| |      http://www.haskell.org/ghc/
\____/\/ /_/\____/|_|      Type :? for help.

Loading package base-1.0 ... linking ... done.
Prelude>

我們要把pi(3.14)乘以半徑的平方,那就這樣

Prelude> 3.14 * 5^2
78.5

太棒了!我們讓這台奇妙而又偉大的計算機幫我們計算出了我們想要的結果。但我們不想pi僅僅保留2位小數。那麼就讓我們再試一次,這次pi變得有點長。

Prelude> 3.14159265358979323846264338327950 * (5 ^ 2)
78.53981633974483

很好,這次我們來計算一下圓的周長(提示: )。

Prelude> 2 * 3.14159265358979323846264338327950 * 5
31.41592653589793

那麼半徑是25的圓的面積呢?

Prelude> 3.14159265358979323846264338327950 * (25 ^ 2)
1963.4954084936207

我們遲早會厭倦把上述代碼一遍又一遍的輸入(或拷貝粘貼)的。如我們所言,編程的目的就是擺脫類似於把pi的前20位輸入一億遍這樣愚蠢、乏味的重複勞動。我們要做的就是讓解釋器記住pi的值:

Prelude> let pi = 3.14159265358979323846264338327950
註解

如果這條命令沒用的話,你可能使用的是hugs而不是GHCi,hugs的語法有一點不同。

這裡你就告訴了Haskell:「讓pi等於3.14159……」。這裡引入了一個新的變量pi,它被定義為一個數字3.14159265358979323846264338327950。這樣就變得非常方便了,我們只要再次輸入pi就可以了:

Prelude> pi
3.141592653589793

別在意那些丟失的小數位,他們僅僅是不顯示罷了。所有這些小數位都會在將來的計算中被使用到的。

有了變量,事情就變得輕鬆了。半徑是5厘米的圓的面積是多少呢?半徑是25厘米的呢?

Prelude> pi * 5^2
78.53981633974483
Prelude> pi * 25^2
1963.4954084936207
註解

我們這裡所說的「變量」("variables"),在其它函數式編程的書籍中經常被叫做是「符號」("symbols")。那是因為在其它的語言中,也就是命令式語言中,變量被賦予了完全不同的使用方式:跟蹤狀態(keeping track of state)。在Haskell中的變量不是這樣的:變量保存了一個值,然後再也不可以修改它了。

類型 編輯

按照上面的示例,你可能會想把半徑保存在一個變量里。讓我們看看會發生什麼吧!

Prelude> let r = 25
Prelude> 2 * pi * r

<interactive>:1:9:
    Couldn't match `Double' against `Integer'
      Expected type: Double
      Inferred type: Integer
    In the second argument of `(*)', namely `r'
    In the definition of `it': it = (2 * pi) * r

怎麼回事!這裡,你遇到了程序設計上的一個概念:類型(types)。類型是許多程式語言都有的一個特性,它被設計用來在你自己發現之前,儘早的捕獲程序設計上的錯誤。我們將在之後的類型基礎詳細討論型別,現在讓我們把它想像成插頭和插座。例如,計算機背後的不同功能的插頭被設計成不同的形狀和大小,這樣你就不必擔心插錯插頭了。類型也有著類似的目的,但是在這個特殊的例子裡,類型好像不怎麼有用啊。

這裡的詭異之處在於,像25這樣的數字會被解釋成Double(雙精度浮點型)或Integer(整型),或者可能是其它類型。由於缺少必要的信息,Haskell「猜測」它的類型是Integer(它不能和一個Double型別的數字相乘)。為了解決這個問題,我們只需簡單的強調一下,把它作為Double就可以了。

Prelude> let r = 25 :: Double
Prelude> 2 * pi * r
157.07963267948966

請注意,Haskell的這裡「猜測」只會發生在當它缺乏足夠的信息來推斷出型別的時候。下面我們會看到,在大多數情況下,Haskell是能夠根據上下文的信息來作出推斷的。也就是說,是否把一個數字作為Integer,或是其它型別。

註解

事實上在這個難題背後有一點點微妙。它涉及一個叫作單一同態限定(monomorphism restriction)的語言特性。事實上現在你不需要知道這個,所以如果只想快一點你可以跳過這條提示。除了指定Double類型,你也可以給它一個多態(polymorphic)類型,像Num a => a,意思是"屬於Num類的任意類型a"。示例代碼如下,它們運行起來像先前的代碼一樣:

Prelude> let r = 25 :: Num a => a
Prelude> 2 * pi * r
157.07963267948966

Haskell可以從理論上系統地指定這樣的多態類型,而不是做預設的存在潛在錯誤的猜測,比如整數。但在現實世界,這可能導致數值被不必要地複製或重複計算。要避免這個潛在的陷阱,Haskell的設計者贊同更謹慎的「單一同態限定」。這意味著如果可以從上下文推斷出來,或者你明確指定了一個,那麼數值只能有一個多態類型。否則編譯器會強制選擇一個默認的單一同態(也就是非多態)類型。無論如何,這個特性是有爭議的。甚至可以使用GHC參數(-fno-monomorphism-restriction)來禁用它,但是這帶來了一些低效率的風險。除此之外,多數情況下它就像明確指定類型一樣容易。


變量中的變量 編輯

變量不僅可以保存像3.14這樣的數值,還可以保存任何Haskell表達式。那麼,為了方便的表達半徑是5的圓的面積,我們可以寫如下代碼:

Prelude> let area = pi * 5^2

多麼有意思啊,我們在一個變量裡面保存了一個複雜的Haskell程序塊(一個包含變量的算術表達式)。

我們可以在變量裡面保存任意的Haskell代碼。那麼,讓我們繼續吧!

Prelude> let r = 25.0
Prelude> let area2 = pi * r ^ 2
Prelude> area2
1963.4954084936207

到現在為止,一直都還不錯。

Prelude> let r = 2.0
Prelude> area2
1963.4954084936207
變量不可以被修改

等一下,怎麼不對?為什麼我們改了半徑,面積還是原來的結果?原因就是Haskell里的變量是不可以被修改的[1]。在你第二次定義r的時候,實際上,操作的是另一個r。這種事情在現實生活中也經常碰到。名字叫John的人有多少呢?很多人名字都會是John。當你向朋友們提起「John」的時候,你朋友會根據當時的情況知道到底是哪一個John。在程序設計上也有類似於「當時的情況」這樣的概念:作用域。我們並不在這裡解釋作用域(至少現在不)。Haskell作用域的詭異之處在於可以定義兩個不同的r,而總能取用正確的那個。遺憾的是,作用域並沒有解決當前的問題。我們要的是定義一個通用的area函數,來告訴我們圓的面積。我們現在能做的,只能把它再定義一遍:

Prelude> let area3 = pi * r ^ 2
Prelude> area3
12.566370614359172

我們是程式設計師,程式設計師是討厭重複勞動的。有更好的解決方法嗎?

函數 編輯

我們為了完成一個通用的求面積功能,其實就是定義一個函數。就如同定義一個變量那樣,在Haskell中定義一個函數十分的簡單。不同之處在於等號的左邊有一些額外的東西。例如,下面我們定義一個pi,接著就是面積函數area:

Prelude> let pi = 3.14159265358979323846264338327950
Prelude> let area r = pi * r ^ 2

要計算兩個圓的面積,只要把不同的半徑傳入就可以了:

Prelude> area 5
78.53981633974483
Prelude> area 25
1963.4954084936207

函數的出現使得我們對代碼的重用方面有了極大的飛躍。先等一下,讓我們來剖析一下上面事物的本質。注意到area r = ...中的r了嗎?這就是所謂的參數。參數用來表示一個函數的輸入。當Haskell在執行這個函數的時候,參數的值是來自於外部的。在area這個例子中,當你調用area 5的時候,r5,而調用area 25的時候,r25.

練習

在Haskell中輸入如下代碼:(先不要在解釋器中輸入)

Prelude> let r = 0
Prelude> let area r = pi * r ^ 2
Prelude> area 5
  1. 你認為會發生什麼?
  2. 實際發生了什麼為什麼(提示:記得以前我們提過的「作用域」嗎?))

作用域和參數 編輯

警告:此章節包含上面練習的一些信息

我們希望你完成上面那個小小的練習(也可說是一個實驗)。那麼,下面的代碼並不會讓你出乎意料:

Prelude> let r = 0
Prelude> let area r = pi * r ^ 2
Prelude> area 5
78.53981633974483

你可能認為會得到一個0,但結果出乎你的預料。原因就上我們上面所說的,一個命名參數的值是你調用這個函數時傳入的。那就是我們的老朋友,作用域。let r = 0中的r和我們定義的函數area中的r,並不是同一個r。在area中的r覆蓋了其它的r。你可以認為Haskell選取了一個最近的一個r。如果你有很多朋友都叫John,那麼和你在一起的那個John就是你在說話中提到的那個Jonh。類似的,r的值是什麼取決於作用域。

多重參數 編輯

關於函數你也許應該知道另一項知識。那就是,函數可以接受一個以上的參數。例如,你想要計算一個矩形的面積。這很容易表達:

Prelude> let areaRect l w = l * w
Prelude> areaRect 5 10
50

又或者你想要計算一個直角三角形的面積 

Prelude> let areaTriangle b h = (b * h) / 2
Prelude> areaTriangle 3 9
13.5

參數傳入的方式很直接:只需要根據和定義時一樣的順序傳入參數就可以了。因此,areaTriangle 3 9會給出底為3而高為9的三角形面積,而areaTriangle 9 3將給出底為9而高為3的三角形面積。

練習
寫一個計算立方體體積的函數。立方體有長、寬、高。把它們乘起來就可以了。


函數中的函數 編輯

我們可以在其他函數的內部調用函數以進一步刪減重複的數量。一個簡單的例子將用於展示怎樣利用這些創建一個計算正方形面積的函數。我們都知道正方形是矩形的一個特殊情況(面積仍然是寬乘以長);然而,我們知道它的寬和長是相同的,因此為什麼我們需要鍵入兩次呢?

Prelude> let areaRect l w = l * w
Prelude> let areaSquare s = areaRect s s
Prelude> areaSquare 5
25
練習
編寫一個計算圓柱體體積的函數. 圓柱體的體積是圓形底的面積 (你已經在這章編寫了這個函數, 所以重新利用它) 乘以高.

總結 編輯

  1. 變量保存著值。其實,他們可以保存任意的Haskell表達式。
  2. 變量不可以改變。
  3. 函數可以幫助你編寫可重複利用的代碼。
  4. 函數可以接受一個以上的參數。

注釋 編輯

  1. 依據讀者以前的編程經驗:變量不可以改變?我只能得到常量?震驚!恐怖!不……相信我們,我們希望在這本書的剩下部分給你說明,不改變一個單一的變量對你大有幫助!事實上,這些不能改變的變量可以使生活更加方便,因為它可以使程序變得更加可以預測。



變量和函數
習題解答
Haskell基礎

起步  >> 變量和函數  >> 列表和元組  >> 更進一步  >> 類型基礎  >> 簡單的輸入輸出  >> 類型聲明


Haskell

Haskell基礎 >> 初級Haskell >> Haskell進階 >> Monads
高級Haskell >> 類型的樂趣 >> 理論提升 >> Haskell性能


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