[TOC]
## 模式匹配

本章講的就是haskell那套酷酷的語法結構,先從模式匹配開始。模式匹配通過檢查數據的特定結構來檢查其是否匹配,并按模式從中取得數據。
在定義函數時,你可以為不同的模式分別定義函數體,這就讓代碼更加簡潔易讀。你可以匹配一切數據類型---數字,字符,List,元組,等等。我們弄個簡單函數,讓它檢查我們傳給它的數字是不是7。
~~~
lucky?::?(Integral?a)?=>?a?->?String???
lucky?7?=?"LUCKY?NUMBER?SEVEN!"???
lucky?x?=?"Sorry,?you're?out?of?luck,?pal!"???
~~~
在調用`lucky`時,模式會從上至下進行檢查,一旦有匹配,那對應的函數體就被應用了。這個模式中的唯一匹配是參數為7,如果不是7,就轉到下一個模式,它匹配一切數值并將其綁定為x。這個函數完全可以使用if實現,不過我們若要個分辨1到5中的數字,而無視其它數的函數該怎么辦?要是沒有模式匹配的話,那可得好大一棵if-else樹了!
~~~
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"??
~~~
注意下,如果我們把最后匹配一切的那個模式挪到最前,它的結果就全都是`"Not between 1 and 5" ?`了。因為它自己匹配了一切數字,不給后面的模式留機會。
記得前面實現的那個階乘函數么?當時是把`n`的階乘定義成了`product [1..n]`。也可以寫出像數學那樣的遞歸實現,先說明0的階乘是1,再說明每個正整數的階乘都是這個數與它前驅(predecessor)對應的階乘的積。如下便是翻譯到haskell的樣子:
~~~
factorial?::?(Integral?a)?=>?a?->?a???
factorial?0?=?1???
factorial?n?=?n?*?factorial?(n?-?1)??
~~~
這就是我們定義的第一個遞歸函數。遞歸在haskell中十分重要,我們會在后面深入理解。如果拿一個數(如3)調用factorial函數,這就是接下來的計算步驟:先計算`3*factorial 2`,`factorial 2`等于`2*factorial 1`,也就是`3*(2*(factorial 1))`。`factorial 1`等于`1*factorial 0`,好,得`3*(2*(1*factorial 0))`,遞歸在這里到頭了,嗯---我們在萬能匹配前面有定義,0的階乘是1.于是最終的結果等于`3*(2*(1*1))`。若是把第二個模式放在前面,它就會捕獲包括0在內的一切數字,這一來我們的計算就永遠都不會停止了。這便是為什么說模式的順序是如此重要:它總是優先匹配最符合的那個,最后才是那個萬能的。
模式匹配也會失敗。假如這個函數:
~~~
charName?::?Char?->?String???
charName?'a'?=?"Albert"???
charName?'b'?=?"Broseph"???
charName?'c'?=?"Cecil"??
~~~
拿個它沒有考慮到的字符去調用它,你就會看到這個:
~~~
ghci>?charName?'a'???
"Albert"???
ghci>?charName?'b'???
"Broseph"???
ghci>?charName?'h'???
"***?Exception:?tut.hs:(53,0)-(55,21):?Non-exhaustive?patterns?in?function?charName??
~~~
它告訴我們說,這個模式不夠全面。因此,在定義模式時,一定要留一個萬能匹配的模式,這樣我們的程序就不會為了不可預料的輸入而崩潰了。
對Tuple同樣可以使用模式匹配。寫個函數,將二維空間中的向量相加該如何?將它們的x項和y項分別相加就是了。如果不了解模式匹配,我們很可能會寫出這樣的代碼:
~~~
addVectors?::?(Num?a)?=>?(a,?a)?->?(a,?a)?->?(a,?a)???
addVectors?a?b?=?(fst?a?+?fst?b,?snd?a?+?snd?b)??
~~~
嗯,可以運行。但有更好的方法,上模式匹配:
~~~
addVectors?::?(Num?a)?=>?(a,?a)?->?(a,?a)?->?(a,?a)???
addVectors?(x1,?y1)?(x2,?y2)?=?(x1?+?x2,?y1?+?y2)??
~~~
there we go!好多了!注意,它已經是個萬能的匹配了。兩個addVector的類型都是`addVectors:: (Num a) => (a,a) -> (a,a) -> (a,a)`,我們就能夠保證,兩個參數都是序對(Pair)了。
fst和snd可以從序對中取出元素。三元組(Tripple)呢?嗯,沒現成的函數,得自己動手:
~~~
first?::?(a,?b,?c)?->?a???
first?(x,?_,?_)?=?x???
second?::?(a,?b,?c)?->?b???
second?(_,?y,?_)?=?y???
third?::?(a,?b,?c)?->?c???
third?(_,?_,?z)?=?z??
~~~
這里的_就和List Comprehension中一樣。表示我們不關心這部分的具體內容。
說到List Comprehension,我想起來在List Comprehension中也能用模式匹配:
~~~
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]
~~~
一旦模式匹配失敗,它就簡單挪到下個元素。
對list本身也可以使用模式匹配。你可以用`[]`或`:`來匹配它。因為`[1,2,3]`本質就是`1:2:3:[]`的語法糖。你也可以使用前一種形式,像`x:xs`這樣的模式可以將list的頭部綁定為x,尾部綁定為xs。如果這list只有一個元素,那么xs就是一個空list。
> **Note**:x:xs這模式的應用非常廣泛,尤其是遞歸函數。不過它只能匹配長度大于等于1的list。
如果你要把list的前三個元素都綁定到變量中,可以使用類似`x:y:z:xs`這樣的形式。它只能匹配長度大于等于3的list。
我們已經知道了對list做模式匹配的方法,就實現個我們自己的head函數。
~~~
head'?::?[a]?->?a???
head'?[]?=?error?"Can't?call?head?on?an?empty?list,?dummy!"???
head'?(x:_)?=?x??
~~~
看看管不管用:
~~~
ghci>?head'?[4,5,6]???
4???
ghci>?head'?"Hello"???
'H'??
~~~
漂亮!注意下,你若要綁定多個變量(用_也是如此),我們必須用括號將其括起。同時注意下我們用的這個error函數,它可以生成一個運行時錯誤,用參數中的字符串表示對錯誤的描述。它會直接導致程序崩潰,因此應謹慎使用。可是對一個空list取head真的不靠譜哇。
弄個簡單函數,讓它用非標準的英語給我們展示list的前幾項。
~~~
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??
~~~
這個函數顧及了空list,單元素list,雙元素list以及較長的list,所以這個函數很安全。`(x:[])`與`(x:y:[])`也可以寫作`[x]`和`[x,y]`(有了語法糖,我們不必多加括號)。不過`(x:y:_)`這樣的模式就不行了,因為它匹配的list長度不固定。
我們曾用List Comprehension實現過自己的length函數,現在用模式匹配和遞歸重新實現它:
~~~
length'?::?(Num?b)?=>?[a]?->?b???
length'?[]?=?0???
length'?(_:xs)?=?1?+?length'?xs??
~~~
這與先前寫的那個factorial函數很相似。先定義好未知輸入的結果---空list,這也叫作邊界條件。再在第二個模式中將這List分割為頭部和尾部。說,List的長度就是其尾部的長度加1。匹配頭部用的_,因為我們并不關心它的值。同時也應明確,我們顧及了List所有可能的模式:第一個模式匹配空list,第二個匹配任意的非空list。
看下拿`"ham"`調用`length'`會怎樣。首先它會檢查它是否為空List。顯然不是,于是進入下一模式。它匹配了第二個模式,把它分割為頭部和尾部并無視掉頭部的值,得長度就是`1+length' "am"`。ok。以此類推,`"am"`的`length`就是`1+length' "m"`。好,現在我們有了`1+(1+length' "m")`。`length' "m"`即`1+length ""`(也就是`1+length' []`)。根據定義,`length' []`等于`0`。最后得`1+(1+(1+0))`。
再實現`sum`。我們知道空list的和是0,就把它定義為一個模式。我們也知道一個list的和就是頭部加上尾部的和的和。寫下來就成了:
~~~
sum'?::?(Num?a)?=>?[a]?->?a???
sum'?[]?=?0???
sum'?(x:xs)?=?x?+?sum'?xs??
~~~
還有個東西叫做as模式,就是將一個名字和@置于模式前,可以在按模式分割什么東西時仍保留對其整體的引用。如這個模式`xs@(x:y:ys)`,它會匹配出與`x:y:ys`對應的東西,同時你也可以方便地通過xs得到整個list,而不必在函數體中重復`x:y:ys`。看下這個quick and dirty的例子:
~~~
capital?::?String?->?String???
capital?""?=?"Empty?string,?whoops!"???
capital?all@(x:xs)?=?"The?first?letter?of?"?++?all?++?"?is?"?++?[x]??
~~~
~~~
ghci>?capital?"Dracula"???
"The?first?letter?of?Dracula?is?D"??
~~~
我們使用as模式通常就是為了在較大的模式中保留對整體的引用,從而減少重復性的工作。
還有——你不可以在模式匹配中使用`++`。若有個模式是`(xs++ys)`,那么這個List該從什么地方分開呢?不靠譜吧。而`(xs++[x,y,z])`或只一個`(xs++[x])`或許還能說的過去,不過出于list的本質,這樣寫也是不可以的。
## 注意,門衛!
模式用來檢查一個值是否合適并從中取值,而門衛(guard)則用來檢查一個值的某項屬性是否為真。咋一聽有點像是if語句,實際上也正是如此。不過處理多個條件分支時門衛的可讀性要高些,并且與模式匹配契合的很好。

在講解它的語法前,我們先看一個用到門衛的函數。它會依據你的BMI值(body mass index,身體質量指數)來不同程度地侮辱你。BMI值即為體重除以身高的平方。如果小于18.5,就是太瘦;如果在18.5到25之間,就是正常;25到30之間,超重;如果超過30,肥胖。這就是那個函數(我們目前暫不為您計算bmi,它只是直接取一個emi值)。
~~~
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!"??
~~~
門衛由跟在函數名及參數后面的豎線標志,通常他們都是靠右一個縮進排成一列。一個門衛就是一個布爾表達式,如果為真,就使用其對應的函數體。如果為假,就送去見下一個門衛,如之繼續。如果我們用24.3調用這個函數,它就會先檢查它是否小于等于18.5,顯然不是,于是見下一個門衛。24.3小于25.0,因此通過了第二個門衛的檢查,就返回第二個字符串。
在這里則是相當的簡潔,不過不難想象這在命令式語言中又會是怎樣的一棵if-else樹。由于if-else的大樹比較雜亂,若是出現問題會很難發現,門衛對此則十分清楚。
最后的那個門衛往往都是`otherwise`,它的定義就是簡單一個`otherwise = True`,捕獲一切。這與模式很相像,只是模式檢查的是匹配,而它們檢查的是布爾表達式 。如果一個函數的所有門衛都沒有通過(而且沒有提供otherwise作萬能匹配),就轉入下一模式。這便是門衛與模式契合的地方。如果始終沒有找到合適的門衛或模式,就會發生一個錯誤。
當然,門衛可以在含有任意數量參數的函數中使用。省得用戶在使用這函數之前每次都自己計算bmi。我們修改下這個函數,讓它取身高體重為我們計算。
~~~
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!"????
~~~
看看我胖不胖......
~~~
ghci>?bmiTell?85?1.90???
"You're?supposedly?normal.?Pffft,?I?bet?you're?ugly!"??
~~~
Yay!我不胖!不過haskell依然說我很猥瑣...什么道理...
注意下,函數名和參數的后面并沒有=。許多新人容易搞出語法錯誤,就是因為在后面加上了=。
另一個簡單的例子:實現個自己的`max`函數。應該還記得,它是取兩個可比較的值,返回較大的那個。
~~~
max'?::?(Ord?a)?=>?a?->?a?->?a???
max'?a?b????
????|?a?>?b?????=?a???
????|?otherwise?=?b??
~~~
門衛也可以堆一行里面。這樣的可讀性會差些,因而是不被鼓勵的。即使是較短的函數也是如此,僅僅出于演示,我們可以這樣重寫max':
~~~
max'?::?(Ord?a)?=>?a?->?a?->?a???
max'?a?b?|?a?>?b?=?a?|?otherwise?=?b??
~~~
Ugh!一點都不好讀!繼續進發,用門衛實現我們自己的compare函數:
~~~
myCompare?::?(Ord?a)?=>?a?->?a?->?Ordering???
a?`myCompare`?b???
????|?a?>?b?????=?GT???
????|?a?==?b????=?EQ???
????|?otherwise?=?LT??
~~~
~~~
ghci>?3?`myCompare`?2???
GT??
~~~
> **Note**:通過反單引號,我們不僅可以以中綴形式調用函數,也可以在定義函數的時候使用它。有時這樣會更易讀。
## Where?
前一節中我們寫了這個bmi計算函數:
~~~
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!"
~~~
注意,我們重復了3次。我們重復了3次。程序員的字典里不應該有“重復”這個詞。既然發現有重復,那么給它一個名字來代替這三個表達式會更好些。嗯,我們可以這樣修改:
~~~
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
~~~
我們的where關鍵字跟在門衛后面(最好是與豎線縮進一致),可以定義多個名字和函數。這些名字對每個門衛都是可見的,這一來就避免了重復。如果我們打算換種方式計算bmi,只需進行一次修改就行了。通過命名,我們提升了代碼的可讀性,并且由于bmi只計算了一次,函數的執行效率也有所提升。我們可以再做下修改:
~~~
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
~~~
函數在_where_綁定中定義的名字只對本函數可見,因此我們不必擔心它會污染其他函數的命名空間。注意,其中的名字都是一列垂直排開,如果不這樣規范,haskell就搞不清楚它們在哪個地方了。
_where_綁定不會在多個模式中共享。如果你在一個函數的多個模式中重復用到同一名字,就應該把它置于全局定義之中。
_where_綁定也可以使用**模式匹配**!前面那段代碼可以改成:
~~~
...???
where?bmi?=?weight?/?height?^?2???
??????(skinny,?normal,?fat)?=?(18.5,?25.0,?30.0)??
~~~
我們再搞個簡單函數,讓它告訴我們姓名的首字母:
~~~
initials?::?String?->?String?->?String???
initials?firstname?lastname?=?[f]?++?".?"?++?[l]?++?"."???
????where?(f:_)?=?firstname???
??????????(l:_)?=?lastname??
~~~
我們完全按可以在函數的參數上直接使用模式匹配(這樣更短更簡潔),在這里只是為了演示在where語句中同樣可以使用模式匹配:
_where_綁定可以定義名字,也可以定義函數。保持健康的編程風格,我們搞個計算一組bmi的函數:
~~~
calcBmis?::?(RealFloat?a)?=>?[(a,?a)]?->?[a]???
calcBmis?xs?=?[bmi?w?h?|?(w,?h)??
????where?bmi?weight?height?=?weight?/?height?^?2??
~~~
這就全了!在這里將`bmi`搞成一個函數,是因為我們不能依據參數直接進行計算,而必須先從傳入函數的list中取出每個序對并計算對應的值。
_where_綁定還可以嵌套。有個已被廣泛接受的理念,就是一個函數應該有幾個輔助函數。而每個輔助函數也可以通過where擁有各自的輔助函數。
## Let it be
let綁定與where綁定很相似。where綁定是在函數底部定義名字,對包括所有門衛在內的整個函數可見。let綁定則是個表達式,允許你在任何位置定義局部變量,而對不同的門衛不可見。正如haskell中所有賦值結構一樣,let綁定也可以使用模式匹配。看下它的實際應用!這是個依據半徑和高度求圓柱體表面積的函數:
~~~
cylinder?::?(RealFloat?a)?=>?a?->?a?->?a???
cylinder?r?h?=??
????let?sideArea?=?2?*?pi?*?r?*?h???
????????topArea?=?pi?*?r?^2???
????in??sideArea?+?2?*?topArea??
~~~

let的格式為`let [bindings] in [expressions]`。在_let_中綁定的名字僅對in部分可見。_let_里面定義的名字也得對齊到一列。不難看出,這用_where_綁定也可以做到。那么它倆有什么區別呢?看起來無非就是,_let_把綁定放在語句前面而_where_放在后面嘛。
不同之處在于,_let_綁定本身是個表達式,而_where_綁定則是個語法結構。還記得前面我們講if語句時提到它是個表達式,因而可以隨處安放?
~~~
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
~~~
用_let_綁定也可以實現:
~~~
ghci>?4?*?(let?a?=?9?in?a?+?1)?+?2???
42??
~~~
_let_也可以定義局部函數:
~~~
ghci>?[let?square?x?=?x?*?x?in?(square?5,?square?3,?square?2)]???
[(25,9,4)]??
~~~
若要在一行中綁定多個名字,再將它們排成一列顯然是不可以的。不過可以用分號將其分開。
~~~
ghci>?(let?a?=?100;?b?=?200;?c?=?300?in?a*b*c,?let?foo="Hey?";?bar?=?"there!"?in?foo?++?bar)???
(6000000,"Hey?there!")??
~~~
最后那個綁定后面的分號不是必須的,不過加上也沒關系。如我們前面所說,你可以在let綁定中使用模式匹配。這在從Tuple取值之類的操作中很方便。
~~~
ghci>?(let?(a,b,c)?=?(1,2,3)?in?a+b+c)?*?100???
600??
~~~
你也可以把let綁定放到List Comprehension中。我們重寫下那個計算bmi值的函數,用個let替換掉原先的where。
~~~
calcBmis?::?(RealFloat?a)?=>?[(a,?a)]?->?[a]???
calcBmis?xs?=?[bmi?|?(w,?h)??xs,?let?bmi?=?w?/?h?^?2]
~~~
List Comprehension中let綁定的樣子和限制條件差不多,只不過它做的不是過濾,而是綁定名字。let中綁定的名字在輸出函數及限制條件中都可見。這一來我們就可以讓我們的函數只返回胖子的bmi值:
~~~
calcBmis?::?(RealFloat?a)?=>?[(a,?a)]?->?[a]???
calcBmis?xs?=?[bmi?|?(w,?h)??xs,?let?bmi?=?w?/?h?^?2,?bmi?>=?25.0]
~~~
在`(w, h) <- xs`這里無法使用`bmi`這名字,因為它在let綁定的前面。
在List Comprehension中我們忽略了let綁定的in部分,因為名字的可見性已經預先定義好了。不過,把一個_let...in_放到限制條件中也是可以的,這樣名字只對這個限制條件可見。在GHCi中in部分也可以省略,名字的定義就在整個交互中可見。
~~~
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???
>:1:0:?Not?in?scope:?`boot'
~~~
你說既然_let_已經這么好了,還要where干嘛呢?嗯,let是個表達式,定義域限制的相當小,因此不能在多個門衛中使用。一些朋友更喜歡_where_,因為它是跟在函數體后面,把主函數體距離類型聲明近一些會更易讀。
## case表達式

有命令式編程(_C, C++, Java, etc_)的經驗的同學一定會有所了解,很多命令式語言都提供了_case_語句。就是取一個變量,按照對變量的判斷選擇對應的代碼塊。其中可能會存在一個萬能匹配以處理未預料的情況。
haskell取了這一概念融合其中。如其名,case表達式就是,嗯,一種表達式。跟_if..else_和_let_一樣的表達式。用它可以對變量的不同情況分別求值,還可以使用模式匹配。Hmm,取一個變量,對它模式匹配,執行對應的代碼塊。好像在哪兒聽過?啊,就是函數定義時參數的模式匹配!好吧,模式匹配本質上不過就是case語句的語法糖而已。這兩段代碼就是完全等價的:
~~~
head'?::?[a]?->?a???
head'?[]?=?error?"No?head?for?empty?lists!"???
head'?(x:_)?=?x??
~~~
~~~
head'?::?[a]?->?a???
head'?xs?=?case?xs?of?[]?->?error?"No?head?for?empty?lists!"???
??????????????????????(x:_)?->?x??
~~~
看得出,_case_表達式的語法十分簡單:
~~~
case?expression?of?pattern?->?result???
???????????????????pattern?->?result???
???????????????????pattern?->?result???
???????????????????...??
~~~
_expression_匹配合適的模式。如料,第一個模式若匹配,就執行第一個代碼塊;否則就交給下一個模式。如果到最后依然沒有匹配的模式,就會產生一個運行時錯誤。
函數參數的模式匹配只能在定義函數時使用,而case表達式可以用在任何地方。例如:
~~~
describeList?::?[a]?->?String???
describeList?xs?=?"The?list?is?"?++?case?xs?of?[]?->?"empty."???
???????????????????????????????????????????????[x]?->?"a?singleton?list."????
???????????????????????????????????????????????xs?->?"a?longer?list."??
~~~
這在表達式中作模式匹配很方便,由于模式匹配本質上就是case表達式的語法糖,那么寫成這樣也是等價的:
~~~
describeList?::?[a]?->?String???
describeList?xs?=?"The?list?is?"?++?what?xs???
????where?what?[]?=?"empty."???
??????????what?[x]?=?"a?singleton?list."???
??????????what?xs?=?"a?longer?list."??
~~~