Haskell/Functor 类
在本章中,我们将介绍重要的 Functor
类型类。
在 其他数据结构 中,我们看到了对某些分组值的所有元素适用的操作。主要例子是 map
,它适用于列表。我们已经解决的另一个例子是以下 Tree 数据类型
data Tree a = Leaf a | Branch (Tree a) (Tree a) deriving (Show)
我们为 Tree 编写的 map 函数是
treeMap :: (a -> b) -> Tree a -> Tree b
treeMap f (Leaf x) = Leaf (f x)
treeMap f (Branch left right) = Branch (treeMap f left) (treeMap f right)
正如之前讨论的,我们可以设想为任何任意数据结构定义一个类似 map 的函数。
当我们在 Lists II 中首次介绍 map
时,我们经历了将列表元素的特定函数泛化的过程,以展示 map
如何将任何合适的函数与各种列表组合在一起。现在,我们将进一步泛化。除了 map-for-lists 和 map-for-trees 以及其他不同的 map 之外,如何将所有类型的可映射类型的 map 概念进行泛化呢?
Functor
是一个 Prelude 类,用于可以映射其上的类型。它只有一个方法,称为 fmap
。该类定义如下
class Functor f where
fmap :: (a -> b) -> f a -> f b
类型变量 f
的使用一开始可能看起来有点奇怪。这里,f
是一个参数化数据类型;在 fmap
的签名中,f
在其一个出现中接受 a
作为类型参数,而在另一个出现中接受 b
。让我们考虑一个 Functor
的实例:通过用 Maybe
替换 f
,我们得到 fmap
的以下签名...
fmap :: (a -> b) -> Maybe a -> Maybe b
... 这符合自然定义
instance Functor Maybe where
fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)
(顺便说一句,这个定义在 Prelude 中;因此,我们不需要为 "其他数据结构" 一章中的那个例子真正实现 maybeMap
。)
列表的 Functor
实例(也在 Prelude 中)很简单
instance Functor [] where
fmap = map
... 并且如果我们在 fmap
签名中用 []
替换 f
,我们将得到 map
的熟悉类型。
因此,fmap
是任何参数化数据类型的 map
的泛化。[1]
自然地,我们可以为我们自己的数据类型提供 Functor
实例。特别是,treeMap
可以立即重新分配到一个实例中
instance Functor Tree where
fmap f (Leaf x) = Leaf (f x)
fmap f (Branch left right) = Branch (fmap f left) (fmap f right)
以下是用上面的实例演示 fmap
的简短演示(要重现它,你只需要加载 Tree
的 data
和 instance
声明;其他的已经在 Prelude 中了)
*Main> fmap (2*) [1,2,3,4] [2,4,6,8] *Main> fmap (2*) (Just 1) Just 2 *Main> fmap (fmap (2*)) [Just 1, Just 2, Just 3, Nothing] [Just 2, Just 4, Just 6, Nothing] *Main> fmap (2*) (Branch (Branch (Leaf 1) (Leaf 2)) (Branch (Leaf 3) (Leaf 4))) Branch (Branch (Leaf 2) (Leaf 4)) (Branch (Leaf 6) (Leaf 8))
注意
除了 []
和 Maybe
之外,还有许多其他已定义的 Functor
实例。从 Prelude 中提供的那些在 Data.Functor 模块中列出。
在提供 Functor
的新实例时,你应该确保它满足两个 Functor 定律。这些定律没有什么神秘之处;它们的作用是保证 fmap
行为正常并实际上执行映射操作(而不是其他任何无意义的操作)。[2] 第一定律是
fmap id = id
id
是恒等函数,它返回其参数而不改变。第一定律指出,在 functorial 值上映射 id
必须返回未更改的 functorial 值。
接下来,第二个定律
fmap (g . f) = fmap g . fmap f
它指出,无论我们是否映射一个组合函数,或者先映射一个函数,然后再映射另一个函数(假设两种情况下应用顺序保持相同),都不应该有任何区别。
在这一点上,我们可以问,Functor
类带来的额外泛化层给我们带来了什么好处。有两个显著的优势
fmap
方法的可用性让我们无需再回忆、阅读和编写大量不同命名的映射方法(maybeMap
、treeMap
、weirdMap
,等等)。因此,代码变得既干净又易于理解。在发现fmap
的使用时,我们立即对正在发生的事情有一个大致了解。[3] 由于 Functor 定律给出的保证,这个大致了解出奇地精确。
- 使用类型类系统,我们可以编写基于
fmap
的算法,这些算法可以立即与任何 Functor 一起使用 - 无论是[]
、Maybe
、Tree
还是你需要的任何其他类型。事实上,核心库中的许多有用类都继承自Functor
。
类型类使我们能够为整类问题创建通用解决方案。根据你使用 Haskell 的目的,你可能不需要经常定义新类,但你一定会经常使用类型类。Haskell 的许多最强大的功能和最复杂的功能都依赖于类型类(位于标准库中或其他地方)。从现在开始,类将是我们研究中的一个突出存在。