类型_Haskell笔记3

一.内置类型

几种常见的类型如下:

  • Int:有界整数,32位机器上的界限是[-2147483648, 2147483647]

  • Integer:无界整数,内置的大数类型,效率不如Int

  • Float:单精度浮点数,6位小数

  • Double:双精度浮点数,15位小数

  • Bool:布尔值,值为True/False

  • Char:字符

  • Tuple:元组本身也是类型,只有()一个值

内置的无界整数让大数运算变得非常方便,例如求100的阶乘:

> product [1..100]
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

二.变量类型

name :: String
name = "ayqy"

::读作“类型为”(has type),告诉编译器变量nameString类型(即[Char]类型)的

另外,类型的首字母都是大写的

P.S.虽然理论上很多场景不需要手动注明类型(编译器会自己推断),但实践建议是至少给顶层变量/函数都标明类型,当然,全都标上肯定是好习惯,毕竟明确的类型能够大大增强可读性,具体见Type signatures as good style

P.S.可以通过:browse <module>命令列出指定模块所有的类型推断,便于给现有代码补充类型

三.函数类型

一些常用函数的类型签名如下:

show :: Show a => a -> String
odd :: Integral a => a -> Bool
fromIntegral :: (Num b, Integral a) => a -> b
(+) :: Num a => a -> a -> a
(++) :: [a] -> [a] -> [a]

其中,::=>之间的部分是类型约束(声明类型变量),=>之后的部分是其类型。类型声明中的小写字母(例如a)叫做类型变量,未加限定的类型变量(如++类型中的a)相当于泛型,用到类型变量的函数称之为多态函数

比如show :: Show a => a -> String的含义是show的类型是一个接受Show类型参数,返回String的函数。(+) :: Num a => a -> a -> a表示+的类型是接受两个Num类型参数,返回Num的(柯里化)函数。而(++) :: [a] -> [a] -> [a]表示++的类型是接受两个List参数,返回另一个List的函数,这里的a没有限定类型,所以List里的元素可以是任意类型

类型部分的->读作“映射到”(maps to),如何理解?

函数的数学定义是定义域到值域的映射关系,所以f = x -> y对应的数学含义是y = f(x),也就是说x映射到y(的映射关系)就是f,输入x返回对应的y

所以a -> b -> c表示一个输入a,返回函数b -> c的函数,继续调用这个返回函数,输入b返回对应的c。忽略柯里化特性的话,可以简单理解为接受两个参数a, b,返回c

四.Typeclass

(==) :: Eq a => a -> a -> Bool

其中,Eq被称为typeclass,相当于interface,即定义了该类型成员必须具有的行为

除函数外的所有类型都属于Eq,都可以判断相等性。另一些常见的typeclass如下:

  • Ord:可以比较大小(能够通过<, >, <=, >=等函数来比较大小,所以Ord一定属于Eq

  • Show:可用字符串表示(除函数外,都是可Show的)。可以通过show函数把其它类型转字符串

  • Read:与Show相反。可以通过read函数把字符串转到其它类型

  • Enum:可枚举,即连续的。包括()BoolCharOrderingIntIntegerFloatDouble,这些类型都可以用于Range,可以通过succpred函数访问该类型值的后继和前驱

  • Bounded:有明确的上下界。可以通过maxBoundminBound取指定类型的上下界(如maxBound :: Int

  • Num:数值。成员都具有数值的特征

  • Integral:整数。包括IntInteger

  • Floating:小数。包括FloatDouble

数字转换的话,大范围转小范围能够隐式完成(如NumFloat),小转大则需要通过fromIntegral :: (Num b, Integral a) => a -> b之类的函数来完成,常见的场景是length函数:

> length "Hello" + 0.5

<interactive>:191:18: error:
    • No instance for (Fractional Int) arising from the literal ‘0.5’
    • In the second argument of ‘(+)’, namely ‘0.5’
      In the expression: length "Hello" + 0.5
      In an equation for ‘it’: it = length "Hello" + 0.5

因为length :: Foldable t => t a -> Int,而IntFractional无法直接相加,所以需要这样做:

> (fromIntegral (length "Hello")) + 0.5
5.5

另外,read函数也很有意思,例如:

> read "12" + 4
16
> 1 : read "[2, 4]"
[1,2,4]

会根据上下文推断出目标类型,所以如果没有上下文就无法推断:

> read "12"
*** Exception: Prelude.read: no parse

编译器不知道我们想要什么类型,可以手动声明类型给点提示:

> read "12" :: Int
12
> read "12" :: Float
12.0

五.自定义类型

代数数据类型

Algebraic Data Type,是指通过代数运算构造出来的数据结构,其中代数运算有两种:

  • sum:逻辑或,例如Maybe类型的可能值之间是逻辑或关系

  • product:逻辑与,例如元组分量之间是逻辑与的关系

例如:

-- 逻辑与,Pair类型是Int-Double对儿
data Pair = P Int Double
-- 逻辑或,Pair类型个数值,要么是Int,要么是Double
data Pair = I Int | D Double

通过逻辑或和逻辑与能造出来任意复杂的数据结构,都可以称为代数数据类型

从地位来看,代数数据类型之于函数式语言,就像代数之于数学,是非常基础的东西。同样,要进行代数运算,先要有数的定义:

map algebraic data type to math

map algebraic data type to math

声明

通过data关键字来声明自定义类型:

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

表示Shape类型有2个值构造器(Circle, Rectangle),即Shape类型的值是Circle或者Rectangle值构造器本质上是函数

Circle :: Float -> Float -> Float -> Shape
Rectangle :: Float -> Float -> Float -> Float -> Shape

值构造器的参数(比如CircleFloat Float Float)也被称为项(field),实际上就是参数

既然值构造器是函数,那么模式匹配也可以用于自定义类型:

circleArea (Circle _ _ r) = pi * r ^ 2
> circleArea (Circle 1 1 1)
3.1415927

求面积函数的类型为:

circleArea :: Shape -> Float

参数类型是Shape,而不是Circle,因为后者只是值构造器,并不是类型

另外,模式匹配都是针对值构造器的,常见的如[], otherwise/Ture, 5等都是无参值构造器

递归定义类型

如果一个类型的值构造器的参数(field)是该类型的,那就产生递归定义了

例如List的语法糖:

[1, 2, 3]
-- 等价于(:右结合,括号非必须)
1 : (2 : (3 : []))

就是一种递归定义:List是把首项插入到剩余项组成的List左侧

不妨手搓一个:

infixr 5 :>
data MyList a = MyEmptyList | a :> (MyList a) deriving (Show)

其中,自定义运算符:>相当于:,都属于值构造器(所以x:xs的模式匹配实际上是针对List的值构造器:的)。试玩一下:

> :t MyEmptyList
MyEmptyList :: MyList a
> 3 :> 5 :> MyEmptyList
3 :> (5 :> MyEmptyList)
> :t 3 :> 5 :> MyEmptyList
3 :> 5 :> MyEmptyList :: Num a => MyList a

除了语法上的差异,和List定义(3 : 5 : [])基本一致。再造几个List特色函数:

_fromList [] = MyEmptyList
_fromList (x:xs) = x :> (_fromList xs)
_map f MyEmptyList = MyEmptyList
_map f (x :> xs) = f x :> _map f xs

继续试玩:

> _fromList [1, 2, 3]
1 :> (2 :> (3 :> MyEmptyList))
> _map (+ 1) (_fromList [1, 2, 3])
2 :> (3 :> (4 :> MyEmptyList))

派生

只有Show类(typeclass)的成员才能在GHCi环境直接输出(因为输出前调用show :: Show a => a -> String),所以,让Shape成为Show的成员:

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

通过deriving关键字声明类型派生,让一个类型的值也成为其它类型的成员。试着直接输出Shape值:

> Circle 1 1 1
Circle 1.0 1.0 1.0

干脆把坐标点也抽离出来:

data Point = Point Float Float deriving (Show)
data Shape = Circle Point Float | Rectangle Point Point deriving (Show)
circleArea (Circle _ r) = pi * r ^ 2

Show外,其它几个能够自动添上默认行为的typeclass是Eq, Ord, Enum, Bounded, Read。比如派生自Eq后可以通过==/=来比较值的相等性:

data Mytype = Mytype Int String deriving (Show, Eq)
> Mytype 3 "a" == Mytype 4 "b"
False
> Mytype 3 "a" == Mytype 3 "a"
True

实际上,派生自Eq时自动添的相等性判断就是检查输入参数是否一致:

1.检查值构造器是否一致
2.检查值构造器的参数是否一致

当然,要求参数也必须是Eq类成员,否则无法自动比较(如果不满足,就会抛个错出来

ShowRead也类似,用来完成字符串与值之间的互相转换:

data Mytype = Mytype Int String deriving (Show, Eq, Read)
> Mytype 3 "a"
Mytype 3 "a"
> read "Mytype 3 \"a\"" :: Mytype
Mytype 3 "a"

Ord很有意思,表示成员是可排序的,但默认的排序依据如何确定呢?

data Mytype = EmptyValue | Singleton | Mytype Int String deriving (Show, Eq, Read, Ord)
> EmptyValue < Singleton
True
> Singleton < Mytype 3 "a"
True
> Mytype 3 "a" < Mytype 4 "a"
True

首先看类型声明中的次序,或(|)在一起的,最先出现的值构造器,造出来的值最小,然后再按照类似的规则比较值构造器的参数,所以同样要求参数都得是Ord成员

Enum, Bounded用来定义枚举类型,即有限集合,Enum要求每个值都有前驱/后继,这样就可以用于Range了,Bounded要求值具有上下界,例如:

data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday deriving (Show, Bounded, Enum)
-- 上下界
> maxBound :: Day
Sunday
> minBound :: Day
Monday
-- 前驱/后继
> pred Wednesday
Tuesday
> succ Wednesday
Thursday
-- Range
> [Wednesday ..]
[Wednesday,Thursday,Friday,Saturday,Sunday]

Record

对于简单的数据类型,比如Vector2D

data Vector2D = Vector2D Float Float deriving(Show)

简单的data定义就能满足语义需要(我们明确知道二维向量的两个参数是横纵坐标),如果要描述的对象是复杂的东西,比如人有年龄、身高、体重、三围:

data Person = Person Float Float Float Float Float Float deriving(Show)

这个看着太不直观了,我们添行注释:

-- 年龄 身高 体重 上围 中围 下围
data Person = Person Float Float Float Float Float Float deriving(Show)

想到了什么?这不就是10几个参数的函数嘛!参数巨多还要求顺序,更麻烦的是,这是个数据类型,我们还需要一系列的 getter

getAge (Person age _ _ _ _ _) = age
getHeight (Person _ height _ _ _ _) = height
-- ...等等一堆getter

其它语言里一般怎么处理这种情况?把零散参数组织起来(比如造个对象):

data Person = Person {
  age :: Float,
  height :: Float,
  weight :: Float,
  xw :: Float,
  yw :: Float,
  tw :: Float
} deriving (Show)

创建一个person,语义清楚,并且不用关心参数顺序:

person =  Person {age=1, height=2, xw=4, yw=5, tw=6, weight=3}

会自动创建一堆getter,例如:

> :t age
age :: Person -> Float
> weight person
3.0

用起来比单纯的类型定义方便多了

类型参数

类型构造器可以传入参数,返回新的类型。例如:

data Maybe a = Nothing | Just a

其中,a是类型参数,Maybe不是类型,而是类型构造器,具体的Maybe xxx才是类型,NothingJust xxx都是该类型的值,例如:

Just 'a' :: Maybe Char
Nothing :: Maybe a

这样做能够得到一堆行为相似的类型,从应用场景上来看,带参数的类型相当于泛型,是在具体类型之上的一层抽象,比如经典的List

[1, 2, 3] :: Num t => [t]
"456" :: [Char]

都支持一些行为(Data.List模块定义的各种函数):

map :: (a -> b) -> [a] -> [b]
> map (+ 1) [1, 2, 3]
[2,3,4]
> map (Data.Char.chr . (+ 1) . Data.Char.ord) "456"
"567"

length :: Foldable t => t a -> Int
> length [1, 2, 3]
3
> length "456"
3

maplength函数并不关心List a具体类型是什么,算是定义在抽象数据类型上的操作

Maybe与Either

data Maybe a = Nothing | Just a     -- Defined in ‘GHC.Base’
data Either a b = Left a | Right b  -- Defined in ‘Data.Either’

应用场景上,Maybe用来表示可能出错的结果,成功就是Just a,失败就是Nothing。适用于单一错误原因的场景,比如elemIndex

Data.List.elemIndex :: Eq a => a -> [a] -> Maybe Int

找到了返回Just Int类型的下标,找不到就返回Nothing,没有第三种结果

单看异常处理的场景,Either更强大一些,一般把失败原因放到Left a,成功结果放到Right b,形式上与Maybe非常像,但Left a可以携带任意信息,相比之下,Nothing就太含糊了

P.S.JS上下文中,Maybe相当于约定成功就返回值,失败返回false,只知道失败了,可能不清楚具体原因。Either相当于约定回调函数的第一个参数携带错误信息,如果不为空就是失败了,具体原因就是该参数的值

类型别名

Type synonyms(类型同义词,即类型别名),之前已经见过了:

> :i String
type String = [Char]    -- Defined in ‘GHC.Base’

通过type关键字给类型定义别名,让String等价于[Char],从而给类型声明带来语义上的好处,例如:

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

inPhoneBook :: Name -> PhoneNumber -> PhoneBook -> Bool
inPhoneBook name pnumber pbook = (name, pnumber) `elem` pbook

输入姓名、电话和电话簿,返回电话簿里有没有这条记录。如果不起别名的话,类型声明就只能是这样:

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

当然,这个场景看起来似乎有些小题大做,为了一个没什么实际作用的东西(类型声明)多做这么多事情。但类型别名的特性是为了提供一种允许类型定义的语义更形象生动的能力,而不是针对具体某个场景,比如:

  • 让类型声明更加易读

  • 替换掉那些重复率高的长名字类型(如[(String, String)])

这种能力能够让类型对事物的描述更加明确

类型别名也可以有参数,比如,自定义的关联列表:

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

允许任意k-v,保证其通用性,例如:

inPhoneBook :: (Eq k, Eq v) => k -> v -> AssocList k v -> Bool
inPhoneBook name pnumber pbook = (name, pnumber) `elem` pbook
> inPhoneBook 1 "1234" [(0, "0012"), (1, "123")]
False

此时AssocList k v对应的具体类型就是AssocList Int String

> read "[(0, \"0012\"), (1, \"123\")]" :: AssocList Int String
[(0,"0012"),(1,"123")]

类型别名也有类似于柯里化的特性,例如:

type IntAssocList = Int
-- 等价于,保留一个参数
type IntAssocList v = Int v

如果参数给够就是个具体类型,否则就是带参数的类型

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code