论 fmap、fmap fmap、与 fmap fmap fmap
希望标题没有吓到读者……
fmap
长话短说,首先让我们看一下 Haskell 的 Functor 中 fmap
类型信息:
fmap :: Functor f => (a -> b) -> f a -> f b
为简洁起见,文本文字部分的对 Functor 的 typeclass 约束将会省略。
fmap
的作用可以简单理解为将普通函数 (a -> b)
提升到能够作用于 f
所在的世界:(f a -> f b)
。
对于 List
,fmap
与 map
等价;对于 Maybe
,fmap
只将提升后的函数作用在 Just
字段的值上,Nothing
的情况则直接返回 Nothing
……等等这些 fmap
的应用理解起来相当简单。
然而,如果你偶然间遇到了 fmap fmap
甚至 fmap fmap fmap
,你是否能够理解它们的含义?
fmap fmap
若不明其类型信息,何谈了解?让我们先看 fmap fmap
:
Prelude> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
Prelude> :t fmap fmap
fmap fmap
:: (Functor f1, Functor f) => f (a -> b) -> f (f1 a -> f1 b)
对 fmap
的表述是:将某函数“提升”。在这里,我们要提升的函数恰恰就是另 fmap
。两个 fmap
中的 f
、a
、b
具体所指的类型不一定会对应相同,为了加以分辨,两个函数的函数名和类型变量名将写为:
fmap :: Functor f => (a -> b ) -> f a -> f b
fmap' :: Functor f1 => (a1 -> b1) -> f1 a1 -> f1 b1
来思考此时的 fmap fmap'
。fmap
传入 fmap'
作为参数, (a -> b)
被 (a1 -> b1) -> (f1 a1 -> f1 b1)
整个置换,a
变成了 (a1 -> b1)
,b
变成了 (f1 a1 -> f1 b1)
。对其返回结果也做同样的置换,(f a) -> (f b)
因此变成了 f (a1 -> b1) -> f (f1 a1 -> f1 b1)
,变下名称也就成了 GHCI 里呈现的正确信息。
fmap fmap
得到的结果包含两个 Functor
:f
和 f1
,此时它依然可以理解为一个提升函数:将 f
世界的一个函数 (a -> b)
提升到能够处理“嵌入”到 f
世界的来自 f1
世界的成员。这句话比较拗口,我举个具体的例子:
- 有函数 fun,参数为
a
,计算结果为b
,该函数可能确实存在,也可能没有值,不难想到:fun' :: Maybe (a -> b)
。 - 有变量
v
为元素类型为a
的列表,该列表可能存在,也可能没有值(注意并非[]
),即v :: Maybe [a]
- 要求提升
fun
,在函数和数据两者都不是Nothing
的情况下作用其至v
的每一个元素。
常规实现:
case fun of
Just fun' -> case v of
Just v' -> fmap fun' v'
Nothing -> Nothing
Nothing -> Nothing
“双 fmap
” + Applicative:
fmap fmap fun <*> v
是不是“奇技淫巧”应有的风范呢?
(目前我发现的 fmap fmap 最有用且通用的情境是编写 Monad Transformer 的时候。)
fmap fmap fmap
通过 fmap fmap
的例子,读者应已对类型推导中的置换有所熟练。而接下来,简单的置换可能就不管用了。
不管怎样,当我看到 fmap fmap fmap
的时候,一句响亮的“WHAT THE HELL?”几乎就要脱口而出了。
冷静下来后,顺着刚才的思路你可能会想,既然:
fmap fmap
:: (Functor f1, Functor f) => f (a -> b) -> f (f1 a -> f1 b)
那 fmap fmap fmap
一定是将上面的结果再提升一层,变成:
fmap fmap fmap
:: (Functor f2, Functor f1, Functor f) =>
(a -> b) -> f (f1 (f2 a)) -> f (f1 (f2 b))
咯?
我也如此想过,于是得意地去看 GHCI 的结果:
Prelude> :t fmap fmap fmap
fmap fmap fmap
:: (Functor f1, Functor f) => (a -> b) -> f (f1 a) -> f (f1 b)
WHAT THE HELL? 这不可思议的类型是怎么来的?
读者仔细想想就可能发现端倪:Haskell 中的函数应用是左结合的,也就是说 fmap fmap fmap
等价于 (fmap fmap) fmap
,而我们期待的结果——将 fmap fmap
再提升一层,明显应该是 fmap (fmap fmap)
。
否定了之前的结论以后,我们只能继续在脑子里对类型进行推导。
根据结合规则和柯里化性质看来,fmap fmap fmap
中,可将前两个 fmap
视为函数整体,该函数类型为 f (a -> b) -> f (f1 a -> f1 b)
,而将第三个 fmap
理解为参数。然而 f (a -> b)
怎么可能和 (a -> b) -> (f1 a -> f1 b)
匹配呢(这里区分了两个 Functor
)?
原因在于在 Haskell 中,函数本身也是一种类型,更进一步的,(->)
是一个拥有两个类型参数的类型构造器,函数类型 (a -> b)
可以写作 ((->) a) b
。而在 Functor
的众多 instances 中,确有函数:
instance Functor ((->) r) where
fmap = (.)
讲到这里应该已经明确了。(a -> b) -> (f1 a -> f1 b)
可以写成 ((->) (a -> b)) (f1 a -> f1 b)
,如此一来(该代码片段中的 typeclass 约束已省略):
fmap fmap :: f (a -> b) -> f (f1 a -> f1 b)
fmap' :: ((->) (a2 -> b2)) (f2 a2 -> f2 b2)
-- 在 fmap fmap fmap' 中:
-- f := ((->) (a2 -> b2))
-- (a -> b) := (f2 a2 -> f2 b2)
-- a := f2 a2
-- b := f2 b2
-- 因此:fmap fmap fmap' :: ((->) (a2 -> b2)) (f1 (f2 a2) -> f2 (f2 b2))
-- 因此:fmap fmap fmap' :: (a2 -> b2) -> (f1 (f2 a2) -> f1 (f2 b2))
fmap fmap fmap' :: (a -> b) -> f (f1 a) -> f (f1 b)
限于篇幅,这里就不再讨论其用例了,读者可自行发掘。
fmap fmap fmap fmap ...
组合下去的规律皆可使用以上方法推导。
结论
基于对 fmap fmap
以及 fmap fmap fmap
的分析,不难发现,Haskell 的类型系统在某些情况下,理解起来还是需要相当缜密的思维和必要的知识的。正如 Euclid 说过的 There is no royal road to Haskell,成为一名好的 Haskell 程序员确实没有捷径,其结构的复杂和与数学紧密的相关性,在使 Haskell 成为一门威力强大、简洁优雅的语言的同时,也大大提升了其学习的难度——只有不断地积累经验才可培养被国外 Haskell 爱好者时常挂在嘴边的 Intuition(直觉)。