为什么仿函数实现是可能的?
Why is the functor implementation possible?
我在正面和负面位置部分阅读了以下文章https://www.schoolofhaskell.com/user/commercial/content/covariance-contravariance,有一个例子:
newtype Callback a = Callback ((a -> IO ()) -> IO ())
Is it covariant or contravariant on a
?
是问题。
解释是:
But now, we wrap up this entire function as the input to a new
function, via: (a -> IO ()) -> IO ()
. As a whole, does this function
consume an Int
, or does it produce an Int
? To get an intuition, let's
look at an implementation of Callback Int
for random numbers:
supplyRandom :: Callback Int
supplyRandom = Callback $ \f -> do
int <- randomRIO (1, 10)
f int
It's clear from this implementation that supplyRandom
is, in fact,
producing an Int
. This is similar to Maybe
, meaning we have a solid
argument for this also being covariant. So let's go back to our
positive/negative terminology and see if it explains why.
对我来说,函数 supplyRandom
产生 int <- randomRIO (1, 10)
一个 Int,同时它消耗 Int f int
。 我看不出来,为什么作者的意思,它只产生一个Int
。
作者进一步解释如下:
In a -> IO ()
, a
is in negative position. In (a -> IO ()) -> IO ()
, a -> IO ()
is in negative position. Now we just follow multiplication rules: when you multiply two negatives, you get a positive. As a
result, in (a -> IO ())-> IO ()
, a is in the positive position, meaning that Callback is covariant on a, and we can define a Functor instance. And in fact, GHC agrees with us.
我明白了解释,但我不明白,为什么 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
的转换发生在何处。
a -> IO ()
类型的函数是一个需要 a
的值:如果某处没有 a
,您将无法使用此值。听起来你已经知道了。但值得重复以使下一点更清楚。
现在,Callback a
,一个愿意对类型 a -> IO ()
的值进行操作的函数呢?它可以对这样的值进行操作的唯一方法是将它可以访问的 a
传递给它:这正是我们在上一段中建立的。因此,虽然您不知道 如何 它产生这个 a
,但它必须能够以某种方式产生一个,否则它无法用它的 a -> IO ()
做任何事情。
因此,您可以 fmap
覆盖 a
,生成 b
,并生成总体 Callback b
,一个可以与任何 [=22] 一起使用的值=].
For me the function supplyRandom
produces int <- randomRIO (1, 10)
an Int and at the same time, it consumes the Int f int
. I can not see, why the author mean, it only produces an 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
,然后将 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 ()
的值。目前,我 returning undefined
。您可以将 undefined
视为实际值的占位符(即填空)。那么,我们如何填补这个空白呢?我们有两个选择。我们可以应用 g
或应用 h
,因为它们都是 return 和 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
),因为我们最终得到了一个 [=65] 类型的值=].我们不想回到选项 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
实例
所以我们有这个:
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 ()
的函数,并且 return 和 IO ()
.
transformCallback f g = \h -> ????
很好,现在我们有一个 a->b
类型的函数 f
,一个 b->IO ()
类型的函数 h
,以及 g
的原始回调输入 ((a->IO()) -> IO())
。我们可以用这些做什么?唯一可能的做法似乎是将 f
和 h
组合起来以获得 a->IO()
.
类型的东西
transformCallback f g = \h -> ??? h . f ???
太好了,我们有 a->IO()
类型的东西,g
接受该类型和 returns IO ()
,正是我们应该 return .
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://www.schoolofhaskell.com/user/commercial/content/covariance-contravariance,有一个例子:
newtype Callback a = Callback ((a -> IO ()) -> IO ())
Is it covariant or contravariant on
a
?
是问题。
解释是:
But now, we wrap up this entire function as the input to a new function, via:
(a -> IO ()) -> IO ()
. As a whole, does this function consume anInt
, or does it produce anInt
? To get an intuition, let's look at an implementation ofCallback Int
for random numbers:supplyRandom :: Callback Int supplyRandom = Callback $ \f -> do int <- randomRIO (1, 10) f int
It's clear from this implementation that
supplyRandom
is, in fact, producing anInt
. This is similar toMaybe
, meaning we have a solid argument for this also being covariant. So let's go back to our positive/negative terminology and see if it explains why.
对我来说,函数 supplyRandom
产生 int <- randomRIO (1, 10)
一个 Int,同时它消耗 Int f int
。 我看不出来,为什么作者的意思,它只产生一个Int
。
作者进一步解释如下:
In
a -> IO ()
,a
is in negative position. In(a -> IO ()) -> IO ()
,a -> IO ()
is in negative position. Now we just follow multiplication rules: when you multiply two negatives, you get a positive. As a result, in(a -> IO ())-> IO ()
, a is in the positive position, meaning that Callback is covariant on a, and we can define a Functor instance. And in fact, GHC agrees with us.
我明白了解释,但我不明白,为什么 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
的转换发生在何处。
a -> IO ()
类型的函数是一个需要 a
的值:如果某处没有 a
,您将无法使用此值。听起来你已经知道了。但值得重复以使下一点更清楚。
现在,Callback a
,一个愿意对类型 a -> IO ()
的值进行操作的函数呢?它可以对这样的值进行操作的唯一方法是将它可以访问的 a
传递给它:这正是我们在上一段中建立的。因此,虽然您不知道 如何 它产生这个 a
,但它必须能够以某种方式产生一个,否则它无法用它的 a -> IO ()
做任何事情。
因此,您可以 fmap
覆盖 a
,生成 b
,并生成总体 Callback b
,一个可以与任何 [=22] 一起使用的值=].
For me the function
supplyRandom
producesint <- randomRIO (1, 10)
an Int and at the same time, it consumes the Intf int
. I can not see, why the author mean, it only produces anInt
.
实际上,在 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
,然后将 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 ()
的值。目前,我 returning undefined
。您可以将 undefined
视为实际值的占位符(即填空)。那么,我们如何填补这个空白呢?我们有两个选择。我们可以应用 g
或应用 h
,因为它们都是 return 和 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
),因为我们最终得到了一个 [=65] 类型的值=].我们不想回到选项 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
实例
所以我们有这个:
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 ()
的函数,并且 return 和 IO ()
.
transformCallback f g = \h -> ????
很好,现在我们有一个 a->b
类型的函数 f
,一个 b->IO ()
类型的函数 h
,以及 g
的原始回调输入 ((a->IO()) -> IO())
。我们可以用这些做什么?唯一可能的做法似乎是将 f
和 h
组合起来以获得 a->IO()
.
transformCallback f g = \h -> ??? h . f ???
太好了,我们有 a->IO()
类型的东西,g
接受该类型和 returns IO ()
,正是我们应该 return .
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
],一切照常进行。