Haskell指南

2016-09-15
9分钟阅读时长

类型

Type

Type 首字母必须大写, 可用 :t data 测试

ghci> :t 'a'
'a' :: Char
ghci> :t True
True :: Bool
ghci> :t "Hello!"
"Hello!" :: [Char]
ghci> :t (True, 'a')
(True, 'a') :: (Bool, Char)

Type 包括以下

Int             与机器字长有关的整数
Integer         无界的整数
Float           单精度浮点数
Double          双精度浮点数
Char            字符(单引号)
Bool            布林值
[Type]          数组
(Type, Type)    Tuple(包括各种Tuple类型 和 空Tuple)
Type -> Type    函数

Type variable

用于写出类型无关的函数(多态函数)

ghci> :t head
head :: [a] -> a
ghci> :t fst
fst :: (a, b) -> a

以上, ab 即为类型变量

Typeclass

ghci> :t (==)
(==) :: (Eq a) => a -> a -> Bool
ghci> :t (>)
(>) :: (Ord a) => a -> a -> Bool
ghci> show 3
"3"
ghci> read "True" || False
True
ghci> [LT..GT]
[LT, EQ, GT]
ghci> succ 'B'
'C'
ghci> maxBound ::(Bool, Int, Char)
(True,9223372036854775807,'\1114111')
ghci> :t maxBound
maxBound :: Bounded a => a
ghci>:t 20
20 :: Num t => t

对于类型约束(=> 左边的部分)中的 Eq 即为 Typeclass

Eq              包含可判断相等的类型
                除函数外所有类型属于Eq
Ord             包含可比较大小的类型
                (比较结果为GT, LT, EQ三者之一, 用 compare 可测试结果)
                若要成为 Ord 成员必须先成为 Eq 成员
                被 < <= >= > 之类用于比较大小的函数使用
                成为 Ord 的成员就可以进行加减运算而不一定是数字
Show            可用字符串表示的类型
                被 show 函数使用
Read            可用字符串转化的类型
                被 read 函数使用
Enum            表示连续的类型, 即每个值都有 后继子 和 前驱子(Bounded 情况见下)
                被 succ, pred, .. 函数使用
                包含类型有 (), Bool, Char, Ordering,
                Int, Integer, Float, Double
Bounded         表示有上限和下限的类型
                如果 Tuple 中的每一项属于 Bounded 则整个 Tuple 也属于 Bounded
                被 minBound, maxBound 函数(多态常量)使用
Num             表示数字
                被 数字 (多态常量)使用
                只有成为 Show, Eq 的成员才可以成为 Num 的成员
Integral        表示整数
                包含类型有 Int, Integer
Floating        表示浮点类型
                包含类型有 Float, Double
RealFloat
RealFrac

类型转换

fromIntegral

对于某些函数得到的 Integral 类型转换为更为通用的 Num 类型

length :: [a] -> Int
fromIntegral :: (Num b, Integral a) => a -> b

指定类型

对于 read 函数, 有

ghci> read "5" - 2
3
ghci> read "5"
Exception: Prelude.read: no parse
ghci> :t read
read :: Read a => String -> a

若不能确定返回的类型, 则需要指定相应的类型, 如下

ghci> read "5" :: Int
5
ghci> read "5" :: Float
5.0
ghci> read "(3, 'a')" :: (Int, Char)
(3, 'a')

Type 和 Typeclass

基本类型

 haskell-main/basetype.png

数字类型

 haskell-main/numtype.png

数字类型类

 haskell-main/numtypeclass.png

Functor typeclass

Funtor Typeclass 的成员形如 f (Maybe a) 等价于 Maybe (f a)

class Functor f where
  fmap :: (a -> b) -> f a -> f b

instance Functor Maybe where
  fmap f (Just x) = Just (f x)
  fmap f Nothing = Nothing

对比 自定义Instance 中的 (Maybe a)

这里的 Functor 接受一个型别参数的型别构造子而不是具体的型别

即应该把 fmap:: (a->b) -> f a -> f b 中的 f 替换成 Maybe, 而不是 (Maybe a)

Either a b 对于两个参数的类型, 应该固定一个参数

instance Functor (Either a) where
  fmap f (Right x) = Right (f x)
  fmap f (Left x) = Left x

Left 不应该进行 (f x) 操作, LeftRight 的类型可能会不同

对二叉搜索数加入 Functor

instance Functor Tree where
  fmap f EmptyTree = Empty
  fmap f (Node x leftsub rightsub) =
    Node (f x) (fmap f leftsub) (fmap f rightsub)

注意经过 fmap 操作后可能不是一个 合法 的二叉搜索数

Kind

建立模组

形如

data Bool = False | True
-- data 类型 = 构造子

建立简单可枚举的类型 (注意类型和构造子首字母都大写)

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

建立一个 Shape 类型 (后面加 deriving (Show) 时构造子可以直接转换成字符串)

可以看到两个构造子的类型已经建立

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

可以使用构造子建立函数

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

还可以使用

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

的形式 (注意 Point 同时是构造子和类型的名字)

注意对应的函数也要进行改动

若要将类型变为模组则导出是需要类似

module Shapes
( Point(..)
, Shape(..)
, surface) where

的形式

Record Syntax

data Person = Person String String Float Float String deriving (Show)
let guy = Person "Alice" "Margatroid" 156 43 "Mahousii"

-- Record Syntax
data Person = Person { firstName :: String
                     , lastName :: String
                     , height :: Float
                     , weight :: Float
                     , occupation :: String
                     } deriving (Show)
let guy = Person "Alice" "Margatroid" 156 43 "Mahousii"
ghci> guy
-- Show 的方式与上面不同
ghci> firstName guy
....

Type Parameters

data Maybe a = Nothing | Just a

应对多种类型通用的情况, 但对于限制类型

data Vector a = Vector a a a deriving (Show)
vplus :: (Num t) => Vector t -> Vector t -> Vector t
(Vector i j k) `vplus` (Vector l m n) = Vector (i+l) (j+m) (k+n)
vectMult :: (Num t) => Vector t -> t -> Vector t
(Vector i j k) `vectMult` m = Vector (i*m) (j*m) (k*m)
scalarMult :: (Num t) => Vector t -> Vector t -> t
(Vector i j k) `scalarMult` (Vector l m n) = i*l + j*m + k*n

注意 不要在data生命中添加类型约束, 而应该在函数定义中添加

Derived Instances

对于 Person, 我们假设没有相同姓名的人存在, 则判断是否同一个人可以用以下方法

data Person = Person { firstName :: String
                     , lastName :: String
                     ) deriving (Eq)

加入 deriving (Eq) 后, 就可以使用 ==, /= 来判断其相等性

对于 deriving (Read), 同样需要指定类型

data Person = Person { firstName :: String
                     , lastName :: String
                     ) deriving (Read, Show)

read "Person {firstName = \"Alice\",
              lastName = \"Margatroid\"}" :: Person

重新定义 Bool 类型

data Bool = False | True deriving (Ord)
ghci> True `compare` False
GT -- 右边的比左边的大, 且加入 Ord 后可以使用 compare 比较大小

定义一个星期的类型

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

ghci> read "Monday" :: Day
Monday
ghci> minBound :: Day
Monday
ghci> [minBound .. maxBound] :: Day
[Monday, TuesDay, Wednesday, Thursday, Friday, Saturaday, Sunday]

一切正常

Type Sunonyms

使用 type 关键字来为一个既有的类型提供别名(创建新类使用 data)

type String = [Char]

也可以这样

type FirstName = String
type LastName = String
type Name = (FirstName, LastName)

getFirstName :: Name -> FirstName
getFirstName (firstName, lastName) = firstName

alice = ("Alice", "Margatroid")

形态别名也可以带上参数

type AssocList k v = [(k, v)]
-- (Eq k) => k -> AssocList k Int

型别构造子

type IntMap v = Map Int v
-- 也可以这样
type IntMap = Map Int

Recursive data structures

递归的定义数据结构

data List a  = Empty | cons a (List a) deriving (Show, Read, Eq, Ord)
fixity 宣告
infixr 5 :-:
-- infixl 表示左结合, infixr 表示右结合
-- 数字表示优先级(乘法为7, 加法为6)
data List a = Empty | a :-: (List a) deriving (Show, Read, Eq, Ord)
-- 这里就可以用 :-: 表示之前的 `cons`

++ 的定义

infixr 5 .++
(.++) :: List a -> List a -> List a
Empty .++ ys = ys
(x :-: xs) .++ ys = x :-: (xs .++ ys)

<<self-instance>>自定instance

在默认 deriving 生成的 instance 不能满意的情况下可以自行定义

data TrafficLight = Red | Yellow | Green
instance Eq TrafficLight where
  Red == Red = True
  Green == Green = True
  Yellow == Yellow = True
  _ == _ = False

instance Show TrafficLight where
  show Red = "Red light"
  show Yellow = "Yellow light"
  show Green = "Green light"

上面 Eq 的定义与 deriving (Eq) 相同, 而 Show 略有区别

关于定义 instance 需要注意

instance (Eq m) => Eq (Maybe m) where
  Just x == Just y = x == y
  Nothing == Nothing = True
  _ == _ = False

注意1 (Maybe m) 才是一个型别而不是 Maybe

注意2 (Maybe m) 并不能保证 m 属于 Eq 从而使用 (==), 因此需要 (Eq m) 来限制

语法

<<let-in>>let..in..

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 (a, b, c) = (1, 2, 3) in a + b + c) * 100

List Comprehension 中的 let

calcBims :: (RealFloat a) => [(a, a) -> [a]]
calcBims xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]

注意这里没有 in

let 的范围在

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

中竖线左边的部分, 竖线右边let前的部分不能使用 bmi

<<case-of>>case..of..->..

类似 模式匹配

以下两个函数等价

head' :: [a] -> a
head' [] = error "Empty lists"
head' all@(x:_) = x
head' :: [a] -> a
head' all = case all of [] -> error "Empty lists"
                        (x:_) -> x

函数

Pattern Matching       检测一个值是否合适(类型或数值)并从中取值
Guards                 检测一个值的某项属性(数值通过某些函数)是否为真

<<pattern-matching>>Pattern Matching

为不同的模式分别定义函数本身 (case的语法糖)

注意 case 不一定要在函数内, 而模式匹配只能定义函数时使用

最后一项用万能的匹配方式避免崩溃

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

List的模式匹配

List本身为语法糖, 即 [1,2,3] 本质上为 1:2:3:[]

故可使用 x:xs 绑定 x 为头部(1), xs 为尾部(2:3:[])

注意1 x:xs 不能匹配{{{BO}}}{{{BC}}}

注意2 模式匹配不能使用 ++

List的as模式

通过 all@(x:xs) 的形式, all 表示整个 List, x 表示头部, xs 表示尾部

capital :: String -> String
capital "" = "Empty string"
capital all@(x:_) = "First letter of " ++ all ++ is ++ [x]

List Comprehension

在 List Comprehension 中也可以使用 Pattern Matching

let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]
[a+b | (a,b) <- xs]

Guards

如果 Guards 没有提供 otherwise 且之前的都没有匹配则寻找下一个模式匹配.

如果模式匹配全部不合适就会发生一个错误.

bmiTell :: (RealFloat a) => a -> String
bmiTell bmi
    | bmi <= 18.5 = "You're underweight"
    | bmi <= 25.0 = "You're supposedly normal"
    | bmi <= 30.0 = "You're fat"
    | otherwise = "You're a whale"

关键字where

where 绑定的名字只对函数内有效

bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
    | bmi <= 18.5 = "You're underweight"
    | bmi <= 25.0 = "You're supposedly normal"
    | bmi <= 30.0 = "You're fat"
    | otherwise = "You're a whale"
    where bmi = weight / height ^ 2

也可以在 where 中绑定多个变量

bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
    | bmi <= skinny = "You're underweight"
    | bmi <= normal = "You're supposedly normal"
    | bmi <= fat = "You're fat"
    | otherwise = "You're a whale"
    where bmi = weight / height ^ 2
          skinny = 18.5
          normal = 25.0
          fat = 30.0

还可以在 where 中使用模式匹配

bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
    | bmi <= skinny = "You're underweight"
    | bmi <= normal = "You're supposedly normal"
    | bmi <= fat = "You're fat"
    | otherwise = "You're a whale"
    where bmi = weight / height ^ 2
          (skinny ,normal ,fat) = (18.5, 25.0, 30.0)

where 绑定函数

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

关键字let

类型 where, 但 let 绑定的名字只在一个任意的局部范围(in 部分)有效

提示1 let 不一定要在函数内

提示2 let 不能在多个 guard 中重复使用

例子

Curried functions

对于 (-1) 处理为一个负数, 若要一个减一的不全调用使用 (subtract 1)

不全调用和函数一样不属于 Show 类型类, 无法转换为字符串输出

Lambda

匿名函数, 形如 \args -> procedure

ghci> (\x y -> x + y) 2 3
5

在可读性方面的一些区别

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

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

使用 Lambda 可以使函数定义与类型声明一致

flip 函数声明

flip' :: (a -> b -> c) -> b -> a -> c
flip' f x y = f y x

flip'' :: (a -> b -> c) -> b -> a -> c
flip'' f = \x y -> f y x

<<fold>>Fold

Fold 可以处理所有通过遍历一个 List 来获得一个值的操作

foldl

foldl 执行过程 (真正的扩展见 foldl 与 foldl' 的区别)

   foldl (+) 0 [1..3]
  (foldl (+) 0 [1..2]) + 3
 ((foldl (+) 0 [1]) + 2) + 3
(((foldl (+) 0 []) + 1) + 2) + 3
(((0) + 1) + 2) + 3
-- 以下为 scanl 保留的过程
(((0) + 1) + 2) + 3 -- 0
((1) + 2) + 3       -- 1
(3) + 3             -- 3
6                   -- 6
-- [0, 1, 3, 6]

左折叠为从 List 尾部不断取出元素计算 (不可处理无限序列)

sum' :: (Num a) => [a] -> a
sum' = foldl (+) 0
-- 等同于 sum' x = foldl (+) 0 x

foldr

foldr 执行过程

               foldr (+) 0 [1..3]
1 +           (foldr (+) 0 [2..3])
1 + (2 +      (foldr (+) 0 [3]))
1 + (2 + (3 + (foldr (+) 0 [])))
1 + (2 + (3 + (0)))
-- 以下为 scanr 保留的过程
1 + (2 + (3 + (0))) -- 0
1 + (2 + (3))       -- 3
1 + (5)             -- 5
6                   -- 6
-- [6, 5, 3, 0]

右折叠为从 List 头部不断取出元素计算 (可以处理无限序列)

map' :: (a -> b) -> [a] -> [b]
map' f xs = foldr (\x acc -> f x : acc) [] xs

对比 map' f xs = foldl (\acc x -> acc ++ [f x]) [] xs

使用 foldr 更快(: 运算比 ++ 要快的多)

foldl1

无需初始值的 foldl

sum'' = foldl1 (+)

以上 sum 处理空 List 时会产生异常, 而 foldl 可以很好的应对

foldr1

无需初始值的 foldr

Scan

Scan 类似 Fold 但是为记录下累加值的所有状态

scanl
scanr
scanl1
scanr1

具体参见 Fold

函数调用符

($) :: (a -> b) -> a -> b
f $ x = f x

作用相当于在 $ 的位置到尾部加一对括号

ghci> sqrt (3 + 4 + 9)
4.0
ghci> sqrt $ 3+4+9
4.0

以上两者等价

还可将一个数字作为函数使用

ghci> map ($ 3) [(4+), (10*), (^2), sqrt]
[7.0,30.0,9.0,1.7320508075688772]

Function Composition

(.) :: (b -> c) -> (a -> b) -> a -> c
f . g = \x -> f (g x)

将两个包含关系的函数变为并列, 方便定义 Point Free Style

ghci> map (\x -> negate (abs x)) [5, -3, 6, 7, -9]
[-5,-3,-6,-7,-9]
ghci> map (negate . abs) [5, -3, 6, 7, -9]
[-5,-3,-6,-7,-9]

以上, 可以省去 (\x -> negate . abs x) 参数和函数末尾的 x

模组

装载模组

-- 装载整个模组
ghci> import Data.List
ghci> nub [0,3,1,2,3]
[0, 3, 1, 2]
-- 装载模组部分函数
ghci> import Data.List (nub, sort)
ghci>
-- 装载模组除去部分函数
ghci> import Data.List hiding (nub)
ghci>
-- 避免命名冲突且装入整个模组
import qualified Data.List
ghci Data.List> Data.List.nub [0,1,2,0]
[0, 1, 2]
-- 起个短一点的模组别名
ghci> import qualified Data.List as L
ghci L> L.nub [0,1,2,0]
[0, 1, 2]

以上

Haskell标准库查询 Hoogle Hooyoo

<<foldl-foldlprime>>Data.List的 foldl

foldl 的执行过程

                   foldl (+) 0 [1..3]

let z1 = 0 + 1  in foldl (+) z1 [2..3]

let z1 = 0 + 1
    z2 = z1 + 2 in foldl (+) z2 [3]

let z1 = 0 + 1
    z2 = z1 + 2
    z3 = z2 + 3 in foldl (+) z3 []

let z1 = 0 + 1
    z2 = z1 + 2
    z3 = z2 + 3 in z3

let z1 = 0 + 1
    z2 = z1 + 2 in z2 + 3

let z1 = 0 + 1 in (z1 + 2) + 3

((0 + 1) + 2) + 3

..

6

foldl' 的执行过程

foldl' (+) 0 [1..3]
foldl' (+) 1 [2..3]
foldl' (+) 3 [3]
foldl' (+) 6 []
6

建立模组

单文件

文件名与模组名一致, 例如文件名 Geometry.hs

module Geometry
( sphereVolume
, sphereArea
, cubeVolume
, cubeArea
, cuboidVolume
, cuboidArea
) where

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

....

导入这个模组只需

import Geometry

即可

多文件

目录名与模组名一致, 例如目录名 Geometry 文件 Geometry/sphere.hs

module Geometry.Sphere
( volume
, area
) where

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

....

文件 Geometry/cuboid.hs, Geometry/cube.hs 类似

导出这个模组可以形如

import qualified Geometry.Sphere as Sphere

ghci相关

:q            退出 ghci
:t (+)        查看类型
:m Data.List  装载模组
:l file.hs    装载文件
:r            重新装载文件
:info datatype 输出型别的信息
下一页 CPS变换