Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: ec3e665c72
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 165 lines (162 sloc) 26.304 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <style>
            @import url(css/style.css);
        </style>
        <script src="js/jquery.js"></script>
        <script src="js/jquery.chili-2.2.js"></script>
        <script src="js/script.js"></script>
        <script>
             ChiliBook.recipeFolder = "js/chili/";
             ChiliBook.automaticSelector = "pre";
        </script>
        <title>函數的語法</title>
    </head>
    <body>
        <div id="header">
            <img id="beta" alt="beta" src="img/beta.png" />
        </div>

        <div id="main">
            <ul class="nav">
                <li class="left"> <img src="img/prv.png"></img><a href="types-and-type-classes.html">Types and Typeclasses</a></li>
                <li class="center"><a href="chapters.html">Index</a></li>
                <li class="right"> <a href="recursion.html">遞迴</a><img src="img/nxt.png"></img></li>
            </ul>
            <a name="函數的語法"></a><h1>函數的語法</h1><a name="模式匹配(Pattern matching)"></a><h2>模式匹配(Pattern matching)</h2><img src="img/pattern.png" style="float:right"></img><p>本章講的就是haskell那套酷酷的語法結構,先從模式匹配開始。模式匹配通過檢查數據的特定結構來檢查其是否匹配,並按模式從中取得數據。</p><p>在定義函數時,你可以為不同的模式分別定義函數體,這就讓程式碼更加簡潔易讀。你可以匹配一切數據類型---數字,字元,List,元組,等等。我們弄個簡單函數,讓它檢查我們傳給它的數字是不是7。</p><pre class="code">lucky :: (Integral a) => a -> String  
lucky 7 = "LUCKY NUMBER SEVEN!"  
lucky x = "Sorry, you're out of luck, pal!"   </pre><p>在呼叫<code>lucky</code>時,模式會從上至下進行檢查,一旦有匹配,那對應的函數體就被應用了。這個模式中的唯一匹配是參數為7,如果不是7,就轉到下一個模式,它匹配一切數值並將其綁定為x。這個函數完全可以使用if實現,不過我們若要個分辨1到5中的數字,而無視其它數的函數該怎麼辦?要是沒有模式匹配的話,那可得好大一棵if-else樹了!</p><pre class="code">sayMe :: (Integral a) => a -> String  
sayMe 1 = "One!"  
sayMe 2 = "Two!"  
sayMe 3 = "Three!"  
sayMe 4 = "Four!"  
sayMe 5 = "Five!"  
sayMe x = "Not between 1 and 5"  </pre><p>注意下,如果我們把最後匹配一切的那個模式挪到最前,它的結果就全都是<code>"Not between 1 and 5"  </code>了。因為它自己匹配了一切數字,不給後面的模式留機會。</p><p>記得前面實現的那個階乘函數麼?當時是把<code>n</code>的階乘定義成了<code>product [1..n]</code>。也可以寫出像數學那樣的遞迴實現,先說明0的階乘是1,再說明每個正整數的階乘都是這個數與它前驅(predecessor)對應的階乘的積。如下便是翻譯到haskell的樣子:</p><pre class="code">factorial :: (Integral a) => a -> a  
factorial 0 = 1  
factorial n = n * factorial (n - 1)  </pre><p>這就是我們定義的第一個遞迴函數。遞迴在haskell中十分重要,我們會在後面深入理解。如果拿一個數(如3)呼叫factorial函數,這就是接下來的計算步驟:先計算<code>3*factorial 2</code><code>factorial 2</code>等於<code>2*factorial 1</code>,也就是<code>3*(2*(factorial 1))</code><code>factorial 1</code>等於<code>1*factorial 0</code>,好,得<code>3*(2*(1*factorial 0))</code>,遞迴在這裡到頭了,嗯---我們在萬能匹配前面有定義,0的階乘是1.於是最終的結果等於<code>3*(2*(1*1))</code>。若是把第二個模式放在前面,它就會捕獲包括0在內的一切數字,這一來我們的計算就永遠都不會停止了。這便是為什麼說模式的順序是如此重要:它總是優先匹配最符合的那個,最後才是那個萬能的。</p><p>模式匹配也會失敗。假如這個函數:</p><pre class="code">charName :: Char -> String  
charName 'a' = "Albert"  
charName 'b' = "Broseph"  
charName 'c' = "Cecil"  </pre><p>拿個它沒有考慮到的字元去呼叫它,你就會看到這個:</p><pre class="code">ghci> charName 'a'  
"Albert"  
ghci> charName 'b'  
"Broseph"  
ghci> charName 'h'  
"*** Exception: tut.hs:(53,0)-(55,21): Non-exhaustive patterns in function charName  </pre><p>它告訴我們說,這個模式不夠全面。因此,在定義模式時,一定要留一個萬能匹配的模式,這樣我們的程序就不會為了不可預料的輸入而崩潰了。</p><p>對Tuple同樣可以使用模式匹配。寫個函數,將二維空間中的向量相加該如何?將它們的x項和y項分別相加就是了。如果不瞭解模式匹配,我們很可能會寫出這樣的程式碼:</p><pre class="code">addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)  
addVectors a b = (fst a + fst b, snd a + snd b)  </pre><p>嗯,可以運行。但有更好的方法,上模式匹配:</p><pre class="code">addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)  
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)  </pre><p>there we go!好多了!注意,它已經是個萬能的匹配了。兩個addVector的類型都是<code>addVectors:: (Num a) => (a,a) -> (a,a) -> (a,a)</code>,我們就能夠保證,兩個參數都是序對(Pair)了。</p><p>fst和snd可以從序對中取出元素。三元組(Tripple)呢?嗯,沒現成的函數,得自己動手:</p><pre class="code">first :: (a, b, c) -> a  
first (x, _, _) = x  

second :: (a, b, c) -> b  
second (_, y, _) = y  

third :: (a, b, c) -> c  
third (_, _, z) = z  </pre><p>這裡的_就和List Comprehension中一樣。表示我們不關心這部分的具體內容。</p><p>說到List Comprehension,我想起來在List Comprehension中也能用模式匹配:</p><pre class="code">ghci> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]
ghci> [a+b | (a,b) <- xs]
[4,7,6,8,11,4]</pre><p>一旦模式匹配失敗,它就簡單挪到下個元素。</p><p>對list本身也可以使用模式匹配。你可以用<code>[]</code><code>:</code>來匹配它。因為<code>[1,2,3]</code>本質就是<code>1:2:3:[]</code>的語法糖。你也可以使用前一種形式,像<code>x:xs</code>這樣的模式可以將list的頭部綁定為x,尾部綁定為xs。如果這list只有一個元素,那麼xs就是一個空list。</p><blockquote><p><b>Note</b>:x:xs這模式的應用非常廣泛,尤其是遞迴函數。不過它只能匹配長度大於等於1的list。</p></blockquote><p>如果你要把list的前三個元素都綁定到變數中,可以使用類似<code>x:y:z:xs</code>這樣的形式。它只能匹配長度大於等於3的list。</p><p>我們已經知道了對list做模式匹配的方法,就實現個我們自己的head函數。</p><pre class="code">head' :: [a] -> a  
head' [] = error "Can't call head on an empty list, dummy!"  
head' (x:_) = x  </pre><p>看看管不管用:</p><pre class="code">ghci> head' [4,5,6]  
4  
ghci> head' "Hello"  
'H'  </pre><p>漂亮!注意下,你若要綁定多個變數(用_也是如此),我們必須用括號將其括起。同時注意下我們用的這個error函數,它可以生成一個運行時錯誤,用參數中的字元串表示對錯誤的描述。它會直接導致程序崩潰,因此應謹慎使用。可是對一個空list取head真的不靠譜哇。</p><p>弄個簡單函數,讓它用非標準的英語給我們展示list的前幾項。</p><pre class="code">tell :: (Show a) => [a] -> String  
tell [] = "The list is empty"  
tell (x:[]) = "The list has one element: " ++ show x  
tell (x:y:[]) = "The list has two elements: " ++ show x ++ " and " ++ show y  
tell (x:y:_) = "This list is long. The first two elements are: " ++ show x ++ " and " ++ show y  </pre><p>這個函數顧及了空list,單元素list,雙元素list以及較長的list,所以這個函數很安全。<code>(x:[])</code><code>(x:y:[])</code>也可以寫作<code>[x]</code><code>[x,y]</code>(有了語法糖,我們不必多加括號)。不過<code>(x:y:_)</code>這樣的模式就不行了,因為它匹配的list長度不固定。</p><p>我們曾用List Comprehension實現過自己的length函數,現在用模式匹配和遞迴重新實現它:</p><pre class="code">length' :: (Num b) => [a] -> b  
length' [] = 0  
length' (_:xs) = 1 + length' xs  </pre><p>這與先前寫的那個factorial函數很相似。先定義好未知輸入的結果---空list,這也叫作邊界條件。再在第二個模式中將這List分割為頭部和尾部。說,List的長度就是其尾部的長度加1。匹配頭部用的_,因為我們並不關心它的值。同時也應明確,我們顧及了List所有可能的模式:第一個模式匹配空list,第二個匹配任意的非空list。</p><p>看下拿<code>"ham"</code>呼叫<code>length'</code>會怎樣。首先它會檢查它是否為空List。顯然不是,於是進入下一模式。它匹配了第二個模式,把它分割為頭部和尾部並無視掉頭部的值,得長度就是<code>1+length' "am"</code>。ok。以此類推,<code>"am"</code><code>length</code>就是<code>1+length' "m"</code>。好,現在我們有了<code>1+(1+length' "m")</code><code>length' "m"</code><code>1+length ""</code>(也就是<code>1+length' []</code>)。根據定義,<code>length' []</code>等於<code>0</code>。最後得<code>1+(1+(1+0))</code></p><p>再實現<code>sum</code>。我們知道空list的和是0,就把它定義為一個模式。我們也知道一個list的和就是頭部加上尾部的和的和。寫下來就成了:</p><pre class="code">sum' :: (Num a) => [a] -> a  
sum' [] = 0  
sum' (x:xs) = x + sum' xs  </pre><p>還有個東西叫做as模式,就是將一個名字和@置於模式前,可以在按模式分割什麼東西時仍保留對其整體的引用。如這個模式<code>xs@(x:y:ys)</code>,它會匹配出與<code>x:y:ys</code>對應的東西,同時你也可以方便地通過xs得到整個list,而不必在函數體中重複<code>x:y:ys</code>。看下這個quick and dirty的例子:</p><pre class="code">capital :: String -> String  
capital "" = "Empty string, whoops!"  
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]  </pre><pre class="code">ghci> capital "Dracula"  
"The first letter of Dracula is D"  </pre><p>我們使用as模式通常就是為了在較大的模式中保留對整體的引用,從而減少重複性的工作。</p><p>還有——你不可以在模式匹配中使用<code>++</code>。若有個模式是<code>(xs++ys)</code>,那麼這個List該從什麼地方分開呢?不靠譜吧。而<code>(xs++[x,y,z])</code>或只一個<code>(xs++[x])</code>或許還能說的過去,不過出於list的本質,這樣寫也是不可以的。</p><a name="什麼是Guards"></a><h2>什麼是Guards</h2><p>模式用來檢查一個值是否合適並從中取值,而guard則用來檢查一個值的某項屬性是否為真。咋一聽有點像是if語句,實際上也正是如此。不過處理多個條件分支時guard的可讀性要高些,並且與模式匹配契合的很好。</p><img src="img/guards.png" style="float:right"></img><p>在講解它的語法前,我們先看一個用到guard的函數。它會依據你的BMI值(body mass index,身體質量指數)來不同程度地侮辱你。BMI值即為體重除以身高的平方。如果小於18.5,就是太瘦;如果在18.5到25之間,就是正常;25到30之間,超重;如果超過30,肥胖。這就是那個函數(我們目前暫不為您計算bmi,它只是直接取一個emi值)。</p><pre class="code">bmiTell :: (RealFloat a) => a -> String
bmiTell bmi
    | bmi <= 18.5 = "You're underweight, you emo, you!"
    | bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
    | bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
    | otherwise = "You're a whale, congratulations!"  </pre><p>guard由跟在函數名及參數後面的豎綫標誌,通常他們都是靠右一個縮進排成一列。一個guard就是一個布爾表達式,如果為真,就使用其對應的函數體。如果為假,就送去見下一個guard,如之繼續。如果我們用24.3呼叫這個函數,它就會先檢查它是否小於等於18.5,顯然不是,於是見下一個guard。24.3小於25.0,因此通過了第二個guard的檢查,就返回第二個字元串。</p><p>在這裡則是相當的簡潔,不過不難想象這在命令式語言中又會是怎樣的一棵if-else樹。由於if-else的大樹比較雜亂,若是出現問題會很難發現,guard對此則十分清楚。</p><p>最後的那個guard往往都是<code>otherwise</code>,它的定義就是簡單一個<code>otherwise = True</code>,捕獲一切。這與模式很相像,只是模式檢查的是匹配,而它們檢查的是布爾表達式 。如果一個函數的所有guard都沒有通過(而且沒有提供otherwise作萬能匹配),就轉入下一模式。這便是guard與模式契合的地方。如果始終沒有找到合適的guard或模式,就會發生一個錯誤。</p><p>當然,guard可以在含有任意數量參數的函數中使用。省得用戶在使用這函數之前每次都自己計算bmi。我們修改下這個函數,讓它取身高體重為我們計算。</p><pre class="code">bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
    | weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"
    | weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
    | weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"
    | otherwise = "You're a whale, congratulations!"  </pre><p>看看我胖不胖......</p><pre class="code">ghci> bmiTell 85 1.90  
"You're supposedly normal. Pffft, I bet you're ugly!"  </pre><p>Yay!我不胖!不過haskell依然說我很猥瑣...什麼道理...</p><p>注意下,函數名和參數的後面並沒有=。許多新人容易搞出語法錯誤,就是因為在後面加上了=。</p><p>另一個簡單的例子:實現個自己的<code>max</code>函數。應該還記得,它是取兩個可比較的值,返回較大的那個。</p><pre class="code">max' :: (Ord a) => a -> a -> a  
max' a b   
    | a > b     = a  
    | otherwise = b  </pre><p>guard也可以堆一行裡面。這樣的可讀性會差些,因而是不被鼓勵的。即使是較短的函數也是如此,僅僅出於演示,我們可以這樣重寫max':</p><pre class="code">max' :: (Ord a) => a -> a -> a  
max' a b | a > b = a | otherwise = b  </pre><p>Ugh!一點都不好讀!繼續進發,用guard實現我們自己的compare函數:</p><pre class="code">myCompare :: (Ord a) => a -> a -> Ordering  
a `myCompare` b  
    | a > b     = GT  
    | a == b    = EQ  
    | otherwise = LT  </pre><pre class="code">ghci> 3 `myCompare` 2  
GT  </pre><blockquote><p><b>Note</b>:通過反單引號,我們不僅可以以中綴形式呼叫函數,也可以在定義函數的時候使用它。有時這樣會更易讀。</p></blockquote><a name="關鍵字Where"></a><h2>關鍵字Where</h2><p>前一節中我們寫了這個bmi計算函數:</p><pre class="code">bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
    | weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"
    | weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
    | weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"
    | otherwise = "You're a whale, congratulations!"</pre><p>注意,我們重複了3次。我們重複了3次。程序員的字典裡不應該有“重複”這個詞。既然發現有重複,那麼給它一個名字來代替這三個表達式會更好些。嗯,我們可以這樣修改:</p><pre class="code">bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
    | bmi <= 18.5 = "You're underweight, you emo, you!"
    | bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
    | bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
    | otherwise = "You're a whale, congratulations!"
    where bmi = weight / height ^ 2</pre><p>我們的where關鍵字跟在guard後面(最好是與豎綫縮進一致),可以定義多個名字和函數。這些名字對每個guard都是可見的,這一來就避免了重複。如果我們打算換種方式計算bmi,只需進行一次修改就行了。通過命名,我們提升了程式碼的可讀性,並且由於bmi只計算了一次,函數的執行效率也有所提升。我們可以再做下修改:</p><pre class="code">bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
    | bmi <= skinny = "You're underweight, you emo, you!"
    | bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!"
    | bmi <= fat = "You're fat! Lose some weight, fatty!"
    | otherwise = "You're a whale, congratulations!"
    where bmi = weight / height ^ 2
          skinny = 18.5
          normal = 25.0
          fat = 30.0</pre><p>函數在<i>where</i>綁定中定義的名字只對本函數可見,因此我們不必擔心它會污染其他函數的命名空間。注意,其中的名字都是一列垂直排開,如果不這樣規範,haskell就搞不清楚它們在哪個地方了。</p><p><i>where</i>綁定不會在多個模式中共享。如果你在一個函數的多個模式中重複用到同一名字,就應該把它置於全局定義之中。</p><p><i>where</i>綁定也可以使用<b>模式匹配</b>!前面那段程式碼可以改成:</p><pre class="code">...  
where bmi = weight / height ^ 2  
      (skinny, normal, fat) = (18.5, 25.0, 30.0)  </pre><p>我們再搞個簡單函數,讓它告訴我們姓名的首字母:</p><pre class="code">initials :: String -> String -> String  
initials firstname lastname = [f] ++ ". " ++ [l] ++ "."  
    where (f:_) = firstname  
          (l:_) = lastname  </pre><p>我們完全按可以在函數的參數上直接使用模式匹配(這樣更短更簡潔),在這裡只是為了演示在where語句中同樣可以使用模式匹配:</p><p><i>where</i>綁定可以定義名字,也可以定義函數。保持健康的編程風格,我們搞個計算一組bmi的函數:</p><pre class="code">calcBmis :: (RealFloat a) => [(a, a)] -> [a]  
calcBmis xs = [bmi w h | (w, h)
    where bmi weight height = weight / height ^ 2  </pre><p>這就全了!在這裡將<code>bmi</code>搞成一個函數,是因為我們不能依據參數直接進行計算,而必須先從傳入函數的list中取出每個序對並計算對應的值。</p><p><i>where</i>綁定還可以嵌套。有個已被廣泛接受的理念,就是一個函數應該有幾個輔助函數。而每個輔助函數也可以通過where擁有各自的輔助函數。</p><a name="關鍵字Let"></a><h2>關鍵字Let</h2><p>let綁定與where綁定很相似。where綁定是在函數底部定義名字,對包括所有guard在內的整個函數可見。let綁定則是個表達式,允許你在任何位置定義局部變數,而對不同的guard不可見。正如haskell中所有賦值結構一樣,let綁定也可以使用模式匹配。看下它的實際應用!這是個依據半徑和高度求圓柱體表面積的函數:</p><pre class="code">cylinder :: (RealFloat a) => a -> a -> a  
cylinder r h = 
    let sideArea = 2 * pi * r * h  
        topArea = pi * r ^2  
    in  sideArea + 2 * topArea  </pre><img src="img/letitbe.png" style="float:right"></img><p>let的格式為<code>let [bindings] in [expressions]</code>。在<i>let</i>中綁定的名字僅對in部分可見。<i>let</i>裡面定義的名字也得對齊到一列。不難看出,這用<i>where</i>綁定也可以做到。那麼它倆有什麼區別呢?看起來無非就是,<i>let</i>把綁定放在語句前面而<i>where</i>放在後面嘛。</p><p>不同之處在於,<i>let</i>綁定本身是個表達式,而<i>where</i>綁定則是個語法結構。還記得前面我們講if語句時提到它是個表達式,因而可以隨處安放?</p><pre class="code">ghci> [if 5 > 3 then "Woo" else "Boo", if 'a' > 'b' then "Foo" else "Bar"]  
["Woo", "Bar"]  
ghci> 4 * (if 10 > 5 then 10 else 0) + 2  
42</pre><p><i>let</i>綁定也可以實現:</p><pre class="code">ghci> 4 * (let a = 9 in a + 1) + 2  
42  </pre><p><i>let</i>也可以定義局部函數:</p><pre class="code">ghci> [let square x = x * x in (square 5, square 3, square 2)]  
[(25,9,4)]  </pre><p>若要在一行中綁定多個名字,再將它們排成一列顯然是不可以的。不過可以用分號將其分開。</p><pre class="code">ghci> (let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey "; bar = "there!" in foo ++ bar)  
(6000000,"Hey there!")  </pre><p>最後那個綁定後面的分號不是必須的,不過加上也沒關係。如我們前面所說,你可以在let綁定中使用模式匹配。這在從Tuple取值之類的操作中很方便。</p><pre class="code">ghci> (let (a,b,c) = (1,2,3) in a+b+c) * 100  
600  </pre><p>你也可以把let綁定放到List Comprehension中。我們重寫下那個計算bmi值的函數,用個let替換掉原先的where。</p><pre class="code">calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]</pre><p>List Comprehension中let綁定的樣子和限制條件差不多,只不過它做的不是過濾,而是綁定名字。let中綁定的名字在輸出函數及限制條件中都可見。這一來我們就可以讓我們的函數隻返回胖子的bmi值:</p><pre class="code">calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]</pre><p><code>(w, h) <- xs</code>這裡無法使用<code>bmi</code>這名字,因為它在let綁定的前面。</p><p>在List Comprehension中我們忽略了let綁定的in部分,因為名字的可見性已經預先定義好了。不過,把一個<i>let...in</i>放到限制條件中也是可以的,這樣名字只對這個限制條件可見。在GHCi中in部分也可以省略,名字的定義就在整個交互中可見。</p><pre class="code">ghci> let zoot x y z = x * y + z
ghci> zoot 3 9 2
29
ghci> let boot x y z = x * y + z in boot 3 4 2
14
ghci> boot
< interactive>:1:0: Not in scope: `boot'</pre><p>你說既然<i>let</i>已經這麼好了,還要where幹嘛呢?嗯,let是個表達式,定義域限制的相當小,因此不能在多個guard中使用。一些朋友更喜歡<i>where</i>,因為它是跟在函數體後面,把主函數體距離類型聲明近一些會更易讀。</p><a name="Case expressions"></a><h2>Case expressions</h2><img src="img/case.png" style="float:right"></img><p>有命令式編程(<i>C, C++, Java, etc</i>)的經驗的同學一定會有所瞭解,很多命令式語言都提供了<i>case</i>語句。就是取一個變數,按照對變數的判斷選擇對應的程式碼塊。其中可能會存在一個萬能匹配以處理未預料的情況。</p><p>haskell取了這一概念融合其中。如其名,case表達式就是,嗯,一種表達式。跟<i>if..else</i><i>let</i>一樣的表達式。用它可以對變數的不同情況分別求值,還可以使用模式匹配。Hmm,取一個變數,對它模式匹配,執行對應的程式碼塊。好像在哪兒聽過?啊,就是函數定義時參數的模式匹配!好吧,模式匹配本質上不過就是case語句的語法糖而已。這兩段程式碼就是完全等價的:</p><pre class="code">head' :: [a] -> a  
head' [] = error "No head for empty lists!"  
head' (x:_) = x  </pre><pre class="code">head' :: [a] -> a  
head' xs = case xs of [] -> error "No head for empty lists!"  
                      (x:_) -> x  </pre><p>看得出,<i>case</i>表達式的語法十分簡單:</p><pre class="code">case expression of pattern -> result  
                   pattern -> result  
                   pattern -> result  
                   ...  </pre><p><i>expression</i>匹配合適的模式。如料,第一個模式若匹配,就執行第一個程式碼塊;否則就交給下一個模式。如果到最後依然沒有匹配的模式,就會產生一個運行時錯誤。</p><p>函數參數的模式匹配只能在定義函數時使用,而case表達式可以用在任何地方。例如:</p><pre class="code">describeList :: [a] -> String  
describeList xs = "The list is " ++ case xs of [] -> "empty."  
                                               [x] -> "a singleton list."   
                                               xs -> "a longer list."  </pre><p>這在表達式中作模式匹配很方便,由於模式匹配本質上就是case表達式的語法糖,那麼寫成這樣也是等價的:</p><pre class="code">describeList :: [a] -> String  
describeList xs = "The list is " ++ what xs  
    where what [] = "empty."  
          what [x] = "a singleton list."  
          what xs = "a longer list."  </pre>
            <ul class="nav">
                <li class="left"> <img src="img/prv.png"></img><a href="types-and-type-classes.html">Types and Typeclasses</a></li>
                <li class="center"><a href="chapters.html">Index</a></li>
                <li class="right"> <a href="recursion.html">遞迴</a><img src="img/nxt.png"></img></li>
            </ul>
        </div>
        <div id="footer">
        </div>
    </body>
</html>
Something went wrong with that request. Please try again.