初学 Haskell 时,总是不太理解函子的概念。虽然看了很多文章,但还是感觉隔着一层迷雾。最近写了很多 TypeScript,和类型系统做了很多搏斗(笑),再回来看函子的概念,突然感觉比之前更清晰了。
很多数据结构,用范型描述起来大都是 Structure<T>
的形式,例如 LinkedList<T>
、BinarySearchTree<T>
和 Optional<T>
等等。这类数据结构虽然对数据的组织各有差异,但都建构于某个类型之上,或者说「持有」某个类型的数据。在下文中,我们称之为容器类型。
对容器类型来说,一个常见的操作就是对容器中的元素进行遍历,以生成新的元素。在没有这个抽象的情况下,你可能会写很多种不同的遍历。有了函子后,这种重复工作可以被抽象成一个简单的 fmap
函数,你可以把某个在原始类型上的变换,转换为在容器类型上的变换。当然,这是最直观的理解,如果你去翻 Haskell 中 Functor
的定义,你会发现其中还有一个操作符 <$
。
class Functor f where
fmap :: (a -> b) -> f a -> f b
(<$) :: a -> f b -> f a
(<$) = fmap . const
它是做什么用的?回忆一下 const
的定义。
const :: a -> b -> a
const x _ = x
带入到上面的函数,可以看出,它代表了一个最常见的操作——丢掉原来容器类型里的内容,把某个有价值的变量(用 $
表示)输送(用 <
表示)到一个容器类型中,这么看 <$
这个符号还是挺传神的。
什么是自函子?
我相信大家都听过这句话了:「单子不就是自函子范畴上的一个幺半群吗?有什么难理解的?」自函子的定义用一句话就能概括:它是一种特殊的函子,其变换的输入类型(定义域)和输出类型(值域)是同一个类型。
Haskell 中自带 Endo
类型,它的定义是参数和返回值都是相同类型的一元函数。
newtype Endo a = Endo { appEndo :: a -> a }
值得一提的是,endo 这个前缀来源于希腊语的 ἔνδον,含义是「在…里面,内部的,被…吸收或包含的」。
有关 fmap
和 map
命名
最开始读《Real World Haskell》的时候,看着示例代码里成片的 fmap
,我心里浮现出一个疑问:fmap
在数组类型上的实现明明和 map
相同,为什么不直接把 Functor
中的 fmap
改名为 map
呢?后来我在 Haskell 的 wiki 上看到了这个问题的解答。
Why not just do away with the current list-only
map
function, and renamefmap
tomap
instead? Well, that’s a good question. The usual argument is that someone just learning Haskell, when usingmap
incorrectly, would much rather see an error about lists than aboutFunctor
s.