haskell学习

工具

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类型转换为字符串。
  • 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模块,所以你能使用到filtermap等常用函数
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 5Nothing
  • 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阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容