zer*_*ing 7 haskell covariance
我看了下面的文章https://www.schoolofhaskell.com/user/commercial/content/covariance-contravariance的部分正面和负面的位置有一个例子:
newtype Callback a = Callback ((a -> IO ()) -> IO ())
Run Code Online (Sandbox Code Playgroud)
是协变还是逆变
a
?
是问题.
解释是:
但是现在,我们通过以下方式将整个函数作为新函数的输入包装起来:
(a -> IO ()) -> IO ()
.总的来说,这个功能是消耗Int
还是产生了Int
?为了获得直觉,让我们看一下Callback Int
随机数的实现:Run Code Online (Sandbox Code Playgroud)supplyRandom :: Callback Int supplyRandom = Callback $ \f -> do int <- randomRIO (1, 10) f int
从这个实现中
supplyRandom
可以清楚地看出,事实上,生成一个Int
.这类似于Maybe
,这意味着我们有一个坚实的论据,这也是协变.那么让我们回到我们的积极/消极术语,看看它是否解释了原因.
对我来说,函数supplyRandom
产生int <- randomRIO (1, 10)
一个Int,同时它消耗了Int f int
.我看不出,为什么作者的意思是,它只产生一个Int
.
一位作者继续进一步解释如下:
在
a -> IO ()
,a
处于负面地位.在(a -> IO ()) -> IO ()
,a -> IO ()
处于负面地位.现在我们只遵循乘法规则:当你乘以两个负数时,你得到一个正数.因此,在(a -> IO ())-> IO ()
a 中,a处于正位置,这意味着Callback在a上是协变的,我们可以定义一个Functor实例.事实上,GHC同意我们的观点.
我理解这个解释,但我没有理解,为什么a
它处于积极的位置,为什么它是协变的.
考虑仿函数定义:
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)
它是如何可能转化变量类型a
中(a -> IO ())-> IO ()
,以(b -> IO ())-> IO ()
?我想,我误解了这个概念.
看看functor的实现:
newtype Callback a = Callback
{ runCallback :: (a -> IO ()) -> IO ()
}
instance Functor Callback where
fmap f (Callback g) = Callback $ \h -> g (h . f)
Run Code Online (Sandbox Code Playgroud)
目前尚不清楚转型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
Run Code Online (Sandbox Code Playgroud)
在作者的例子中,supplyRandom
不会Int
凭空产生.相反,它需要的Int
是randomRIO
生产,进而用品是Int
到f
.那很好.
supplyRandom
(即(Int -> IO ()) -> IO ()
解包时)的类型签名只告诉我们supplyRandom
产生一些Int
.它没有具体说明Int
必须如何产生.
原始答案:
我们来看看fmap
for 的类型Functor Callback
:
fmap :: (a -> b) -> Callback a -> Callback b
Run Code Online (Sandbox Code Playgroud)
让我们Callback
用它的unwrapped类型替换:
Callback a Callback b
__________|__________ _________|_________
| | | |
fmap :: (a -> b) -> ((a -> IO ()) -> IO ()) -> (b -> IO ()) -> IO ()
|______| |_____________________| |__________|
| | |
f g h
Run Code Online (Sandbox Code Playgroud)
如您所见,fmap
需要三个输入并需要生成类型的值IO ()
:
f :: a -> b
g :: (a -> IO ()) -> IO ()
h :: b -> IO ()
--------------------------
IO ()
Run Code Online (Sandbox Code Playgroud)
这是我们目标的直观表示.线上的一切都是我们的背景(即我们的假设或我们所知道的事物).线下的一切都是我们的目标(即我们试图用我们的假设证明的事情).就Haskell代码而言,这可以写成:
fmap f g h = (undefined :: IO ()) -- goal 1
Run Code Online (Sandbox Code Playgroud)
如您所见,我们需要使用输入f
,g
并h
生成类型的值IO ()
.目前,我正在回来undefined
.您可以将其undefined
视为实际值的占位符(即填充空白).那么,我们如何填补这个空白?我们有两种选择.我们可以申请g
或申请,h
因为他们都返回IO ()
.假设我们决定申请h
:
fmap f g h = h (undefined :: b) -- goal 2
Run Code Online (Sandbox Code Playgroud)
如您所见,h
需要应用于类型的值b
.因此,我们的新目标是b
.我们如何填写新的空白?在我们的上下文中产生类型值的唯一函数b
是f
:
fmap f g h = h (f (undefined :: a)) -- goal 3
Run Code Online (Sandbox Code Playgroud)
但是,我们现在必须生成一个类型的值,a
我们既没有类型的值a
也没有任何产生类型值的函数a
.因此,申请h
不是一种选择.回到目标1.我们的另一个选择是申请g
.那么,让我们试试吧:
fmap f g h = g (undefined :: a -> IO ()) -- goal 4
Run Code Online (Sandbox Code Playgroud)
我们的新目标是a -> IO ()
.类型的值是a -> IO ()
什么样的?因为它是一个函数,我们知道它看起来像一个lambda:
fmap f g h = g (\x -> (undefined :: IO ())) -- goal 5
Run Code Online (Sandbox Code Playgroud)
我们的新目标又来了IO ()
.好像我们回到了第1方,但等等......有些不同.我们的背景不同,因为我们引入了一个新值x :: a
:
f :: a -> b
g :: (a -> IO ()) -> IO ()
h :: b -> IO ()
x :: a
--------------------------
IO ()
Run Code Online (Sandbox Code Playgroud)
这个价值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
Run Code Online (Sandbox Code Playgroud)
正如你所看到的,\x -> h (f x)
只是h . f
(即功能组合),其余的是包装和拆包newtype
.因此,实际功能定义为:
fmap f (Callback g) = Callback $ \h -> g (h . f)
Run Code Online (Sandbox Code Playgroud)
希望能解释为什么a
是协变的(a -> IO ()) -> IO ()
.因此,可以定义一个Functor
实例Callback
.
归档时间: |
|
查看次数: |
177 次 |
最近记录: |