我阅读了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 ::回调Int supplyRandom =回调$ \f -> do int <- randomRIO (1,10) f int 从这个实现中可以清楚地看到,supplyRandom
实际上正在生成一个Int
。这类似于Maybe
,这意味着我们有一个坚实的论据来证明它也是协变的。那么,让我们回到正负术语,看看它是否解释了原因。
对于我来说,函数supplyRandom
生成Int,同时,它使用Int f int
。我不明白,为什么作者的意思是,它只产生一个Int
.
一位提交人进一步解释了以下情况:
在
a -> IO ()
中,a
处于负位置。在(a -> IO ()) -> IO ()
中,a -> IO ()
处于负位置。现在我们只需遵循乘法规则:当你把两个负数相乘时,你得到了一个正数。因此,在(a -> IO ())-> IO ()
中,a处于正位置,这意味着回调在a上是协变的,我们可以定义一个函子实例。事实上,GHC同意我们的观点。
我理解这个解释,但我不明白为什么a
是正面的,为什么它是协变的。
考虑函子的定义:
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
如何将(a -> IO ())-> IO ()
中的类型变量(a -> IO ())-> IO ()
转换为(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
的转换是在哪里进行的。
发布于 2018-02-05 02:56:11
对于我来说,函数
supplyRandom
生成Int,同时,它使用Intf int
。我不明白,为什么作者的意思是,它只产生一个Int
.
实际上,在int <- randomRIO (1, 10)
行中,randomRIO
正在生成Int
,而supplyRandom
正在消耗它。类似地,在f int
行中,生成(即提供) Int
的是supplyRandom
,消费Int
的是f
。
当我们说生产和消费时,我们实际上只是指给予和索取。生产并不一定意味着凭空生产,尽管这也是可能的。例如:
produceIntOutOfThinAir :: Callback Int
produceIntOutOfThinAir = Callback $ \f -> f 42 -- produced 42 out of thin air
在作者的例子中,supplyRandom
不会凭空产生Int
。相反,它需要randomRIO
生成的randomRIO
,然后将Int
提供给f
。那完全没问题。
supplyRandom
的类型签名(即打开包装时的(Int -> IO ()) -> IO ()
)只告诉我们supplyRandom
生成一些Int
。它没有指定必须如何生成Int
。
原始答案:
让我们来看看fmap
的Functor Callback
类型
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 ()
。看来我们又回到第一步了,但是等等.有些事不一样。我们的上下文是不同的,因为我们引入了一个新的值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 ()
中是协变的。因此,可以定义一个Functor
实例的Callback
。
发布于 2018-02-05 02:33:52
a -> IO ()
类型的函数是一个需要a
的值:如果没有a
,就不能使用这个值。听起来你已经知道这一点了,但需要重复一遍,以使下一个问题更加清晰。
那么,Callback a
呢?它是一个愿意对a -> IO ()
类型的值进行操作的函数吗?它可以对这样一个值进行操作的唯一方法是传递它一些它可以访问的a
:这正是我们在上一段中建立的。所以,虽然您不知道它是如何产生这个a
的,但是它必须能够以某种方式产生一个,否则它就不能用它的a -> IO ()
做任何事情。
因此,您可以在该fmap
之上创建一个a
,生成一个b
,并生成一个Callback b
,这个值可以与任何b -> IO ()
一起工作。
发布于 2018-02-05 03:18:57
所以我们有这个:
newtype Callback a = Callback
{ runCallback :: (a -> IO ()) -> IO ()
}
让我们暂时去掉新的类型并对函数进行操作。
给定(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 -> ????
很好,现在我们有一个类型为f
的函数a->b
,一个类型为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
,一切都按其应有的方式继续进行。
https://stackoverflow.com/questions/48619201
复制