haskell學習

工具

haskell platform,直接百度安裝.

打開控制臺輸入ghci即進入交互模式。

假如定義了myfunction.hs,在ghci中輸入:l myfunction.hs便會進行加載?!?/p>

細節

  • 函數的優先級比運算高,如succ 2*3會先計算succ 2。
  • 非運算使用not
  • 真值用True,假值用False,注意開頭大寫。
  • /=表示不等
  • 取余用的是mod
  • 單行注釋使用--,多行注釋使用{- -}

初學者第一個函數

doubleMe x = x * x.
創建test.hs,鍵入以上函數,加載方式為::l test.hs,之后就可使用。
也可以在test.hs鍵入兩行函數:

doubleMe x y = x*y + x*y
doubleUs x y = doubleMe x y + doubleMe x y

之后再重新加載,兩個函數都可以使用。

在haskell中if then else是一種表達式。如

doubleSmallNumber x = if x>100 then x else x*2

可以看到,then esle是不可省略的,必須有一個確定的最終值。

首字母大寫的函數是不允許的。

類似下面的沒有參數的函數,其實就是定義了一個常量字符串:

someName = "hahahhahahahahahah..."

list

在ghci下使用let定義一個常量

let a = 1
  • 字符串"aaaa"其實就是list的語法糖=> ['a','a','a','a']
  • list中的所有的元素的數據類型必須相同。
  • list中是通過++進行合并操作。[1,2,3] ++ [4,5,6].但注意使用++進行合并字符串的時候,其會遍歷++左邊的字符串,如果左邊字符串較長,會浪費很長時間,這個時候,可以使用:運算符,表示插入操作,如:1 : [2,3,4,5]
  • [1,2,3]實際是1:2:3:[]的語法糖,[]表示空list
  • 按照索引取list中的元素,可以使用!!,如:"im ok"!!0會打印出i字符
  • list可以比較大小,但是會從前往后依次比較,直到遇到不等的關系
  • head取list頭,tail取除head外的數據,last去list的尾,init取除last外的數據
  • length返回list的長度,null檢查list是否為空,reverse將一個list反轉。take返回一個list的前幾個元素,如:take 3 [1,2,3,4,5],drop與take用法大體相同,會刪除list的前幾個元素。maximum返回list中最大的元素,minimum返回最小,sum,elem判斷list中是否有某個元素,使用中綴形式。

range

  • [1..20]表示從1到20的list
  • ['a'..'z']表示從a到z的list
  • [1,3..20]表示[1,3,5,7,9,11,13,15,17,19]
  • 不推薦range使用浮點數
  • take 24 ([2,4..])可以獲取24個2往后的偶數,和[2,4..2*24]是一樣的,但是前者好點
  • take 10 (cycle [1,2,3]),cycle表示某個列表的循環
  • take 10 (repeat 5),repeat表示某個元素的循環。另一種簡便方法是replicate 3 10 => [10,10,10]

list comprehension

定義集合的操作

  • [x*2 | x <- [1..10]]
  • 有條件的集合:[x*2 | x<-[1..10],x*2 >= 12],逗號隔開
  • 取50到100間除7余3的數: [x | x<-[50..100],x`mod`7 == 3]
  • 偶數轉換為even,基數為odd:[if x `mod` 2 == 0 then "even" else "odd" | x <- [1..10]]
  • 多個限制條件:[ x | x <- [10..20], x /= 13, x /= 15, x /= 19]
  • 多個元素:[ x*y | x <- [2,5,10], y <- [8,10,11]]
  • length' xs = sum [1 | _ <- xs] 表示獲取xs列表的長度,其中_表示不關心當前值。
  • 嵌套list:let xxs = [[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],[1,2,4,2,1,6,3,1,3,2,3,6]] => [ [ x | x <- xs, even x ] | xs <- xxs]

tuple

tuple是元組。

  • list中的元素數據類型必須相同,但是tuple中的元素數據類型不必相同
  • [(1,2),(8,11,5),(4,5)]會報錯,因為(1,2)(8,11,5)不是相同類型。而(1,2)(4,5)是相同類型
  • tuple可以用來表示某個人的一系列信息,如("zhangsan","henan",19)
  • tuple中的項的數目是確定的,不允許追加
  • fst取tuple(二元組)的首元素,snd取tuple(二元組)的尾元素
  • zip函數將兩個交叉list生成tuple形式的list:zip [1,2,3,4,5] [5,5,5,5,5]=>[(1,5),(2,5),(3,5),(4,5),(5,5)]

type

  • 在ghci中使用:t來獲取任何表達式的類型.如:t 'a'輸出'a'::Char
  • 凡是明確的類型,其首字母必須為大寫的字母,所以一般對于函數來說,首字母不能大寫。
  • 函數也有類型,定義函數的時候,加上參數的類型和輸出類型是好習慣,如:removeNonUppercase :: [Char]->[Char]表示輸入的是字符串,輸出還是字符串。其中[Char]String是等價的。如果是多個參數,則使用以下形式:addThree :: Int -> Int -> Int -> Int表示輸入三個整形,輸出1個整形。
  • Integer也表示整數,但是是無界的,所以可以表示大數
  • 某些函數定義的時候傳入的不是參數,而是a,b這種,這些表示類型參數,表示可以傳入任何類型。如:t head=>head::[a] -> a
  • :t (==)可以查看==的類型,輸出為(==) :: Eq a => a -> a -> Bool,=>為類型約束,表示a這種類型應該為Eq類型,即相同類型。

typeclass

(一)

  • 可以把typeclass想像為java中的interface
  • Eq表示可判斷相等性的類型,除函數以外所有類型都屬于Eq
  • Ord可包含比較大小的類型
  • Show除函數以外所有類型都是show類型,show函數可以取任意Show類型轉換為字符串。
  • Readread函數讀取字符串轉換為某Read成員類型。如read "5" + 5 ,read "5"::Int這種方式可以指定轉換的類型,若不知道類型的情況下。其中::Int表示類型注釋,明確前邊的類型。
  • Enum表示可枚舉,好處是可以使用succ,pred等函數來取得上一個,下一個。
  • Bounded表示成員有上下界。如minBound :: Int => -2147483648
  • Num所有數字類型
  • Integral所有整數類型,如Int,Integer即為Integral類型
  • Floating 同上。FloatDouble為該類型
    (二)
  • class關鍵字可以創造一個typeclass

函數

succ 6 輸出7 表示某個值的后繼
min 4 5,max 4 5.

模式匹配

其實就是類似switch
如下代碼:

lucky::(Integral a)=> a -> String
lucky 7 = "its 7"
lucky x = "its not 7"

如果匹配到7,則后續不執行。如果未匹配到,則所傳參數綁定到x上。但是如果是下面的代碼:

lucky::(Integral a)=> a -> String
lucky x = "its not 7"
lucky 7 = "its 7"

則報錯,因lucky 7 已經被加載,lucky x已經包含了lucky 7。

可以通過這種方式實現遞歸:

factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)

_符號表示不關心值,如:

first :: (a,b,c) -> a
first (x,_,_) = x

可以通過:來匹配List,因為[a,b,c]本來就是a:b:c:[]的語法糖
a:b會將[1,2,3]匹配成1:[2,3],而如果匹配單元素List,可以寫為(x:[]),匹配雙元素List:(x:y:[]),也可以不加括號,寫為[x][x,y],但是(x:y:_)必須加括號,注意此處加上括號并不是表示tuple.因為([1,2,3])還是一個數組,單元素的tuple其實毫無意義。
實現head:

head' :: [a] -> a
head' [] = error "Can't call head on an empty list, dummy!"
head' (x:_) = x

error是一個函數,會導致程序崩潰。

實現length:

length' :: (Num b) => [a] -> b
length' [] = 0
length' (_:xs) = 1 + length' xs

xs@(x:y:ys)類似這種形式的模式,xs就表示整體,如:

capital :: String -> String
capital "" = "Empty string, whoops!"
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]

guard

類似if語句,模式匹配是匹配值,而guard則匹配bool

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!"

與模式匹配不同的是,guard模式是通過判斷表達式的真假來運作的。直到遇到一個為真的表達式,并且|必須與前邊有縮進。

在定義函數的時候如func a b也可以這么定義a `func` b
guard也可以和模式匹配進行配合,如果guard沒有匹配到結果,后續沒有代碼則報錯,但是如果后續還有模式匹配的代碼則繼續執行,比如實現take:

myTake :: (Num b, Ord b) => [a] -> b -> [a]
myTake _ b
        | b <= 0 = []
myTake [] _ = []
myTake (x:xs) b = x : myTake xs (b-1)

guard后邊跟著模式匹配,代碼不會報錯。

where

在guard模式中,可以通過where來引用某個復雜的變量值,這樣就不用重復出現某個復雜的表達式了。如:

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也支持模式匹配,如:

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也可以定義函數:

calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi w h | (w, h) <- xs]
    where bmi weight height = weight / height ^ 2

let

格式:let [bindings] in [expressions] let in是一個表達式,其值是expressions表示的值,bindings中進行局部變量的定義。與where不同的是,let in是一個表達式,所以可以隨處安放,同if else then,而where是一個語法結構,一般只用在guard后綴。

maax x = let y = 1 in y

let也可以定義局部函數:

[let square x = x * x in (square 5, square 3, square 2)]

定義多個名字,使用;隔開

(let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey "; bar = "there!" in foo ++ bar)

使用模式匹配:

(let (a,b,c) = (1,2,3) in a+b+c) * 100

用在list中:

calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]

(w, h) <- xs 這里無法使用 bmi 這名字,因為它在 let 綁定的前面。

case

case 是一個表達式,與switch相似:
格式為:

case expression of pattern -> result
                   pattern -> result
                   pattern -> result

如:

head xs = case xs of [] -> error "error"
                      (x:_) -> x

模式匹配本質上就是case的語法糖。
上述代碼寫成模式匹配則為:

head [] = error "error"
head (x:_) = x

遞歸

haskell中實現while和for的方案就是遞歸。

實現取列表中最大值:

maximum' :: (Ord a) => [a] -> a
maximum' [] = error "maximum of empty list"
maximum' [x] = x
maximum' (x:xs)
    | x > maxTail = x
    | otherwise = maxTail
    where maxTail = maximum' xs

遞歸實現的快排,真是特妹的優雅!!

quicksort :: (Ord a) => [a] -> [a]
quicksort [] = []
quicksort (x:xs) =
    let smallerSorted = quicksort [a | a <- xs, a <= x]
        biggerSorted = quicksort [a | a <- xs, a > x]
    in smallerSorted ++ [x] ++ biggerSorted

如實現的reverse函數:

myReverse :: [a] -> [a]
myReverse [] = []
myReverse (x:xs) = myReverse xs ++ [x]

一定要注意,最后一行為什么不寫成:myReverse xs : x呢,原因是沒有[1,2,3]:3這種寫法,但是有[1,2,3] ++ [3]這種寫法或者1:[1,2,3]

遞歸的固定模式可以描述成這樣:先定義一個邊界條件,再定義函數,讓它從一堆元素中取一個并做點事情后,把剩余的元素重新交給該函數。

高端函數

指可以接受函數作為參數,也可以返回函數作為結果。

curried functions

原則上haskell的所有函數都只有一個參數,定義的函數傳多個參數是怎么來的?
所有多個參數的函數都是curried function,如func a b傳入兩個參數,實際上是func a回傳了一個函數,并將b傳給該函數。
如:max::(Ord a)=> a->a->a可以看作max::(Ord a) => a -> (a -> a)
max a表示返回一個a->a類型的函數。那么max a b可以理解為 (max a) b

所以如果想要構造一個和7比較大小的函數,直接調用max 7即可,因為max 7會返回一個(a->a)的函數。 如下代碼:

max7 :: (Ord a, Num a) => a -> a
max7 = max 7

所以此時max7為(a->a)的函數。
查看以下代碼:

ghci> let multWithEighteen = multTwoWithNine 2
ghci> multWithEighteen 10
180

以上代碼可以看出,一個參數沒有傳入全的函數會返回另一個函數,等待剩余的參數傳遞完畢。

中級函數也可以返回函數:

divideByTen :: (Floating a) => a -> a
divideByTen = (/10)

這個例子就可知道形如(/10) (+3) (++ "abc") (3:) (3+)都是函數。
同樣的(*) (+) (++)也都是函數,不過這樣函數的參數為兩個。
(/10) 200200/10是等價的。而(200/) 10200/10也是等價的。

以下代碼調用某個函數兩次:

applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)

從代碼中可以看出 第一個參數是(a->a)類型,也就是該類型的函數,在這括號是必須的,表示第一參數必須是一個函數,第二個參數可以是任意元素,最后一個參數返回某個元素。
那么調用:applyTwice (+3) 10 其實就是((+3) ((+3) 10))

map和filter

map:

map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs

map是結合高端函數和遞歸來實現的。

filter:

filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter p (x:xs)
    | p x = x : filter p xs
    | otherwise = filter p xs

lambda

匿名函數,樣式是\ 參數 -> 函數體,通常會用括號將lambda函數括起來,否則會引起歧義。
\x -> x + 3表示輸入x輸出x+3。同樣lambda可以取多個參數:\a b -> a + b。再看個復雜點的例子:

addThree :: (Num a) => a -> a -> a -> a
addThree = \x -> \y -> \z -> x + y + z

這種樣式也是可以。

其他高級函數

foldl其實就是Java stream中的reduce
foldl是折疊,將一個數組從前折到后,傳入的第一個參數是函數,第二個參數是初始值,第三個參數為List,
foldl 是fold left, 而foldr則是fold right,即從右邊開始折疊。
foldl1foldr1則與foldlfoldr相似,不過他們的初始值為數組的第一個元素(首個或末尾),只不過計算空List則會報錯。

foldl (\x y -> x+y) 0 [1,2,3]
foldl (+) 0 [1,2,3]
let sum = foldl (+) 0 [1,2,3] in sum [1,2,3]

因為fold函數的特殊性,傳入List傳入Item,所以可以用來實現一些遍歷的庫函數,如max,min等。只要滿足返回的結果不為List,都可以想辦法完成。

scan函數與fold函數不同的是,scan會將每步的計算結果保存在List中,如:

scanl (+) 0 [1,2,3,4] 輸出:[0,1,3,6]

其同樣有scanl,scanr, scanl1,scanr1等函數

$操作符

$操作符的優先級最低,所以其可以充當(),如sqrt 3 + 4 + 5表示根3 + 4 + 5的值,如果我想取sqrt(3 + 4 +5)那么也可以寫成sqrt $ 3+4+5其首先會計算符號右邊的值。 同樣的,如果有一些函數的括號特別多,就可以使用符號來簡化代碼:sum (map sqrt [1..130])可以寫成sum $ map sqrt [1..130]

函數組合:

數學中的函數組合為:

image.png

在haskell中可以使用.號來表示,如:map (\x -> negate (abs x)) [5,-3,-6,7,-3,2,-19,24]表示將所有的x先獲取絕對值,然后通過negate取負,那么使用函數組合則表示為:map (negate . abs) [5,-3,-6,7,-3,2,-19,24],這樣表示的函數更為方便,但是注意函數組合的順序。
函數組合可以構造更多的函數,形如f1 . f2 a的函數實際上和f1(f2 a)等價的。

模塊

類比Java中的類,裝在模塊的方式是通過import
在某個.hs文件中使用import Data.List可以將Data.List模塊裝入,這個模塊有很多操作List的方法。
如果使用ghci交互界面來裝載模塊,可以使用:m Data.List,使用:m Data.List Data.Map Data.Set裝載多個函數。其實ghci初始的時候會裝載Prelude模塊,所以你能使用到filter,map等常用函數
import Data.List (nub,sort)只裝載nub和sort函數
import Data.List hiding (nub)除了nub函數,其他都裝載
import qualified Data.Map 關鍵字qualified表示,如果調用該模塊中的某個與外部函數同名的函數,就必須加上Data.Map前綴。
import qualified Data.Map as M同名函數加M前綴即可:M.filter

Data.List

Data.List有很多方便處理List的函數,如map,filter等,為了方便,將Data.List中的一些函數直接加入到haskell中,所以調用的時候,就不用再寫Data.List前綴。
以幾個罕見函數舉例:

  • intersperse '.' "money":類似java中的join函數,將第一個參數.加到list中每兩個元素的中間
  • intercalate " " ["I","Love","you"]:跟上邊的 函數類似,不過將第一個參數換成了list
  • transpose [[1,2,3],[4,5,6],[7,8,9]] = > [[1,4,7],[2,5,8],[3,6,9]] ,不做解釋了,心累。
  • concat將一組list鏈接為一個list。concat ["123","456"]輸出"123456"
  • and and取一組Bool的list,只有所有的元素都為True才會返回True,否則返回False.and $ map (>4) [5,6,7,8]
  • orand類似,不過是or邏輯
  • anyall表示取list判斷是否有一個符合或者都符合如:any (== 4) [2,3,4,5,6]返回True,all (>4) [5,6,7,8]返回True.
  • splitAt分割list,在指定位置斷開splitAt 3 "heyman"返回("hey","man")
  • takeWhile從list中取元素,一旦遇到不符合條件的元素就停止:takeWhile (>3) [6,5,4,3,2,1,2,3,4,5,4,3,2,1] 返回[6,5,4]
  • dropWhiletakeWhile相似,不過符合條件就刪掉,直到遇到不符合條件的。dropWhile (/=' ') "This is a sentence" 返回 " is a sentence"
  • spantakeWhile相似,不過其返回兩個list,第一個list是takeWhile返回的list,第二個list是剩余的list.
  • sort排序一個list
  • group取一個list作參數,將其中相鄰并相等的元素各自歸類。
  • isPrefixOfisSuffixOf檢查一個List是否以另一個List開頭或結尾。
  • partition取一個限制條件和List作為參數,返回兩個List,第一個List包含所有符合條件的元素,第二個List包含剩余的元素。
  • find接受一個函數和List,返回第一個符合條件的元素,這個元素是個Maybe值。返回如下面的形式Just 5、Nothing
  • findIndexfind相似,不過返回的是Maybe的索引。
  • elemIndexelem相似,返回的是Maybe的索引。
  • lines傳入一個字符串,將字符串分行:lines "first line\nsecond line\nthird line" 返回 ["first line","second line","third line"]
  • unlineslines的反函數
  • wordsunwords可以把一個字符串分成一組單詞或運行相反的操作。
  • nub將一個List中的重復元素全部篩掉。
  • delete 刪除List中第一個出現的某元素:delete 'h' "hey there"返回 "ey there",delete 'h'. delete 'h' "hey there"返回"ey tere"
  • \\差集,左集扣除右集合后的集合
  • union并集
  • intersection交集
  • insert將某個元素插入的可排序的List中,插入到首個大于等于該值的前邊

有個函數叫on函數,其定義如下:

on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
f `on` g  \x y -> f (g x) (g y)

即把g函數的結果傳遞給f函數,所以(==) `on` (>0)就表示函數\x y -> x>0 == y >0 ,同理 compare `on` length就表示\x y -> length x `compare` length y

Data.Char

處理字符串的模塊

  • isControl 判斷一個字符是否是控制字符。
  • isSpace 判斷一個字符是否是空格字符,包括空格,tab,換行符等. -
  • isLower判斷一個字符是否為小寫.
  • isUper 判斷一個字符是否為大寫。
  • isAlpha 判斷一個字符是否為字母.
  • isAlphaNum 判斷一個字符是否為字母或數字.
  • isPrint 判斷一個字符是否是可打印的.
  • isDigit 判斷一個字符是否為數字.
  • isOctDigit 判斷一個字符是否為八進制數字.
  • isHexDigit 判斷一個字符是否為十六進制數字.
  • isLetter 判斷一個字符是否為字母.
  • isMark 判斷是否為 unicode注音字符,你如果是法國人就會經常用到的. - isNumber 判斷一個字符是否為數字.
  • isPunctuation 判斷一個字符是否為標點符號.
  • isSymbol判斷一個字符是否為貨幣符號.
  • isSeperater 判斷一個字符是否為 unicode 空格或分隔符.
  • isAscii 判斷一個字符是否在 unicode 字母表的前 128 位。
  • isLatin1 判斷一個字符是否在 unicode 字母表的前 256 位.
  • isAsciiUpper 判斷一個字符是否為大寫的 ascii 字符.
  • isAsciiLower 判斷一個字符是否為小寫的 ascii 字符.
  • ord將字符轉換為數字 chr將數字轉換為字符
    可以通過ordchr來實現一個字符串平移的函數如:
import Data.Char
encode :: Int -> String -> String
encode salt msg = let msgs = map ord msg
        |       |     digs = map (+ salt) msgs
        |       |     in map chr digs
decode :: Int -> String -> String
decode salt msg = encode (-salt) msg

則輸入encode 1 "abc"輸出"bcd",同樣decode 1 "bcd"輸出"abc"

Data.Map

Map是key不重復一種KV鍵值對集合(List)。

  • fromList取一個關聯列表,返回一個與之等價的Map ,eg:fromList [("a",1),("b",2)]
  • toListfromList的反函數
  • insert插入一個新的KV,如:Map.insrt 3 100 Map.empty
  • size返回Map的大小
  • singleton返回只有一個KV的Map:Map.singleton 3 100
  • lookup查詢對應的鍵值
  • member某個鍵是否存在一個Map中,返回Bool:Map.member 3 $ Map.fromList [(2,5),(4,5)]
  • mapMap中的map操作的是V值:Map.map (*100) $ Map.fromList [(1,1),(2,4),(3,9)]返回fromList [(1,100),(2,400),(3,900)]
  • filter同樣操作的是V值。
  • keys返回一個由K組成的List
  • elems返回一個由V組成的List

Data.Set

Set中的數據是唯一的,元素是必須可排序的。注意其因為和Data.List很多的函數重復,所以導入的時候使用import qulified Data.Set as Set的方式。創建一個Set是通過fromList函數,其他函數就不列舉了。

創建自己的模塊

在根目錄創建geometry.hs。

module Geometry
( sphereVolume,
sphereArea
) where

sphereVolume :: Float -> Float
sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3)

sphereArea :: Float -> Float
sphereArea radius = 4 * pi * (radius ^ 2)

分級模塊創建方式,創建geometry/sphere.hs
則在sphere.hs中的內容為:

module Geometry.Sphere
( volume,
area
) where

volume :: Float -> Float
volume radius = (4.0 / 3.0) * pi * (radius ^ 3)

area :: Float -> Float
area radius = 4 * pi * (radius ^ 2)

構造自己的Types和Typeclasses

Bool在標準函數庫的定義為:

data Bool = False | True

構造自己的類型,一種方法就是使用data關鍵字。
如構造一個圖形,該圖形可以是Circle也可以是Rectangle,定義為:

data Shape = Circle Float Float Float | Rectangle Float Float Float Float

上述Circle表示圓形,后邊的跟著的三個Float表示一個Circle由三個Float組成。
創建完Shape后會自動生成Circle及Rectangle類型,此時查看其類型聲明為:

ghci> :t Circle
Circle::Float -> Float -> Float -> Shape

所以Circle 50 50就是返回一個Shape類型,到這就明白了,并不是先有Shape后有Circle,而是現有Circle后有了Shape
同樣類型:data Maybe a = Nothing | Just aNothing并不是事先定義的,并且Nothing是一個值構造子,也不是類型。要創建個某個類型的值,必須使用后邊的值構造子,即你不能使用Shape關鍵字來創建一個Shape類型。
構造的方式為:

surface :: Shape -> Float
surface (Circle _ _ r) = pi * r ^ 2
surface (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1)

注意看傳入的Circle和Rectangele的方式不一樣。
那么調用方式為:

ghci> surface $ Circle 10 20 30
ghci> surface $ Rectange 0 0 100 100

但是下面的調用時錯誤的

ghci>Circle 10 20 30 

因為Circle 10 20 30并不是Show類型,只有Show類型的數據才能被顯示。
那么修改構造的方式為:

data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)

即在定義的后邊加上deriving (Show).然后再調用就可以顯示出來了。

還可以這么定義:

data Point = Point Float Float deriving (Show)
data Shape = Circle Point Float | Rectangle Point Point deriving (Show)

這樣在定義函數的時候就應該是:

surface::Shape -> Float
surface (Circle _ r) = pi * r ^ 2
surface (Rectangle (Point x1 y1) (Point x2 y2))  = (abs $ x2 - x1) * (abs $ y2 - y1)

那么調用就變為:

surface (Rectangle (Point 0 0) (Point 100 200))

前邊知道了如何導出函數的模塊,那么這種數據定義的模塊應該怎么導入呢,方式如下:

module Shapes
( Point(..)
, Shape(..)
, surface
, nudge
, baseCircle
, baseRect
) where

即數據定義使用Shape(..)的方式。..表示將Shape中的CircleRectangle類型都導出,這樣外部就可以使用到這兩個構造子,如果只導出Circle則需要寫為Shape(Circle)

Record Syntax

定義一個人的名字,并且生成各種函數:

data Person = Person {firstName :: String,
lastName::String,
age :: Int,
phoneNumber::String
} deriving (Show)  

這樣就生成了Person類型以及firstName``lastName..等等的函數.
創建的時候則為

> Person {firstName="q",lastName="xg",age=10,phoneNumber="123456"}

并且打印Person的時候,會將firstName等等顯示出來。

Type parameters

類型參數,類似泛型,如:

data Maybe a = Nothing | Just a

如果傳給Maybe的是Char,他就是Maybe Char類型。如:Maybe 'a'就是Maybe Char類型的。
Nothing也是Maybe a類型,所以可以是Maybe Int,也可以是Maybe String
再看一個例子:

data Vector a = Vector a a a deriving (Show)
vplus :: (Num t) => Vector t -> Vector t -> Vector t
...

Derived instances

上邊有涉及到Eq,Ord,Num的typeclass等等類似Java interface的東東,比如Int屬于Num,但是如何實現這些interface呢,用java表達就是如何implement,方法就是通過派生deriving,使用data創建類型的時候,后續跟上deriving Num就表明該類型屬于Num,并自動加上對應的行為。
但是注意data創建的是instance,而不是typeclass,且等號左邊是類型構造子,等號右邊是值構造子,一般在創建某種類型的值時,使用的是值構造子,而類型構造子則用于函數聲明等位置處,類型構造子無法來創建某一個值,一定要注意,比如下邊創建了一個Person的類型,那么要創建一個Person類型的值,就不能使用Person創建,而是要使用等號右邊的關鍵字(值構造子)創建.
只要派生為Eq類,那么定義的數據類型就有可比性:

data Person = Person{firstName :: String,lastName :: String,age::Int} deriving Eq

haskell會檢查值構造子是否一致,再用==檢查其中的所有數據(必須都是Eq的成員)是否一致。
同樣也可以指定為多個類型:

data Person = Person{firstName :: String,lastName :: String,age::Int} deriving (Show,Eq,Read)

若將一個類型指定派生為某個A類型,則該類型的所有參數必須都屬于A類型,才可以進行派生。
舉一個經常會遇到的例子:

data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday 
        deriving (Eq, Ord, Show, Read, Bounded, Enum)

每個值構造子都沒有參數,因為派生了Eq,所以具有可比性,派生了Ord,所以可以比較大小,有Bounded,可以使用minBound獲取下界,有了Enum,可以使用succ等函數進行枚舉。

在data聲明中,=左邊是類型構造子,=右邊用|分割的是值構造子,要注意區分。因為函數生命中只能填寫類型,如果分不清楚,可能就將值構造子填入,導致死都不知道怎么死的。
注:不要在data中添加類型約束,即便看起來沒問題。

Type synonyms

給某個類型提供別名:

type String = [Char]

又如:

type PhoneNumber = Stirng
type Name = String
type PhoneBook = [(String,String)]

那么定義某個函數則為:

inPhoneBook::Name -> PhoneNumber -> PhoneBook -> Bool

而如果不定義別名,則該函數為:

inPhoneBook::String -> String -> [(String,String)] -> Bool

定義別名也可以有參數:

type AssocList k v = [(k,v)]

類型別名也可以定義不全的類型構造子.
類型別名一般可以用于函數類型聲明或類型注釋上,如果要創建一個新類型,不能用類型名+參數的方式去創建,一定要明白類型構造子和值構造子的區別。
類似函數在定義的時候進行聲明,變量在定義的時候也可以進行聲明,所以將來::看成通用的一類看待會更好理解這門語言。

a::Int
a=1

是可以工作的,同樣函數在定義的時候也是這種格式:

test::a->a
test a = a+1

只不過這個地方用到了泛型,你也可以這樣聲明

test :: Int -> Int
test a=a+1

遞歸地定義數據結構

如List [1]1:[]的語法糖,[1,2]1:2:[][1,2,3]1:2:3:[]
可以看到List的類型類似這樣x:listx其中x是匹配到的第一個元素,而listx是一個匹配的新的list,每個List類型都可以匹配為x:listx類型,并且每個listx都可以匹配為x:listx類型,除了基礎類型[],所以在定義List的時候就可以通過遞歸來進行定義,如將[]替換為empty,:替換為Cons,那么定義List就可以是這樣:
data List a = Empty | Cons a List a deriving (Show ,Read,Eq,Ord)
那么在創建List的時候就可以這樣,1 `Cons` Empty注意最后的Empty就已經表明其是一個List,所以創建的遞歸數據類型一般都是需要最基礎的類型,比如Empty,比如原始List的[]等。
Cons也可以替換為一個符號,如

data List a = Empty | a :-: (List a) deriving (Show, Read,Eq, Ord)

那么如果要定義該符號的優先級,需要加上infixr關鍵字,其后的數字表示優先級:

infixr 5 :-:
data List a = Empty | a :-: List a deriving (Show, Read,Eq, Ord)

那么如果要做模式匹配的話,是可以這樣做的:

a:-:b = xxxx

原因就是:-:是構造子,而模式匹配就是通過構造子來進行匹配的,同理[]也是構造子,:也是構造子,所以你可以通過x:xs來進行匹配,注意模式匹配中匹配的都是值構造子,一定不要寫類型。
二叉樹

data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show,Read,Eq)

創建節點

singleton:: a -> Tree a
singleton x = Node x EmptyTree EmptyTree

插入節點

treeInsert::(Ord a)=> a->Tree a->Tree a
treeInesrt x EmptyTree = singleton x
treeInsert x (Node a left right)
        | x == a = Node x left right
        | x < a = Node a (treeInsert x left) right
        | x> a = Node a left (treeInsert x right)

查找節點

treeElem :: (Ord a) => a -> Tree a -> Bool
treeElem x EmptyTree = False  
treeElem x (Node a left right)
    | x == a = True
    | x < a = treeElem left
    | x > a = treeElem right
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • //Clojure入門教程: Clojure – Functional Programming for the J...
    葡萄喃喃囈語閱讀 3,712評論 0 7
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,719評論 18 399
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,785評論 18 139
  • 第5章 引用類型(返回首頁) 本章內容 使用對象 創建并操作數組 理解基本的JavaScript類型 使用基本類型...
    大學一百閱讀 3,261評論 0 4
  • 看著路旁綠油油的田野,路旁的樹,一直在后退,我要回家了。 謝謝閱讀。非常感謝你,感謝你看著我成長。
    我心我愿秀閱讀 149評論 0 4