从程序员的角度来看,函子的本质是能够轻松地适应事物。我这里所说的“适配”的意思是,如果我有一个f a 并且我需要一个f b,我想要一个适合我的f a 在我的f b 形孔中的适配器。
似乎很直观,如果我可以将a 转换为b,我可能可以将f a 转换为f b。事实上,这就是 Haskell 的 Functor 类所体现的模式;如果我提供了一个a -> b 函数,那么fmap 让我可以将f a 的东西适应f b 的东西,而不用担心f 涉及的任何内容。1
当然在这里谈论参数化类型,如 list-of-x [x]、Maybe y 或 IO z,我们可以使用适配器更改的东西是 x、y 或z 在那些。如果我们想要灵活地从任何可能的函数a -> b 获取适配器,那么我们正在调整的东西当然必须同样适用于任何可能的类型。
(起初)不太直观的是,有些类型几乎可以与 functory 完全相同的方式进行调整,只是它们是“倒退的”;对于这些,如果我们想修改 f a 来满足 f b 的需求,我们实际上需要提供 b -> a 函数,而不是 a -> b 一个!
我最喜欢的具体例子其实是函数类型a -> r(a 表示参数,r 表示结果);所有这些抽象的废话在应用于函数时都非常有意义(如果你做过任何实质性的编程,你几乎可以肯定在不知道术语或它们的广泛适用性的情况下使用了这些概念),这两个概念是如此明显在这种情况下相互对偶。
众所周知a -> r 是r 中的函子。这是有道理的;如果我有一个a -> r 并且我需要一个a -> s,那么我可以使用r -> s 函数来调整我的原始函数,只需对结果进行后处理。2
另一方面,如果我有一个a -> r 函数并且我需要一个b -> r,那么很明显我可以通过在将参数传递给原始函数之前对其进行预处理来满足我的需求。但是我用什么来预处理它们呢?原来的功能是一个黑盒子;无论我做什么,它总是期待a 输入。所以我需要将我的b 值转换为它所期望的a 值:我的预处理适配器需要一个b -> a 函数。
我们刚刚看到函数类型a -> r 是r 中的协变 函子,而a 中是逆变 函子。我认为这是说我们可以调整函数的结果,结果类型“随”适配器 r -> s 而变化,而当我们调整函数的参数时,参数类型会“以相反的方向”更改为适配器。
有趣的是,函数结果fmap 和函数参数contramap 的实现几乎完全相同:只是函数组合(. 运算符)!唯一的区别是您在哪一侧编写适配器函数:3
fmap :: (r -> s) -> (a -> r) -> (a -> s)
fmap adaptor f = adaptor . f
fmap adaptor = (adaptor .)
fmap = (.)
contramap' :: (b -> a) -> (a -> r) -> (b -> r)
contramap' adaptor f = f . adaptor
contramap' adaptor = (. adaptor)
contramap' = flip (.)
我认为每个块的第二个定义最有见地; (协变)对函数结果的映射是左侧的组合(如果我们想采用“这发生在之后”的观点,则为后组合),而对函数参数的逆变映射是右侧的组合(前-作文)。
这种直觉概括得很好;如果f x 结构可以给我们 类型为x 的值(就像a -> r 函数给我们r 值一样,至少可能),它可能是一个协变的Functor在x 中,我们可以使用x -> y 函数将其调整为f y。但是,如果一个f x 结构接收来自我们的x 类型的值(同样,就像a -> r 函数的a 类型的参数),那么它可能是一个Contravariant 仿函数和我们需要使用y -> x 函数将其调整为f y。
当您从源/目标的实施者而不是调用者的角度思考时,我发现反映这种“源是协变的,目标是逆变的”直觉很有趣。如果我尝试实现一个接收x 值的f x,我可以“调整我自己的界面”,这样我就可以改用y 值(同时仍然显示“接收通过使用x -> y 函数,x 值“与我的调用者的接口)。通常我们不会这样想。即使作为f x 的实现者,我也会考虑调整我正在调用的东西,而不是“让我的调用者的界面适应我”。但这是您可以采取的另一种观点。
我对@987654390@ 所做的唯一半真实世界使用(与通过使用右侧组合在其参数中隐式使用函数的逆变性相反,这很常见)是用于键入 Serialiser a 可以序列化 x 值。 Serialiser 必须是 Contravariant 而不是 Functor;鉴于我可以序列化 Foos,如果可以,我也可以序列化 Bars Bar -> Foo。4 但是当你意识到 Serialiser a 基本上是 a -> ByteString 时,它变得很明显;我只是在重复 a -> r 示例的一个特例。
在纯函数式编程中,“接收值”的东西没有它也回馈一些东西并没有太大用处,因此所有逆变函子往往看起来像函数,但几乎任何可以包含值的简单数据结构任意类型将是该类型参数中的协变函子。这就是为什么Functor 早早地偷走了这个好名字并被到处使用(嗯,那个和那个Functor 被认为是Monad 的基本部分,在定义Functor 之前它已经被广泛使用作为 Haskell 中的一个类)。
在命令式 OO 中,我相信逆变函子可能更常见(但不是用像 Contravariant 这样的统一框架抽象出来的),尽管它也很容易具有可变性,并且副作用意味着参数化类型不能完全成为函子(通常:您的标准容器a 既可读又可写,既是a 的发射器又是接收器,而不是意味着它既是协变的又是逆变的,事实证明这两者都不是) .
1 每个个体f 的Functor 实例说明了如何将任意函数应用于f 的特定形式,而不用担心正在应用f 的特定类型到;很好的关注点分离。
2 这个函子也是一个monad,相当于Reader monad。我不打算在这里详细讨论函子,但鉴于我的其余帖子,一个明显的问题是“a -> r 类型也是a 中的某种逆变单子吗?”。不幸的是,逆变不适用于单子(请参阅Are there contravariant monads?),但有一个类似于Applicative 的逆变:https://hackage.haskell.org/package/contravariant-1.4/docs/Data-Functor-Contravariant-Divisible.html
3 请注意,这里的contramap' 与Haskell 中实现的Contravariant 中的实际contramap 不匹配;您不能仅仅因为a 不是(->) 的最后一个类型参数,就无法在Haskell 代码中使a -> r 成为Contravariant 的实际实例。 从概念上来说它工作得很好,而且您总是可以使用新类型包装器来交换类型参数并使其成为一个实例(逆变器正是为此目的定义了Op 类型)。
4 至少对于“序列化”的定义,它不一定包括以后能够重建 Bar,因为它将序列化 a Bar 与它映射到的 Foo 相同,没有包含有关映射是什么的任何信息的方法。