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 的许多最强大的功能和最复杂的功能都依赖于类型类(位于标准库中或其他地方)。从现在开始,类将是我们研究中的一个突出存在。