工具
haskell platform,直接百度安装.
打开控制台输入ghci即进入交互模式。
假如定义了myfunction.hs
,在ghci中输入:l myfunction.hs
便会进行加载。‘
细节
- 函数的优先级比运算高,如
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类型转换为字符串。 -
Read
read
函数读取字符串转换为某Read成员类型。如read "5" + 5
,read "5"::Int
这种方式可以指定转换的类型,若不知道类型的情况下。其中::Int
表示类型注释,明确前边的类型。 - Enum表示可枚举,好处是可以使用succ,pred等函数来取得上一个,下一个。
-
Bounded表示成员有上下界。如
minBound :: Int
=>-2147483648
- Num所有数字类型
-
Integral所有整数类型,如
Int
,Integer
即为Integral类型 -
Floating 同上。
Float
和Double
为该类型
(二) -
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) 200
和200/10
是等价的。而(200/) 10
和200/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,即从右边开始折叠。
foldl1
和foldr1
则与foldl
和foldr
相似,不过他们的初始值为数组的第一个元素(首个或末尾),只不过计算空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]
函数组合:
数学中的函数组合为:
在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]
-
or
与and
类似,不过是or逻辑 -
any
和all
表示取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] -
dropWhile
与takeWhile
相似,不过符合条件就删掉,直到遇到不符合条件的。dropWhile (/=' ') "This is a sentence"
返回" is a sentence"
-
span
与takeWhile
相似,不过其返回两个list,第一个list是takeWhile返回的list,第二个list是剩余的list. -
sort
排序一个list -
group
取一个list作参数,将其中相邻并相等的元素各自归类。 -
isPrefixOf
与isSuffixOf
检查一个List是否以另一个List开头或结尾。 -
partition
取一个限制条件和List作为参数,返回两个List,第一个List包含所有符合条件的元素,第二个List包含剩余的元素。 -
find
接受一个函数和List,返回第一个符合条件的元素,这个元素是个Maybe值。返回如下面的形式Just 5
、Nothing
-
findIndex
与find
相似,不过返回的是Maybe的索引。 -
elemIndex
与elem
相似,返回的是Maybe的索引。 -
lines
传入一个字符串,将字符串分行:lines "first line\nsecond line\nthird line"
返回["first line","second line","third line"]
-
unlines
是lines
的反函数 -
words
和unwords
可以把一个字符串分成一组单词或运行相反的操作。 -
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
将数字转换为字符
可以通过ord
和chr
来实现一个字符串平移的函数如:
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)]
-
toList
是fromList
的反函数 -
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)]
-
map
Map中的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 a
中Nothing
并不是事先定义的,并且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
中的Circle
及Rectangle
类型都导出,这样外部就可以使用到这两个构造子,如果只导出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