论 fmap、fmap fmap、与 fmap fmap fmap

本文探讨了Haskell中fmap的用法,包括fmap、fmap fmap和fmap fmap fmap。通过类型推导分析了fmap如何将函数应用于不同类型的结构,并解释了遇到fmap的多重应用时可能出现的类型混淆。文章强调了理解Haskell类型系统的重要性,指出积累经验和培养直觉是成为优秀Haskell程序员的关键。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

论 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)

对于 Listfmapmap 等价;对于 Maybefmap 只将提升后的函数作用在 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 中的 fab 具体所指的类型不一定会对应相同,为了加以分辨,两个函数的函数名和类型变量名将写为:

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 得到的结果包含两个 Functorff1,此时它依然可以理解为一个提升函数:将 f 世界的一个函数 (a -> b) 提升到能够处理“嵌入”到 f 世界的来自 f1 世界的成员。这句话比较拗口,我举个具体的例子:

  1. 有函数 fun,参数为 a,计算结果为 b,该函数可能确实存在,也可能没有值,不难想到: fun' :: Maybe (a -> b)
  2. 有变量 v 为元素类型为 a 的列表,该列表可能存在,也可能没有值(注意并非 []),即 v :: Maybe [a]
  3. 要求提升 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(直觉)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值