跳转到内容

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

[编辑 | 编辑源代码]

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 的简短演示(要重现它,你只需要加载 Treedatainstance 声明;其他的已经在 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 的新实例时,你应该确保它满足两个 Functor 定律。这些定律没有什么神秘之处;它们的作用是保证 fmap 行为正常并实际上执行映射操作(而不是其他任何无意义的操作)。[2] 第一定律是

fmap id = id

id 是恒等函数,它返回其参数而不改变。第一定律指出,在 functorial 值上映射 id 必须返回未更改的 functorial 值。

接下来,第二个定律

fmap (g . f) = fmap g . fmap f

它指出,无论我们是否映射一个组合函数,或者先映射一个函数,然后再映射另一个函数(假设两种情况下应用顺序保持相同),都不应该有任何区别。

我们得到了什么?

[编辑 | 编辑源代码]

在这一点上,我们可以问,Functor 类带来的额外泛化层给我们带来了什么好处。有两个显著的优势

  • fmap 方法的可用性让我们无需再回忆、阅读和编写大量不同命名的映射方法(maybeMaptreeMapweirdMap,等等)。因此,代码变得既干净又易于理解。在发现 fmap 的使用时,我们立即对正在发生的事情有一个大致了解。[3] 由于 Functor 定律给出的保证,这个大致了解出奇地精确。
  • 使用类型类系统,我们可以编写基于 fmap 的算法,这些算法可以立即与任何 Functor 一起使用 - 无论是 []MaybeTree 还是你需要的任何其他类型。事实上,核心库中的许多有用类都继承自 Functor

类型类使我们能够为整类问题创建通用解决方案。根据你使用 Haskell 的目的,你可能不需要经常定义新类,但你一定会经常使用类型类。Haskell 的许多最强大的功能和最复杂的功能都依赖于类型类(位于标准库中或其他地方)。从现在开始,类将是我们研究中的一个突出存在。

笔记

  1. 数据结构提供了最直观的例子;但是,有一些 Functor 不能合理地被视为数据结构。一个常见的隐喻是将 Functor 视为容器;然而,就像所有隐喻一样,它只能延伸到一定程度。
  2. 定律排除了无意义操作的一些例子:从列表中删除或添加元素,反转列表,将 Just 值更改为 Nothing
  3. 这类似于用基于高阶函数的实现替换列表上的显式递归算法带来的清晰度提升。
华夏公益教科书