我阅读了以下文章 https://www.schoolofhaskell.com/user/commercial/content/covariance-contravariance 关于正负位置部分有一个例子:
newtype Callback a = Callback ((a -> IO ()) -> IO ())
a
是协变还是逆变?
是问题。
解释是:
但是现在,我们将整个函数包装为新的输入 函数,通过:
(a -> IO ()) -> IO ()
.作为一个整体,这个功能 消耗Int
,还是产生Int
?为了获得直觉,让我们 查看随机数的Callback Int
实现:supplyRandom :: Callback Int supplyRandom = Callback $ f -> do int <- randomRIO (1, 10) f int
从这个实现中可以清楚地看出,
supplyRandom
实际上是, 产生Int
.这类似于Maybe
,这意味着我们有一个固体 对此的论点也是协变的。因此,让我们回到我们的 正面/负面术语,看看它是否解释了原因。
对我来说,supplyRandom
产生int <- randomRIO (1, 10)
Int 的函数,同时消耗 Intf int
.我不明白,为什么作者的意思是,它只产生了一个Int
.
一位作者继续解释说:
在
a -> IO ()
,a
处于负位置。在(a -> IO ()) -> IO ()
,a -> IO ()
处于负位置。现在我们只遵循乘法规则:当你将两个负数相乘时,你会得到一个正数。作为一个 结果,在(a -> IO ())-> IO ()
中,a 处于正位置,这意味着回调在 a 上是协变的,我们可以定义一个函子实例。事实上,全康同意我们的观点。
我理解这个解释,但我不明白,为什么a
处于积极位置以及为什么它是协变的。
考虑函子定义:
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
如何将(a -> IO ())-> IO ()
中的类型变量a
转换为(b -> IO ())-> IO ()
?我想,我误解了这个概念。
查看函子实现:
newtype Callback a = Callback
{ runCallback :: (a -> IO ()) -> IO ()
}
instance Functor Callback where
fmap f (Callback g) = Callback $ h -> g (h . f)
目前尚不清楚a -> b
的转变发生在何处。
对我来说,函数
supplyRandom
产生int <- randomRIO (1, 10)
一个 Int,同时,它消耗 Intf int
.我不明白,为什么作者的意思是,它只产生一个Int
.
实际上,在生产线int <- randomRIO (1, 10)
中,是randomRIO
在生产Int
,是supplyRandom
在消费它。同样,在生产线f int
中,supplyRandom
在生产(即供应)Int
,f
在消费它。
当我们说生产和消费时,我们实际上只是意味着给予和索取。生产并不一定意味着凭空生产,尽管这也是可能的。例如:
produceIntOutOfThinAir :: Callback Int
produceIntOutOfThinAir = Callback $ f -> f 42 -- produced 42 out of thin air
在作者的例子中,supplyRandom
不会凭空产生Int
。相反,它采用randomRIO
产生的Int
,进而向f
提供Int
。这完全没问题。
supplyRandom
的类型签名(即(Int -> IO ()) -> IO ()
打开包装时)只告诉我们supplyRandom
会产生一些Int
.它没有指定必须如何生成该Int
。
原答案:
让我们看看Functor Callback
的fmap
类型:
fmap :: (a -> b) -> Callback a -> Callback b
让我们将Callback
替换为其解包类型:
Callback a Callback b
__________|__________ _________|_________
| | | |
fmap :: (a -> b) -> ((a -> IO ()) -> IO ()) -> (b -> IO ()) -> IO ()
|______| |_____________________| |__________|
| | |
f g h
如您所见,fmap
需要三个输入,需要生成类型IO ()
的值:
f :: a -> b
g :: (a -> IO ()) -> IO ()
h :: b -> IO ()
--------------------------
IO ()
这是我们目标的直观表示。线以上的一切都是我们的背景(即我们的假设或我们知道的事物)。线以下的一切都是我们的目标(即我们试图用我们的假设证明的事情)。就Haskell代码而言,这可以写成:
fmap f g h = (undefined :: IO ()) -- goal 1
如您所见,我们需要使用输入f
、g
和h
来生成类型IO ()
的值。目前,我正在返回undefined
.您可以将undefined
视为实际值的占位符(即填充空白)。那么,我们如何填补这个空白呢?我们有两个选择。我们可以应用g
或应用h
因为它们都返回一个IO ()
.假设我们决定应用h
:
fmap f g h = h (undefined :: b) -- goal 2
如您所见,h
需要应用于类型b
的值。因此,我们的新目标是b
.我们如何填写新空白?在我们的上下文中,唯一生成类型b
值的函数是f
:
fmap f g h = h (f (undefined :: a)) -- goal 3
但是,我们现在必须生成一个类型a
的值,我们既没有类型a
的值,也没有任何生成类型a
值的函数。因此,应用h
不是一种选择。回到目标1。我们的另一个选择是应用g
。所以,让我们试试:
fmap f g h = g (undefined :: a -> IO ()) -- goal 4
我们的新目标是a -> IO ()
。a -> IO ()
类型的值是什么样的?由于它是一个函数,我们知道它看起来像一个lambda:
fmap f g h = g (x -> (undefined :: IO ())) -- goal 5
我们的新目标再次IO ()
。似乎我们又回到了原点 1,但等等...有些不同。我们的上下文是不同的,因为我们引入了一个新的值x :: a
:
f :: a -> b
g :: (a -> IO ()) -> IO ()
h :: b -> IO ()
x :: a
--------------------------
IO ()
这个价值x
从何而来?好像我们只是凭空拉出来的吧?不,我们不是凭空拉出来的。x
的价值来自g
。你看,类型a
在g
中是协变的,这意味着g
产生a
。事实上,当我们创建 lambda 来填补目标 4 的空白时,我们在上下文中引入了一个新的变量x
,它从g
中获取其值,无论它是什么。
无论如何,我们再次需要生成一个类型IO ()
的值,但现在我们可以回到选项 1(即应用h
),因为我们终于有了类型a
的值。我们不想回到选项 2(即应用g
),因为那样我们只会绕圈子。选项 1 是我们的出路:
fmap f g h = g (x -> h (undefined :: b)) -- goal 6
fmap f g h = g (x -> h (f (undefined :: a))) -- goal 7
fmap f g h = g (x -> h (f x)) -- goal proved
如您所见,x -> h (f x)
只是h . f
(即功能组合),其余的就是newtype
的打包和解包。因此,实际函数定义为:
fmap f (Callback g) = Callback $ h -> g (h . f)
希望这能解释为什么a
在(a -> IO ()) -> IO ()
中是协变的。因此,可以定义Callback
的Functor
实例。
类型a -> IO ()
的函数是一个需要a
的值:如果没有某处的a
,你就不能使用此值。听起来你已经知道这一点了,但值得重复一遍,以使下一点更清楚。
现在,一个Callback a
,一个愿意对类型a -> IO ()
的值进行操作的函数呢?它可以对这样的值进行操作的唯一方法是向它传递一些它有权访问的a
:这正是我们在上一段中建立的。所以虽然你不知道它是如何产生这个a
,但它必须能够以某种方式产生一个,否则它无法用它的a -> IO ()
做任何事情。
因此,你可以fmap
该a
,产生一个b
,并产生一个总体Callback b
,一个可以处理任何b -> IO ()
的值。
所以我们有这个:
newtype Callback a = Callback
{ runCallback :: (a -> IO ()) -> IO ()
}
让我们暂时剥离 newtype 并对函数进行操作。
给定一个(a -> IO ()) -> IO ()
类型的函数和一个a->b
类型的函数,我们需要生成一个((b -> IO ()) -> IO ())
类型的函数。我们怎么能做到这一点?让我们试试:
transformCallback :: (a->b) -> ((a -> IO ()) -> IO ()) -> ((b -> IO ()) -> IO ())
transformCallback f g = ????
所以得到的回调,我们用????,表示的表达式应该接受类型b -> IO ()
的函数,并返回一个IO ()
。
transformCallback f g = h -> ????
很好,现在我们有一个类型a->b
的函数f
,一个类型b->IO ()
的函数h
,以及((a->IO()) -> IO())
类型的原始回调g
。我们能用这些做什么?唯一可能的行动方案似乎是将f
和h
结合起来,以获得a->IO()
类型的东西。
transformCallback f g = h -> ??? h . f ???
太好了,我们有a->IO()
类型的东西,并且g
接受该类型并返回IO ()
,正是我们应该返回的内容。
transformCallback f g = h -> g ( h . f )
那么f
在哪里被召唤呢?我们给它喂什么?
回想一下,原始回调的类型为(a -> IO ()) -> IO ()
。我们可以问,这个(a -> IO ())
函数在哪里调用?喂它吃的是什么?
首先,它不必被调用。回调很可能会忽略它并独立生成IO()
。但是,如果调用它,回调会调用它,并且它会得到一个a
,以便从某个地方馈送到该a->IO()
。重复这一点很重要:回调生成一个a
并将其提供给其参数。
现在,如果我们向原始回调提供一个函数,该函数将a
转换为b
,然后将结果提供给b->IO
类型的函数,回调就像使用它一样a->IO
类型的任何其他函数一样。现在和以前一样,回调产生一个a
并将其提供给它的参数,参数将其转换为b
,然后产生一个IO
,一切都按应有的方式进行。