为何fmap无法作用于元组?附Haskell代码报错示例
哈哈,这个坑我刚学Haskell的时候也踩过!先给你拆解一下问题所在,再给你几个解决办法~
首先,你得搞懂Haskell里Functor的基本要求:它只能作用于接受单个类型参数的类型构造器上。比如:
- 列表
[]是单参数构造器([] Int就是整数列表),所以天生符合Functor要求; Maybe也是单参数构造器(Maybe Int表示可能存在的整数),Just 4是Maybe Int的实例,所以fmap能直接用。
那元组为啥不行?因为元组是多参数类型构造器啊!比如你用的5元组(,,,,),它需要5个类型参数才能变成具体类型(比如(Int,Int,Int,Int,Int))。而Functor只认单参数的构造器,所以Haskell给元组的Functor实例是部分应用后的构造器——比如5元组的Functor实例是((,,,,) a b c d),也就是固定前4个类型参数,只把第5个作为可变参数。
你直接写fmap (+3) (10,11,12,13,14)的时候,Haskell没办法自动推导出“固定前4个Int类型”的Functor约束,因为默认不允许这种非类型变量的约束,所以就报了那个错。
解决办法来了!
办法1:开启FlexibleContexts扩展,直接用fmap
这是最简单的方式,开启扩展后Haskell就允许这种约束了,而且fmap会自动作用在元组的最后一个元素上:
Prelude> :set -XFlexibleContexts Prelude> fmap (+3) (10,11,12,13,14) (10,11,12,13,17)
注意哦,这里只会修改最后一个元素,这是元组Functor实例的设计规则——因为部分应用是从左到右固定参数的,所以Functor只能操作最后一个“可变”的位置。
办法2:如果想修改所有元素,别用Functor!
Functor的设计就是只能操作容器里的一个“焦点”元素,要是想给元组的每个元素都加3,你得自己写个工具函数,或者用 lens 库(先给你写个手动版的):
-- 专门处理5元组的映射函数 map5Tuple :: (a -> b, c -> d, e -> f, g -> h, i -> j) -> (a, c, e, g, i) -> (b, d, f, h, j) map5Tuple (f1, f2, f3, f4, f5) (x1, x2, x3, x4, x5) = (f1 x1, f2 x2, f3 x3, f4 x4, f5 x5) -- 调用的时候把每个元素的处理函数传进去 Prelude> map5Tuple ((+3),(+3),(+3),(+3),(+3)) (10,11,12,13,14) (13,14,15,16,17)
要是你经常处理这种多元素元组的映射,也可以去了解一下Control.Lens库,它能更优雅地操作元组的任意位置。
办法3:手动解构重构(不推荐,太麻烦)
要是不想开扩展也不想写函数,那就手动拆元组再拼回去:
Prelude> let t = (10,11,12,13,14) in (fst t, snd t, (\(a,b,c,d,e)->c) t, (\(a,b,c,d,e)->d) t, (+3) $ (\(a,b,c,d,e)->e) t) (10,11,12,13,17)
当然这种方式写起来太啰嗦,除非迫不得已不然别用~
内容的提问来源于stack exchange,提问作者devio

