为什么函数实现可能?

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随机数的实现:

supplyRandom :: Callback Int
supplyRandom = Callback $ \f -> do
    int <- randomRIO (1, 10)
    f int
Run Code Online (Sandbox Code Playgroud)

从这个实现中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发生在何处.

Aad*_*hah 8

对我来说,函数supplyRandom产生int <- randomRIO (1, 10)一个Int,同时它消耗了Int f int.我看不出,为什么作者的意思是,它只产生一个Int.

实际上,在线上int <- randomRIO (1, 10)它正在randomRIO产生IntsupplyRandom并且正在消耗它.类似地,在一行f intsupplyRandom即产生真实(即供给)的Int和它的f那真实消费它.

当我们说生产和消费时,我们只是意味着给予和服用.生产并不一定意味着凭空产生,尽管这也是可能的.例如:

produceIntOutOfThinAir :: Callback Int
produceIntOutOfThinAir = Callback $ \f -> f 42 -- produced 42 out of thin air
Run Code Online (Sandbox Code Playgroud)

在作者的例子中,supplyRandom不会Int凭空产生.相反,它需要的IntrandomRIO生产,进而用品是Intf.那很好.

supplyRandom(即(Int -> IO ()) -> IO ()解包时)的类型签名只告诉我们supplyRandom产生一些Int.它没有具体说明Int必须如何产生.


原始答案:

我们来看看fmapfor 的类型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,gh生成类型的值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.我们如何填写新的空白?在我们的上下文中产生类型值的唯一函数bf:

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.