Luyu Cheng

What are Functors?

haskell
functional-programming
functor

初学 Haskell 时,总是不太理解{函子}^(functor)的概念。虽然看了很多文章,但还是感觉隔着一层迷雾。最近写了很多 TypeScript,和类型系统做了很多搏斗(笑),再回来看函子的概念,突然感觉比之前更清晰了。

很多数据结构,用{范型}^(generic)描述起来大都是 Structure<T> 的形式,例如 LinkedList<T>BinarySearchTree<T>Optional<T> 等等。这类数据结构虽然对数据的组织各有差异,但都建构于某个类型之上,或者说「持有」某个类型的数据。在下文中,我们称之为{容器类型}^(container types)。

对容器类型来说,一个常见的操作就是对容器中的元素进行遍历,以生成新的元素。在没有这个抽象的情况下,你可能会写很多种不同的遍历。有了函子后,这种重复工作可以被抽象成一个简单的 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

带入到上面的函数,可以看出,它代表了一个最常见的操作——丢掉原来容器类型里的内容,把某个有价值的变量(用 $ 表示)输送(用 < 表示)到一个容器类型中,这么看 <$ 这个符号还是挺传神的。

什么是{自函子}^(endofunctor)?

我相信大家都听过这句话了:「{单子}^(monad)不就是*{自函子}^(endofunctor)*范畴上的一个{幺半群}^(monoid)吗?有什么难理解的?」{自函子}^(endofunctor)的定义用一句话就能概括:它是一种特殊的函子,其变换的输入类型(定义域)和输出类型(值域)是同一个类型。

Haskell 中自带 Endo 类型,它的定义是参数和返回值都是相同类型的一元函数。

newtype Endo a = Endo { appEndo :: a -> a }

值得一提的是,endo 这个前缀来源于希腊语的 {ἔνδον}^(endon),含义是「{在…里面,内部的,被…吸收或包含的}^(within, inner, absorbing, or containing)」。

有关 fmapmap 命名

最开始读《Real World Haskell》的时候,看着示例代码里成片的 fmap,我心里浮现出一个疑问:fmap 在数组类型上的实现明明和 map 相同,为什么不直接把 Functor 中的 fmap 改名为 map 呢?后来我在 Haskell 的 wiki 上看到了这个问题的解答。

Why not just do away with the current list-only map function, and rename fmap to map instead? Well, that’s a good question. The usual argument is that someone just learning Haskell, when using map incorrectly, would much rather see an error about lists than about Functors.