有人声称newtype T a = T (a -> Int)
是一个不是函子的类型构造函数(但是一个逆变函子)。怎么会这样?或者什么是逆变函子(因此我认为为什么不能将其设为正常函子是显而易见的)?
给定
newtype T a = T (a -> Int)
让我们尝试为此数据类型构建Contravariant
实例。
这是有问题的类型类:
class Contravariant f where
contramap :: (a -> b) -> f b -> f a
基本上,contramap
类似于fmap
,但不是将函数提升a -> b
f a -> f b
,而是将其提升到f b -> f a
。
让我们开始编写实例...
instance Contravariant T where
contramap g (T f) = ?
在我们填写?
之前,让我们考虑一下g
和f
的类型是什么:
g :: a -> b
f :: b -> Int
为了清楚起见,我们不妨提到
f a ~ T (a -> Int)
f b ~ T (b -> Int)
因此,我们可以按如下方式填写?
:
instance Contravariant T where
contramap g (T f) = T (f . g)
为了超级迂腐,您可以将g
重命名为aToB
,f
重命名为bToInt
。
instance Contravariant T where
contramap aToB (T bToInt) = T (bToInt . aToB)
您可以为T a
编写Contravariant
实例的原因归结为这样一个事实,即a
在T (a -> Int)
中处于逆变位置。说服自己T a
不是Functor
的最好方法是尝试(并失败)自己编写Functor
实例。
假设 T 是一个函子。然后我们有
fmap :: (a -> b) -> T a -> T b
现在,让我们尝试实现它。
instance Functor T where
fmap f (T g) = T $ x -> _y
显然,我们从这样的东西开始,并以某种方式组合f
、g
和x
的值,以计算正确类型的y
值。我们该怎么做呢?好吧,请注意,我称之为_y
,它告诉GHC我需要一些帮助来弄清楚放什么。全康有什么要说的?
<interactive>:7:28: error:
• Found hole: _y :: Int
Or perhaps ‘_y’ is mis-spelled, or not in scope
• In the expression: _y
In the second argument of ‘($)’, namely ‘ x -> _y’
In the expression: T $ x -> _y
• Relevant bindings include
x :: b (bound at <interactive>:7:23)
g :: a -> Int (bound at <interactive>:7:13)
f :: a -> b (bound at <interactive>:7:8)
fmap :: (a -> b) -> T a -> T b (bound at <interactive>:7:3)
所以现在我们清楚了所有相关事物的类型,对吧?我们需要以某种方式返回一个Int
,我们必须构建它是:
x :: b
g :: a -> Int
f :: a -> b
好吧,好吧,我们唯一可能创建Int
的东西是g
,所以让我们填写一下,将g
的论点留空以寻求GHC的更多帮助:
instance Functor T where
fmap f (T g) = T $ x -> g _y
<interactive>:7:31: error:
• Found hole: _y :: a
Where: ‘a’ is a rigid type variable bound by
the type signature for:
fmap :: forall a b. (a -> b) -> T a -> T b
at <interactive>:7:3
Or perhaps ‘_y’ is mis-spelled, or not in scope
• In the first argument of ‘g’, namely ‘(_y)’
In the expression: g (_y)
In the second argument of ‘($)’, namely ‘ x -> g (_y)’
• Relevant bindings include
x :: b (bound at <interactive>:7:23)
g :: a -> Int (bound at <interactive>:7:13)
f :: a -> b (bound at <interactive>:7:8)
fmap :: (a -> b) -> T a -> T b (bound at <interactive>:7:3)
好吧,我们可以自己预测:要调用g
,我们需要一个来自某处的a
类型的值。但是我们在作用域中没有任何a
类型的值,也没有任何返回a
类型值的函数!我们陷入了困境:不可能产生我们现在想要的值,尽管在每一步我们都做了唯一可能的事情:没有什么可以备份和尝试不同的。
为什么会这样?因为如果我给你一个类型a -> Int
的函数并说"但顺便说一下,这里有一个来自a -> b
的函数,请给我一个来自b -> Int
的函数",你实际上不能从a -> b
使用该函数,因为没有人给你任何a
来调用它!如果我给你一个b -> a
的功能,那会很有帮助,对吧?然后,你可以从b -> Int
生成一个函数,首先调用b -> a
函数来获取a
,然后调用原始a -> Int
函数来获取所需的Int
。
这就是逆变函子的意义所在:我们反转传递给fmap
的函数中的箭头,因此它可以映射你"需要"的东西(函数参数)而不是你"拥有"的东西(具体值,函数的返回值等)。
旁白:我之前声称我们在每一步都做了"唯一可能的事情",这有点虚构。我们不能用f
、g
和x
来构建一个Int
,但我们当然可以凭空编造各种数字。我们对类型b
一无所知,所以我们无法以某种方式获得从它派生的Int
,但我们可以说"让我们总是返回零",这在技术上满足类型检查器:
instance Functor T where
fmap f (T g) = T $ const 0
显然,这看起来很不对劲,因为它似乎f
和g
应该非常重要,而我们忽略了它们!但它是类型检查,所以我们没事,对吧?
不,这违反了函子定律之一:
fmap id = id
我们可以很容易地证明这一点:
fmap id (T $ const 5) = (T $ const 0) /= (T $ const 5)
现在我们真的尝试了一切:我们必须在不使用我们的b
类型的情况下构建Int
的唯一方法是无中生有,所有这些用途都将与使用const
同构,这将违反函子定律。
这是另一个观点。正如利米纳利什特所表明的那样,T
Contravariant
。关于协变和逆变的类型,我们能说些什么?
import Data.Void
change1, change1', change2 :: (Functor f, Contravariant f) => f a -> f b
change1 = contramap (const ()) . fmap (const ())
change1' = (() >$) . (() <$)
change2 = fmap absurd . contramap absurd
前两个实现基本上是相同的(change1'
是change1
的优化);它们中的每一个都使用()
是Hask的"终端对象"的事实。 相反,change2
使用了Void
是"初始对象"的事实。
这些函数中的每一个都用b
替换了f a
中的所有a
,而对a
、b
或它们之间的关系一无所知,而其他一切都保持不变。可能应该清楚的是,这意味着f a
并不真正依赖于a
。也就是说,f
的参数必须是幻像。T
不是这样,所以它不能也是协变的。