Learn You a Haskell for Great Good 读书笔记 7

7.Data Type & Type Parameters

数值类型定义:
1
data Bool = False | True
数值构造器(value Constructor):
1
data Shape = Circle Float Float Float | Rectangle Float Float Float Float

上面Circle 和 Rectangle 就是俩,他接受若干类型和个数的数值然后返回一个类型对象,他们本质上也是函数

1
Rectangle :: Float -> Float -> Float -> Float -> Shape
1
2
3
area :: Shape -> Float
area (Circle _ _ r) = pi * r ^ 2
area $ Circle 10 20 10

上面用到了模式匹配(pattern matching)

为了在控制台展示一个类型,要让他得到(deriving)?Show类型

1
2
3
4
data Shape = Circle Float Float Float | Rectangle Float Float Float Float
deriving(Show)
> Circle 10 20 30
> Circle 10.0 20.0 30.0

因为数值构造器是函数,所以我们可以做任何高阶函数干的事,比如map, currying.

除了系统自带的类型,我们还可以将自定义类型作为参数类型:

1
2
3
4
5
6
data Point = Point Float Float deriving(Show)
data Shape = Circle Point Float | Rectangle Point Point deriving(Show)

area :: (Rectangele (Point x1 y1) (Point x2 y2)) = (abs $ x2 - x1) * (abs $ y2 - y1)
> area (Rectangle (Point 0 0) (Point 2 3))
> 6.0

将数值构造器向外暴露用俩. 通配符,可以把该类型下所有的构造器全部暴露出去

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

你想给构造器中的类型起个有意义的名字,可以用record syntax 语法起别名(实际上也是函数)

1
2
3
4
5
data Person = Person { firstName :: String , lastName :: String , age :: Int , height :: Float , phoneNumber :: String , flavor :: String } deriving (Show)

ghci> :t flavor
flavor :: Person -> String
ghci> let sPerson = {fistName = "mother", lastName= "dad", age = 12, height = 123.4}
类型构造器(Type Constructors):

接受类型参数(type parameter)返回一个新类型的函数叫 类型构造器

1
data Maybe a = Nothing | Just a

a 是类型参数,它可以是String 也可以是 Int 等。

绝大多数时间,我们不需要指定类型参数,因为haskell自带类型推导。

碰上模棱俩可的情况就要指定一下了,比如:

1
Just 3 :: Maybe Int

其实相当于java中的泛型

泛型约束:

1
(Ord k) =>

k只能是可排序的 实现Ord,书本不建议在类型构造器上加约束,因为对应的所有函数都要加一遍。完全可以在需要的函数上再加约束。

几个常用的deriving
  • Show ,就是把数据类型变成字符串
  • Read ,就是把字符串读取成数据类型
1
2
mysteryDude = "Person { firstName =\"Michael\"" ++ ", lastName =\"Diamond\"" ++ ", age = 43}"
ghci> read mysteryDude :: Person Person {firstName = "Michael", lastName = "Diamond", age = 43}
  • Eq , 比较是否相等
  • Ord , 排序。先看构造器,再看参数。先声明的构造器更小,相同构造器,比较参数大小。
  • Enum , 枚举,对于某个值有前值和后值
1
2
3
4
ghci> succ Monday 
Tuesday
ghci> pred Saturday
Friday
  • Bounded , 有上下限
1
2
3
4
ghci> minBound :: Day 
Monday
ghci> maxBound :: Day
Sunday
类型同义词(Type Synonyms)

就是给一个类型定义一个短名

1
type PhoneBook = [(String, String)]

也可以给类型构造器用

1
type AssocList k v = [(k. v)]

与data 关键字不同,左边是实际类型(构造器)对象,而前者是统称,非实际对象。

递归构造器
1
data List a = Empty | Cons a (List a) deriving (Show, Read, Eq, Ord)

把Con用中缀构造器简化一下(中缀构造器只能以:打头)

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

然后最头疼的来了,看下数组连接函数混用中缀构造器:

1
2
3
4
5
6
7
8
(^++) :: List a -> List a -> List a 
Empty ^++ ys = ys
(x :-: xs) ^++ ys = x :-: (xs ^++ ys)

ghci> let a = 3 :-: 4 :-: 5 :-: Empty
ghci> let b = 6 :-: 7 :-: Empty
ghci> a ^++ b
3 :-: (4 :-: (5 :-: (6 :-: (7 :-: Empty))))

构造器在这种的作用主要是 模式匹配 和 值构造。

类型类/接口(Type Classes)

类型类定义了一些函数的声明(类似接口),这些函数声明构成一定的规则。

所有类型类的实体要实现最少个函数去覆盖类型类中所有函数声明(minimal complete definition )。

1
2
3
4
5
class Eq a where 
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
x == y = not (x /= y)
x /= y = not (x == y)
1
2
3
4
5
6
data TrafficLight = Red | Yellow | Green
instance Eq TrafficLight where
Red == Red = True
Green == Green = True
Yellow == Yellow = True
_ == _ = False

因为类型类里已经定义了不相等的情况,所以实现类里只要定义相等的情况就能完成实现

子类/接口(Subclassing)

其实就是继承了父类的函数声明规则

1
2
class (Eq a) => Num a where 
...

Num必须带有有Eq的规则,也就是类型实体要定义==或/=

泛型类型的类/接口实现
1
2
3
4
instance (Eq m) => Eq (Maybe m) where 
Just x == Just y = x == y
Nothing == Nothing = True
_ == _ = False

类型构造器不能直接实现类才怪,如果类型构造器作为类型,那么声明里面每个参数和返回必须是一个确定的(Concrete)类型。带上参数后就行了。

函子(Functor)

开始变魔术了。

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

这里的 f 是一个 类型构造器(type constructor)。

比如 Maybe, 比如 []。

但是对于那种需要多个参数的类型构造器,fmap只对最后一个类型起效,因为函子要的类型构造器只能拿进一个类型,就只能拿构造器的偏函数,如 Map k v的Map k

1
2
3
4
> fmap (+1) $ Left 1
> Left 1
> fmap (+1) $ Right 1
> Right 2
类型的类型(Kind)
1
2
ghci> :k Maybe 
Maybe :: * -> *

* 代表确定的类型

就是说Maybe是拿一个确定类型产生确定类型的函数(构造器)。

所以函子里的 f 的 kind 必须是 * -> *