TOC
Open TOC
类型
类型检查
显式类型声明
基本类型
- Int
- Interger
- Float
- Double
- Bool
- Char
- String: [Char]
类型变量(参数)
泛型、多态
类型类
类型约束
常见类型类
- Eq
- Ord
- Show
- Read
- Enum
- Bounded
- Num
- Floating
- Intergeral
类型注解
::
自定义类型
值构造器
data 类型名 = 值构造器
类型名和值构造器的首字母必须大写
值构造器的本质:返回某数据类型值的函数
对值构造器进行模式匹配
将自定义类型导出到模块中
借助 Point
数据类型优化 Shape
数据类型
将要导出的类型和函数写到一起,可以选择是否导出值构造器
Shape(..)
表示导出 Shape
的所有值构造器,等价于 Shape(Circle,Rectangle)
若只写 Shape
,使用模块的用户只能使用 baseCircle
和 baseRect
来得到 Shape
了,同时也无法使用该值构造器进行模式匹配了
测试
记录语法
自动创建函数,允许直接按字段取值
测试
类型构造器
data 类型名 类型变量 = 值构造器
值构造器可以取几个参数,产生一个新值;类型构造器可以取类型作为参数,产生新的类型
声明中不允许添加类型约束,如 data (Num a) => Vector a = Vector a a a deriving (Show)
,否则导入时会报错
实例:Maybe
正因为有了类型变量,Maybe
才成为了类型构造器
自动类型推导 & 类型注解
实例:Either
测试
实例:向量函数
测试
派生实例
关键词 deriving
注:此处类型 Day
没有类型参数,是具体的;若一个类型有类型参数,在派生后必须要有相应的类型约束,另见作为类型类实例的带参数类型
测试
类型别名
type 新名 = 原名
类型别名只可以在 Haskell 的类型部分中使用,如 data
声明和 type
声明,以及显式类型声明和类型注解中跟在 ::
后面的部分
参数化类型别名
实际上是
来看一个例子
测试
递归数据结构
Fixity declaration
指定了运算符的优先级和结合性
- infix [integer] ops
- infixl [integer] ops
- infixr [integer] ops
中缀值构造器必须以冒号开头
测试
实例:二叉搜索树
测试
自定义类型类
class
class
关键词定义新的类型类
并不一定要实现函数的本体,不过必须要写出函数的类型声明
给出的函数体是以交叉递归的形式实现的,在实例声明中只需要实现一个函数即可
其中 a
扮演着 Eq
实例类型的角色
instance
instance
关键词将类型转为某类型类的实例
没有直接通过 deriving
派生为某类型类的实例
测试
subclass
在 class
声明中添加一条类型约束,将一个类型类实现为另一个类型类的子类
此处 a
必须为类型类 Eq
的实例
作为类型类实例的带参数类型
(Maybe m)
取代了 a
在 instance Eq a where
的位置:保留相应的类型参数作为类型变量
我们希望所有 Maybe m
形式的类型都属于 Eq
,但这要求 m
也属于 Eq
,所以我们在 instance
声明中添加一条类型约束
对应的,函数的类型声明如下
:info
查看某类型类拥有哪些实例
查看类型(构造器)属于哪些类型类
查看值(构造器)属于哪些类型
实例:Yes-No typeclass
测试
Functor 类型类
定义
Functor 类型类定义如下,其中 f
不是一个具体类型,而是一个取一个类型参数的类型构造器
注意到 map
就是仅处理 List
的 fmap
,于是立即可以得到 List
作为 Functor 类型类实例的实现
其中 []
是一个取一个类型参数的类型构造器
注意不能写成 instance Functor [a] where
,因为 f
是一个取一个类型参数的类型构造器,而 [a]
则已经是一个具体类型
对于 List
,fmap
只不过是 map
术语辨析
- 类型类:
Functor
- 类型构造器和值构造器:functor,也就是
Functor
类型类的实例(的值),中译为函子
- 函子值,就是函子作为容器所包裹的值,也是函数实际作用的对象
Maybe functor
凡是拥有容器性质的类型都可以视作函子
- 容器是包含值和值的变形关系,这个变形关系就是函数。
- 在范畴论中,函子 (functor) 是范畴间的一类映射,通俗地说,是范畴间的同态。
测试
Either functor
部分应用,使其成为取一个类型参数的类型构造器,如此,第一个类型保持不变,而第二个类型参数则可以改变,符合 Either
的应用场景
实例:Tree functor
测试
kind
值有自己的标签(函数也是值的一种),叫做 type
类型也有自己的标签,叫做 kind
一个 *
代表这个类型是具体类型,一个具体类型是没有任何类型参数,而值只能属于具体类型
Maybe
的类型构造器接受一个具体类型,然后返回一个具体类型;Either
的类型构造器接受两个具体类型作为参数,并返回一个具体类型
不难推断出 Functor
类型类中的 f
的 kind
为 * -> *
再谈 Functor 类型类
引子
我们观察一下 Functor
类型类拥有哪些实例
IO functor
IO
可以成为 Functor
类型类的实例
于是可以使用 fmap
对 I/O action
进行操作,考虑如下程序
使用 fmap
改写为
测试一下
此时,我们可以认为 fmap
的类型为 fmap :: (a -> b) -> IO a -> IO b
function functor
函数也可以成为 Functor
类型类的实例
注意到函数类型 r -> a
可以写成 (->) r a
,于是 (->)
可以认为是一个取两个类型参数的类型构造器,类似 Either
然而 Functor
类型类定义中的 f
是一个取一个类型参数的类型构造器,于是我们使用部分应用
function functor 定义在 Control.Monad.Instances
中
我们来思考 fmap
的类型
即
这便是函数组合,于是 function functor 有另一种定义
在交互界面试一试
我们用 Curried functions 的思想,再来思考 fmap
的类型
上面我们认为 fmap
是一个函数,它接受另一个函数和一个 functor,得到另一个 functor
我们还可以认为 fmap
是一个函数,它接受另一个函数并把它提升 (lifting) 为操作 functor 的函数
这里 functor 是 Functor
类型类的实例
来看几个例子
functor laws
一个东西要成为 functor,必须要遵守某些定律,这些定律不会在 Haskell 中自动被检查
这里的 functor 与 Functor
类型类的实例有微妙的区别
fmap id = id
,其中 id
为恒等函数
fmap (f . g) = fmap f . fmap g
或 fmap (f . g) x = fmap f (fmap g x)
,其中 x
为一个 functor
我们来构造一个 Functor
类型类的实例,但它不满足上述定律
导入后我们测试第一条定律
不满足
Applicative 类型类
引子
到目前为止,我们调用 fmap
时传入的函数都是单参数函数,若是多参数函数会如何呢,来试一试
可以看到,我们得到了一个被包裹起来的 functor,这仍然是 functor
在这里,被包裹起来的 functor 是具体的单参数函数
于是我们可以将这个 functor 继续传入 fmap
中
我们来考虑一个更复杂的例子
回顾一下,目前 fmap
的两个参数,一个是普通函数,另一个是 functor,假如我们想取出某一个 functor 中的函数作为 fmap
参数中的普通函数呢,下面有请 Applicative
类型类
定义
模块 Control.Applicative
中定义了 Applicative
类型类
可见 Applicative
类型类是 Functor
类型类的子类
pure
接受一个值,然后返回一个包含那个值的 applicative functor
<*>
则是接受一个装有函数的 applicative functor 和另一个 applicative functor,然后取出 applicative functor 中的函数作用于另一个 applicative functor,返回一个 applicative functor
观察一下 Applicative
类型类拥有哪些实例
Maybe applicative functor
在交互界面试试
只要 <*>
返回的是一个装有函数的 applicative functor,我们就可以把 <*>
串起来用
我们来解读第一个例子
注意部分应用后是 (3+)
而非 (+3)
,因为加法满足交换律,所以这里看不出区别,换为除法便可看出区别
applicative style
现在让 fmap
露个面吧,由定义容易得到 pure f <*> x = fmap f x
为了书写方便,模块 Control.Applicative
定义一个名为 <$>
的函数
<$>
就是 fmap
的中缀版本,于是上面的例子可以这样写
对比普通的函数
可知,只需要点缀一点符号,普通函数就能操作这些 applicative functor,并返回 applicative functor
这也被称为 applicative style
List applicative functor
这里我们要回到容器的概念,容器其实就是一个上下文 (context)
pure
是把一个值放进默认的上下文中。换种说法就是一个会产生那个值的最小上下文
对 List
而言,最小上下文就是 []
,但由于空的 List
并不包含一个值,这也是为什么 pure
被实现为接受一个值,然后返回一个包含这个值的单元素 List
同样的,Maybe
的最小上下文是 Nothing
,但它其实表示的是没有值,所以 pure
被实现为 Just
<*>
被实现为列表推导式,这里 <*>
的类型为 (<*>) :: [a -> b] -> [a] -> [b]
来看几个例子
applicative style 处理列表是列表推导式的一个替代品
IO applicative functor
return
产生一个不做任何事的 I/O action
,把相同的值作为结果,符合 pure
的定义:把一个值放进最小的上下文中
从一个 I/O action
里取出函数,从另一个 I/O action
取出被函数作用的值,返回一个 I/O action
,包含了被作用后的值
来看个例子
使用 applicative style 改写
function applicative functor
pure
接受一个值,返回一个函数,该函数总是返回那个值
于是这里 pure
的类型为 pure :: a -> (r -> a)
,符合 Applicative
类型类中的定义
在交互界面试试
来看看 pure 3
的类型
先看一个例子
为了更好的理解 <*>
的实现,参考了 functions as applicative functors (Haskell / LYAH) - Stack Overflow 的内容进行解读,并联系之前部分的内容
根据定义
等价于(看之前的部分)
观察 pure (+)
的类型
根据 <*>
的实现,先考虑前半部分
这里需要注意 x
的位置,下面的例子作为参考
再考虑后半部分,用 (*100)
替换 lambda 中的 x
最后令 r
等于 5
就破案了
于是最后的效果是,我们创建了一个函数,它将 (+3)
和 (*100)
作用在参数上的各自的结果相加
还有一个例子作为习题
最后的效果是,函数会调用 \x y z -> [x,y,z]
,而传递的参数是 (+3)
、(*2)
和 (/2)
作用在外界参数上的各自的结果
ZipList
List
实际上有多种方式成为 applicative functor
ZipList
是另一个 Applicative
类型类的实例
注意这里 zipWith
函数的使用,结果列表会和两个列表中较短的那个长度相同
为了满足 pure f <*> x = fmap f x
,pure
的实现中用到了 repeat
由于 ZipList
类型并非 Show
类型类的实例,我们需要使用 getZipList
函数从 ZipList
中取出原生的 List
来看几个例子
applicative functor laws
这一条在之前的部分中多次提到,也是 applicative style 的核心之一
下面几条留作习题
pure id <*> v = v
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
pure f <*> pure x = pure (f x)
u <*> pure y = pure ($ y) <*> u
applicative functions
liftA2
先来简单的实现它
一方面,它在两个 applicative functor 间应用一个函数,这实际上隐藏了刚刚讨论过的 applicative style
另一方面,这让我们想起了 fmap
的类型
也就是说,对于普通的 functor,可以把函数应用到一个 functor 上,而对于 applicative functor,可以把函数应用到多个 applicative functor 上
来看几个例子
sequenceA
这让我们想起了 sequence
为了具体起见,我们以 List applicative functor 为基础来实现 sequenceA
一种方式是递归
另一种方式是使用折叠
来看几个示例,首先是 Just
然后是函数
接着是 List
总结
applicative functor 允许我们通过 applicative style 组合多种不同行为的计算,比如 I/O 计算、非确定性计算、可能会失败的计算等。只要用上 <$>
和 <∗>
,我们就能统一使用普通函数,利用 applicative functor 各自的语义来操作它们,操作任意数量的 applicative functor
Monoid 类型类
newtype
首先让我们观察 ZipList
和 getZipList
的类型
思考我们应该如何定义 ZipList
呢,一种方式是使用 data
关键词
或者用记录语法
另一种方式是使用 newtype
关键词
使用 newtype
关键词,相当于将一个已有类型包裹成一个新的类型
当使用 newtype
来根据已有类型创建新类型时,只能有一个值构造器,这个值构造器也只能有一个字段
我们可以使用 newtype
关键词创建类型类的实例,就像之前对 ZipList
所做的那样,这里再举一个例子
我们希望元组 (a,b)
能够成为 Functor
类型类的实例,但是 (a,b)
有两个类型变量,而且似乎也不好部分应用
于是我们使用 newtype
关键词来创建一个新类型
注意这里 a
和 b
的顺序,这样部分应用就是限定了元组的第二项了
这里使用了模式匹配,可以认为 fmap
的类型为
注意这里 Pair c a
对应 (a,c)
,Pair c b
对应 (b,c)
来装载脚本测试一下
使用 newtype
关键词和使用 data
关键词还有什么区别呢,实际上 newtype
比 data
模式匹配的惰性程度更深(因此往往也更快)
举个例子来说明,这里迫使 Haskell 计算 undefined
会触发异常,然而利用 List
的惰性就能免于计算 undefined
现在们考虑下面的类型
来装载脚本测试一下
在定义中,无论 CoolBool
里的 Bool
是什么,都会返回 "hello"
,然而由于用 data
关键词定义的类型可以有多个值构造器,所以检查提供给函数的参数是否匹配模式 CoolBool _
,Haskell 必须要计算这个参数,在这里就是 undefined
现在我们使用 newtype
关键词并重新测试
因为用 newtype
关键词定义的类型只能有一个值构造器,所以在这里能够匹配模式 CoolBool _
,就不需要计算提供给函数的参数了
Monoid
幺半群
来看看 Monoid
类型类的定义
首先,只有具体类型才能成为 Monoid
类型类的实例,因为这里的 m
没有类型变量
其次,mempty
表示单位元
然后,mappend
表示二元运算,请无视这个有误导性的名字
最后,mconcat
取一个 monoid 组成的列表,并规归约成一个 monoid,它一个默认的实现
monoid laws
主要是满足单位元的性质和二元运算的结合律
List monoid
注意这里的 [a]
,表示这里是一个列表的具体类型,而非列表的类型构造器 []
看几个例子
Product and Sum
对应了乘法与加法
现在问题是如何将数变成 Monoid
类型类的实例,我们可以使用 newtype
关键词
模块 Data.Monoid
给出了 Product
的定义
这里有一个 Num a
的类型约束,说明对于任何 Num
类型类的实例 a
,Product a
都是 Monoid
类型类的实例
来看个例子
Sum
与 Product
类似,不再赘述
Any and All
Bool
有两种方式成为 Monoid
类型类的实例
或
Ordering monoid
Ordering
类型也可以成为 Monoid
类型类的实例
类比用字典序比较单词(高位优先)
这样操作的好处就是能够允许我们用不同衡量标准来比较事物,并且根据重要性放置衡量标准,来看个例子
我们写一个比较字符串的例子,第一优先级为长度,第二优先级为字典序,可以这样写
如果利用 Ordering
类型是 Monoid
类型类的实例这一点,可以重写为
这种写法不仅清晰,而且容易扩展
Maybe monoid
Maybe a
有多种方式成为 Monoid
类型类的实例
若类型参数 a
为 monoid 时,可以这样实现
来测试一下
如果类型参数 a
不是 monoid,在 mappend
的两个参数都是 Just
时,我们就不能对 Just
包裹的值进行 mappend
操作,一种方案时简单的丢弃第二个 Just
,出于这个目的,我们有 First
类型
看一看 First
的类型签名
用途是取出第一个非 Nothing
,如果没有就返回 Nothing
类似的我们也有 Last
类型
Foldable 类型类
处理 monoid 的一个更有趣的方式是让它们帮我们折叠各种数据结构,比如 List
为此 Haskell 引入了 Foldable
类型类,表示可以折叠的东西,我们可以观察 foldr
的类型签名
除了 List
,Maybe
也是 Foldable
类型类的实例
现在我们想让 Tree
类型成为 Foldable
类型类的实例
为此我们需要实现 foldMap
方法
需要注意的是,这里最后要用 mappend
把结果合并为一个 monoid
于是在这种实现下,顺序是左子树、当前节点、右子树,也就是中序遍历
装载脚本测试一下
Monad 类型类
monad,中译为单子
回顾
一个普通函数,一个 functor
一个包含函数的 applicative functor,一个 applicative functor
一个普通函数,任意多个 applicative functor
现在让我们思考这样一个函数
现在的问题是我们有一个带有上下文的值 m a
,如何对它应用这样一个函数,这个函数取类型为 a
的参数,返回一个带有上下文的值。
为了具体起见,我们限定上下文为 Maybe
,并实现 (>>=)
定义
这里的 return
和 Applicative
类型类中的 pure
是一样的,这里暗示了 IO
是 Monad
类型类的实例
其余的函数我们后面用到的时候再说
Maybe monad
现在来看看 Maybe
是如何成为 Monad
类型类的实例
和我们之间的实现是差不多的,在交互界面试试
现在我们要用 >>=
来处理若干带有 Maybe
上下文的值的计算(Applicative
类型类无法处理)
考虑这样一个问题,一个人拿着平衡杆玩高空绳索行走,这里假设平衡杆两边的鸟的数目差在三之内的时候,人能够保持平衡,我们需要模拟鸟飞来飞去以及人是否能保持平衡
先来定义一些类型
接下来便是模拟鸟飞来飞去的函数
因为一旦人失去平衡,Pole
的二元组就失去了意义,所以我们用 Maybe
来表达这种上下文
装载脚本测试一下
没有问题,现在关键的地方来了,我们可以用 >>=
表达一连串的停靠操作
这都要归功于 >>=
函数的类型和语义,它在变换中保存着上下文(人保持平衡或失去平衡),变换的每一步都会依赖上一步的结果
现在让我们来设计一个香蕉,它可以让人直接失去平衡
重新装载脚本测试
为了简化这种操作,我们可以使用 >>
函数,来看一下它的类型与默认定义
可知 >>
函数将一个 monad 传递给一个忽略参数,返回一个 monad 的函数,结果就是返回的那个 monad
于是我们把上面的 banana
换成 Nothing
总结一下,把返回值变成 Maybe
值,把普通的函数应用变成 >>=
,我们就可以轻松地处理可能会失败的计算,并传递上下文,在这里,上下文就是可能会失败的计算。
do notation
我们在 IO
的 do blocks
中已经遇到了这种语法,要想理解这种语法,我们需要嵌套使用 >>=
为了清晰起见,我们让每一个 monad 单独占一行,并写到脚本里
使用 do notation
,可以写成
在 do notation
中,每一行都是一个 monad,要获取 monad 的值就使用 <-
如果某一行没有 <-
,就相当于我们用 >>
函数忽略了这个 monad 的值
这也解释了为什么在 IO
中最后一个 action
不能绑定任何名字(无法变换为嵌套 >>=
)
对于 Nothing
的情形,都可以使用 do notation
改写
让我们用 do notation
重新描述前面的鸟人问题
使用 do notation
,我们需要显式的写出前一步的结果
补充一点,我们可以在 <-
时使用模式匹配
如果模式匹配失败了,fail
函数会被调用,下面是默认实现
Maybe
的 fail
函数被实现为
List monad
之前 Maybe
的上下文是可能会失败的计算,现在 List
的上下文是非确定性计算
来看看 List
如何成为 Monad
类型类的实例
下面是几个简单的示例
这里的 []
就相当于 Nothing
下面是 >>=
的组合使用
用 do notation
可以改写为
还可以用列表推导式改写
由此可见,列表推导式正是 List monad 的语法糖
那列表推导式中的过滤呢,我们需要先引入几个概念
MonadPlus
类型类用来表示具有 monoid 行为的 monad
List
是这个类型类的实例
再来看一个函数
guard
函数取一个布尔值,如果为 True
,将 ()
放在最小上下文中返回(这样做是为了让计算继续进行下去),否则产生一个表示计算失败的 monad
来看几个例子
在列表推导式的过滤中,我们用 guard
函数来过滤非确定性计算
使用 do notation
改写
如果忘记写 return x
这一行,结果便是 [()]
monad laws
return x >>= f
= f x
m >>= return
= m
(m >>= f) >>= g
= m >>= (\x -> f x >>= g)
这样描述也许看起来不太明显,模块 Control.Monad
导出了一个函数 (<=<)
,其类型为
来测试一下
如此,我们便将两个 monad 式的函数组合了起来,上面的三条定律也可以等价描述为
f <=< return
= f
return <=< f
= f
f <=< (g <=< h)
= (f <=< g) <=< h
不难看出,这主要是为了满足单位元的性质和二元运算的结合律
Writer monad
用来表示附有日志的值,这里日志是 Monoid
类型类的实例(便于使用 mappend
)
注意这里使用了 newtype
关键词,将一个二元组包裹成一个新的类型
return
函数产生的最小上下文让附着的日志为 mempty
,这是合乎逻辑的
让我们理解一下 >>=
函数的实现,这里 f x
返回一个 monad,我们使用模式匹配,取出新的值,并用 mappend
作用于原来的日志与新的日志。
来测试一下
字符串是 List
的语法糖,当然是 Monoid
类型类的实例
由于 Writer
并非 Show
的实例,我们需要用 runWriter
提取被包裹的二元组
下面我们试试 do notation
,将两个数相乘,并附上日志
我们用 return
让 a*b
成为结果,因为 return
让附着的日志为 mempty
,所以不会往日志里添加内容
需要提醒的时,模块 Control.Monad.Writer
没有导出 Writer
的值构造器,而导出了 writer
函数(作用与值构造器相同,包裹一个二元组)
装载测试一下
有时候我们想显式地扩充日志,可以使用 tell
函数,它创建了一个表示 ()
的 monad
我们修改上面的例子
重新装载测试
再来一个例子,我们为欧几里得算法配备日志功能
这里 b == 0
的情况也可以用一行解决
装载测试一下
妙极了
然而,如果我们记录日志的顺序反过来,就像这样
装载测试的结果
List
拼接的效率就会降低(原来是右结合,现在是左结合),更多解释见 performance - Why are difference lists more efficient than regular concatenation in Haskell? - Stack Overflow
这是我们就要引入一个新的数据结构 DiffList
DiffList
DiffList
实际上就是一个取 List
为参数,把另一个 List
放在其前面的函数
比如 [1,2,3]
的 DiffList
便是 \xs -> [1,2,3] ++ xs
,简写为 [1,2,3]++
我们给出类型定义
并给出 List
和 DiffList
的互相转换
下面让 DiffList
成为 Monoid
类型类的实例
这里要注意的是,原书中的写法已经过时了,遂按照 A basic Monoid definition gives “No instance for (Semigroup MyMonoid) arising from the superclasses of an instance declaration” - Stack Overflow 中的回答修改
关键是下面这段话:
(<>)
has been moved from Monoid
to Semigroup
, and all Monoid instances are required to also be Semigroup. mappend
is just a synonym for (<>)
.
装载后测试
这样我们就可以提升 gcdReverse
的性能了
装载后测试良好
Reader monad
回顾函数类型是 Functor 函子和 Applicative 函子
其实函数类型也是 Monad
类型类的实例,函数的上下文是结果值还没有得到,我们需要给函数提供一个参数来得到结果
来解读一下 >>=
的实现,这里的 h
是一个函数,(h w)
得到结果后,再对结果应用 f
,返回一个 lambda 形式的匿名函数
我们把上面 Applicative 函子的例子用 do notation
重写为
装载后测试
由于这里所有的函数都从一个共同的源头读取参数,function monad 也被称为 Reader monad
State monad
我们可以使用 State monad 优雅的表示带状态的计算
其中 s
表示状态类型,a
表示结果类型
联系 Writer monad 和 Reader monad 来解读这里的实现
return
创建了一个以特定值为结果的带状态计算,这个计算不会改变当前状态 s
,注意要用 State
值构造器将其包裹起来
>>=
则先通过将 h
(一个带状态的计算)作用在当前状态 s
上,得到结果 a
和新的状态 newState
,并使用函数 f
作用在结果 a
上,得到一个新的带状态的计算,并返回新的带状态的计算和新的状态
来看看如何用 State monad 实现一个栈
和 Writer monad 一样,我们使用 state
函数充当值构造器
装载后测试一下
当然我们这里的 stackManip
也是一个带状态的计算,我们可以在其他的带状态的计算中将其复用
这里还要介绍两个重要的函数 get
和 put
可以这样去理解
我们可以用 get
和 put
来实现入栈和出栈的操作
特别要注意上面实现中的模式匹配,需要保证栈中元素数目不为零才能进行模式匹配,否则返回 undefined
State monad 可以让我们不用显式地写出每一步的状态,这可以帮助我们优化前面的鸟人问题的 do notation
写法以及随机数的产生,来回顾一下掷三个硬币的问题
使用 State monad,我们可以这样写
装载测试一下
Either monad
类似 Maybe monad,上下文是可能失败的计算,但是这里有失败的具体信息
这里的 return
类似 Maybe monad 中的 return
>>=
检查左边的参数,若为 Right x
,将 f
作用在 x
上,否则保持 Left err
不变
这里还要求 Left
包裹的值的类型是 Error
类型类的实例
尝试导入 Control.Monad.Error
,发现好像被废弃了,那就不管 fail
和 strMsg
函数了
来测试一下
注意最后一个例子,Haskell 无法推断出 Either e
中的类型参数 e
,所以我们显式加上类型注解
我们用 Either monad 优化前面的鸟人问题,用错误信息表示杆子的两边分别有几只鸟
装载测试
monadic functions
以 monad 为参数或返回值的函数称为 monadic functions
liftM
先看类型
我们发现 liftM
的类型与 fmap
几乎一致,只不过两者的类型约束不同
回顾一下,在 Monad
类型类的定义中,我们并没有对实例的类型进行约束
这就意味着,如果我们能够实现 liftM
函数,那么 monad 提供的功能不会弱于 functor(我们只用 Monad
类型类提供给我们的函数,就实现了和 fmap
相同的效果)
现在我们来实现 liftM
函数
或使用 do notation
来测试一下
可以发现,liftM
函数或者 fmap
不会改变 monad 的上下文(在这里便是附有的日志)
ap
先看类型
我们发现 ap
的类型与 (<*>)
几乎一致,只不过两者的类型约束不同
于是相似的一幕将要发生了,我们来实现 ap
函数
于是,monad 提供的功能不会弱于 applicative functor
来测试一下
liftM2
对于 applicative style,我们有类似的 liftM2
函数,对应了 liftA2
函数
其实现与 liftM
类似,来测试一下
Monad
类型类已经杀疯了,我们来总结一下,如果某类型是 Monad
类型类的实例:
- 将
fmap
定义为 liftM
,便可以得到 Functor
类型类的实例
- 将
pure
定义为 return
,(<*>)
定义为 ap
,便可以得到 Applicative
类型类的实例
join
更进一步,可以将嵌套的 monad 展平,例如让 Just (Just 9)
变成 Just 9
,我们可以使用 join
函数
我们来实现它
这里的关键是,当执行 m <- mm
时,monad 的上下文已经被考虑到了
先试试 Maybe
和 Either
对于 List
,join
等价于 concat
注意这里 mappend
的顺序
装载上我们栈的实现来试试
这里的 [0,0,0]
作为参数传入 lambda 中的 s
对于 join
函数,一个重要的特性是 m >>= f
等同于 join (fmap f m)
,先举一个例子
这等价于 join (fmap (\x -> Just (Just (x + 1))) Just 9)
,也就是 join (Just (Just (Just 10)))
,即 Just (Just 10)
(不过好像不能直接在交互界面计算),其用途在后面会得到体现
对于 join
函数,另一个重要的特性是它无法通过 Applicative
类型类或者 Functor
类型类提供的函数实现
filterM
对比 filter
的类型
filterM
的谓词函数有一个上下文,举个例子,我们让过滤带上日志功能
装载测试一下
另一个例子是我们可以用 filterM
得到一个列表的幂集
这里的上下文是非确定性,装载测试一下
foldM
对应 foldl
举个例子,下面的折叠拥有可能会失败的上下文
装载测试一下
Making monads
概率列表,令列表中的每一个元素都伴随着这个元素出现的概率
由于浮点数有精度问题,我们将浮点数换为有理数
其中模块 Data.Ratio
导入了有理数
我们来创建一个新的类型
这首先是 Functor
类型类的实例,我们保持上下文(概率)不变
下面我们让它成为 Monad
类型类的实例
return
函数容易实现,单元素列表,对应的概率上下文为 1
思考 >>=
函数,由于 m >>= f
等同于 join (fmap f m)
,我们只要实现对应的 join
就可以了,我们称它为 flatten
在新版本中,还要让其成为 Applicative
类型类的实例
概率列表可以帮助我们处理概率,比如说我们有两枚普通硬币和一枚不均匀的硬币,如果同时投掷三枚硬币,三枚都是反面朝上的概率是多少,我们装载模块进行测试
这里 True
(也就是三枚都是反面朝上)的占比是 9/40
(思考如何合并同类项)
另一个重要的话题是,我们需要确认类型 Prob
满足三个类型类的定律
在这里 Applicative
类型类的实现似乎有点问题(第一个参数只能是单元素列表,而且类型会报错)
我的想法是让函数和参数都拥有概率的上下文,对于函数的概率
相当于
总之每次计算后,合并同类项,并使概率之和为 1 % 1
问题求解
快速排序
知识点:递归函数
TODO
知识点:IO、文件读写、命令行参数
最短路径
知识点:数据类型的创建
马走日
知识点:List monad、composing monadic functions
先简单介绍一下 composing monadic functions,其实核心就是在 monad laws 中提到的函数 (<=<)
对于普通的函数列表,想将其组合起来,可以这样做
对于 monad 式的函数列表,我们只要用 (<=<)
来代替 (.)
,return
来代替 id
就可以完成相同的效果
现在我们来考虑马走日问题,给定初始位置,在给定步数内,能否走到终点位置,限定棋盘为 8×8
的大小
我们用二元组表示马的位置
函数 moveKnight
中的 <-
表示 List monad,是非确定性计算,并使用 guard
处理越界的情况
函数 inMany
表示在给定步数内马能够走到的位置
装载测试一下
若这些计算都是惰性的,那么蕴藏在这段程序后的思想便是 BFS
逆波兰表达式求值
知识点:foldM、Maybe monad
我们用一个被 Maybe
包裹的列表表示操作数栈,由于这里表示可能会失败的计算,我们使用 return
和 liftM
对被包裹的列表进行拼接
这里的 readMaybe
中用到了 reads
函数
当解析成功时,reads
返回一个单元素的列表,这个元素是一个二元组,前面的是成功读入的值,后面是没有被消耗的部分,若解析失败,则返回一个空列表(这里的解析是对 Double
类型的解析,如 "3.14"
可以被成功解析,"hello"
则不能被解析)
在 readMaybe
中,我们认为只有消耗了完整的输入才算读入成功
装载测试一下
来几个失败的例子
第一次失败是因为最后栈的长度大于 1
,solveRPN
函数中的模式匹配失败了,第三次失败是因为没有消耗了完整的输入
来看一下第二次失败的原因,在得到 ["1","2","*","*","4","5"]
后,第一个和第二个读取的是数字,压入栈中,第三个读取到了 *
,函数模式匹配和列表模式匹配成功,return
后栈中只有一个数字,第四个读取到了 *
,此时列表模式匹配失败(对 (x:y:ys)
的匹配)
Zipper
Zipper - HaskellWiki
在某个数据结构的基础上,使用 Zipper
来定位数据结构中的一个特定部分
List
场景:文本编辑器,表示光标所在的行
装载测试一下
Tree
场景:文件系统,文件和文件夹
解读一下 Zipper
数据类型,第一个分量记录了以当前焦点为根节点的树的信息,第二个分量记录了在树上移动的轨迹,这里的轨迹是一个列表,列表中的每一个元素代表了向左走(并且记录了向左走时离开的那个节点以及没有被我们访问的右子树)或是向右走
为了更加清楚的显示移动轨迹,这里自定义了 Crumb
类型的表示方法
由于 goLeft
、goRight
和 goUp
在移动当前焦点的位置时,可能会导致模式匹配失败(例如在空树上向左或向右走,或者在原树根上向上走)因此在函数的实现中引入了 Maybe monad
goLeft
、goRight
和 goUp
移动当前焦点的位置,同时记录移动的轨迹
modify
修改当前焦点代表的树的根节点,若当前焦点代表了空树,则保持原样
attach
替换当前焦点代表的树,使用场景一般是当前焦点代表了空树,我们替换成一棵非空的树
topMost
则移动到原树根
装载测试一下
来些 Nothing
的例子
需要注意的是,当我们通过 modify
和 attach
改变了一棵树时,并没有修改原来的树,而是返回了一棵新的树,这是因为 Haskell 的纯粹性,不允许赋值。也就是说,通过使用 Zipper
,我们无偿获得了版本功能,这便是 Haskell 数据结构的持久性所带来的价值。