我会尽力让你相信fmap = (.)
实际上是使容器的形状保持不变,但将函数应用于容器中的所有元素。但在我们这样做之前(->)
,让我们对一些更简单的类型进行处理。具体来说,让我们对具有特定数量元素的容器类型进行此操作 - 即,恰好具有两个元素的容器将是TwoF
,而没有元素的一个将是ZeroF
。像这样:
data ZeroF a = ZeroF
data OneF a = OneF a
data TwoF a = TwoF a a
data ThreeF a = ThreeF a a a
应该做什么Functor
这些实例看起来像什么?嗯,那个是为了OneF
looks exactly就像你的问题一样:
instance Functor OneF where
fmap f (OneF x) = OneF (f x)
其他的看起来pretty类似——只是申请f
更多(或更少)次来解释存在更多(或更少)元素的事实。它们都在这里,有一些创造性的空白来突出相似性/模式:
instance Functor ZeroF where fmap f (ZeroF ) = ZeroF
instance Functor OneF where fmap f (OneF x0 ) = OneF (f x0)
instance Functor TwoF where fmap f (TwoF x0 x1 ) = TwoF (f x0) (f x1)
instance Functor ThreeF where fmap f (ThreeF x0 x1 x2) = ThreeF (f x0) (f x1) (f x2)
希望现在你同意这绝对有味道Functor
您在问题中描述的实例:保持容器的形状相同,并应用给定的函数f
到其中包含的每个元素。
这些是具有给定数量元素的容器。现在,让我们为这些容器编写访问器——即我们想要相当于(!!)
对于列表,如果给定一个数字,我们从容器中取出该字段。由于 a 中有零个元素ZeroF
,我们需要一个零值的索引类型;同时为ThreeF
我们需要一个具有三个值的索引类型。
data Zero
data One = One0
data Two = Two0 | Two1
data Three = Three0 | Three1 | Three2
索引函数的类型如下所示:
indexZero :: ZeroF a -> Zero -> a
indexOne :: OneF a -> One -> a
indexTwo :: TwoF a -> Two -> a
indexThree :: ThreeF a -> Three -> a
我不会全部实现它们——它们非常简单——但这里有一个可以给你一个想法,以防它不是很明显。
indexTwo (TwoF x0 x1) Two0 = x0
indexTwo (TwoF x0 x1) Two1 = x1
事实证明,索引函数有一个逆函数——如果你给我一个函数,当给定索引时,它会产生一个值,那么我可以给你一个包含这些值的容器。类型如下所示:
tabulateZero :: (Zero -> a) -> ZeroF a
tabulateOne :: (One -> a) -> OneF a
tabulateTwo :: (Two -> a) -> TwoF a
tabulateThree :: (Three -> a) -> ThreeF a
(你明白为什么这是逆矩阵的正确类型吗?请注意,比如说,TwoF a -> Two -> a
与以下类型相同TwoF a -> (Two -> a)
!)只是为了让您了解它们是如何实现的(如果不是很明显),我们只需将索引函数应用于每个索引:
tabulateZero ix = ZeroF
tabulateOne ix = OneF (ix One0 )
tabulateTwo ix = TwoF (ix Two0 ) (ix Two1 )
tabulateThree ix = ThreeF (ix Three0) (ix Three1) (ix Three2)
证明这一点并不难tabulateX . indexX = id
and indexX . tabulateX = id
对于每个X
,即制表实际上是索引的逆过程。
好的,但现在请稍等一下,看看我们刚刚做了什么:我们已经变成了function (like Two -> a
) 变成容器 (like TwoF a
),反之亦然。类型Two -> a
and TwoF a
从道德上来说,完全相同的事情。所以认为我们可以实施似乎是合理的fmap
for Two -> a
-- 例如,只需转换为TwoF a
并酌情返回!
twomap :: (a -> b) -> (Two -> a) -> (Two -> b)
twomap f = indexTwo . fmap f . tabulateTwo
让我们想象一下它在做什么。我们将从任意索引函数开始:
\case Two0 -> x0; Two1 -> x1
现在我们来回顾一下整个过程:
\case Two0 -> x0; Two1 -> x1
tabulateTwo
TwoF x0 x1
fmap f
TwoF (f x0) (f x1)
indexTwo
\case Two0 -> f x0; Two1 -> f x1
Since f
应用于两个分支,我们可以将其从case
:
f . (\case Two0 -> x0; Two1 -> x1)
第二项正是我们开始使用的索引函数。换句话说,我们刚刚确定了另一种更简单的实现twomap
:
twomap f ix = f . ix
如果你通过类似的推理来解决zeromap
, onemap
, and threemap
,你会发现它们实际上都有相同的实现!我们可以通过多态来对所有不同大小的容器统一执行此操作;而不是有onemap
为了改变One -> a
等,让我们有一个xmap
为了改变x -> a
's:
xmap :: (a -> b) -> (x -> a) -> (x -> b)
xmap f ix = f . ix
当然,我们不必说出名字f
and ix
:
xmap = (.)
...这就是Functor
实例为(x -> _)
.