为什么可以实现仿函数实现?

时间:2018-02-05 09:19:55

标签: haskell covariance

我在正面和负面位置部分阅读了以下文章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,同时它消耗了Int f int我看不出,为什么作者的意思是,它只产生Int

作者继续进一步解释了以下内容:

  

a -> IO ()中,a处于负面位置。在(a -> IO ()) -> IO ()中,a -> IO ()处于负面位置。现在我们只遵循乘法规则:当你乘以两个负数时,你得到一个正数。作为一个   结果,在(a -> IO ())-> IO ()中,a处于正位置,这意味着Callback在a上是协变的,我们可以定义一个Functor实例。事实上,GHC同意我们的观点。

我理解这个解释,但我没有理解,为什么a处于积极的位置以及为什么它是协变的。

考虑仿函数定义:

class Functor (f :: * -> *) where
  fmap :: (a -> b) -> f a -> f b

如何将a中的类型变量(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的转变发生在哪里。

3 个答案:

答案 0 :(得分:8)

  

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

实际上,int <- randomRIO (1, 10)行生成了randomRIO Int及其supplyRandom f intsupplyRandom。消耗它。同样,在Int行中f生成(即提供)produceIntOutOfThinAir :: Callback Int produceIntOutOfThinAir = Callback $ \f -> f 42 -- produced 42 out of thin air 及其supplyRandom&#39}消耗它。

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

Int

在作者的例子中,Int并没有凭空产生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 () 需要三个输入,需要生成fmap f g h = (undefined :: IO ()) -- goal 1 类型的值:

f

这是我们目标的直观表示。线上的一切都是我们的背景(即我们的假设或我们所知道的事物)。线下的一切都是我们的目标(即我们试图用我们的假设证明的事情)。就Haskell代码而言,这可以写成:

g

如您所见,我们需要使用输入hIO ()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的值,也没有任何生成类型g的值的函数。因此,应用fmap f g h = g (undefined :: a -> IO ()) -- goal 4 不是一种选择。回到目标1.我们的另一个选择是申请a -> IO ()。所以,让我们试试吧:

a -> IO ()

我们的新目标是fmap f g h = g (\x -> (undefined :: IO ())) -- goal 5 。类型IO ()的值是什么样的?由于它是一个函数,我们知道它看起来像一个lambda:

x :: a

我们的新目标又是f :: a -> b g :: (a -> IO ()) -> IO () h :: b -> IO () x :: a -------------------------- IO () 。好像我们回到了方阵1,但等等......有些不同。我们的上下文不同,因为我们引入了一个新值x

x

此值g来自何处?好像我们只是凭空掏出来的吧?不,我们没有凭空掏空。值a来自g。您看,g中的a类型是协变的,这意味着x生成g。实际上,当我们创建lambda以填充目标4的空白时,我们在我们的上下文中引入了一个新变量IO (),它从h获取其值,无论它是什么。

无论如何,我们再次需要生成a类型的值,但现在我们可以返回选项1(即应用g),因为我们最终得到类型为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 的值。我们不想回到选项2(即申请\x -> h (f x)),因为我们只是在圈子里跑。选项1是我们的出路:

h . f

如您所见,newtype只是fmap f (Callback g) = Callback $ \h -> g (h . f) (即函数组合),其余的是a的打包和解包。因此,实际函数定义为:

(a -> IO ()) -> IO ()

希望能解释为什么FunctorCallback中是协变的。因此,可以定义{{1}}的{​​{1}}实例。

答案 1 :(得分:2)

类型a -> IO ()的函数是需要a的值:如果没有a某个地方,则无法使用此值。听起来你已经知道了这一点,但重复的是要让下一点更清楚。

现在,Callback a,一个愿意对类型为a -> IO ()的值进行操作的函数呢?它可以对这样一个值进行操作的唯一方法是向它传递一些它有权访问的a:这正是我们在前一段中建立的。因此,虽然你不知道如何它会产生这个a,但它必须能够以某种方式产生一个,否则它无法对其a -> IO ()做任何事情。

因此,您可以fmap覆盖a,生成b,并产生Callback b,这个值可以与任何b -> IO ()一起使用

答案 2 :(得分:2)

所以我们有这个:

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,一个类型为h的函数b->IO (),以及类型为{的原始回调g {1}}。我们可以用这些做什么?唯一可行的做法似乎是将((a->IO()) -> IO())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()的{​​{1}}。重复这一点很重要:回调产生a并将其提供给其参数

现在,如果我们向原始回调提供一个将a->IO()转换为a,然后将结果提供给类型a的函数的函数,则回调同样乐于消费它与b类型的任何其他函数一样。现在和以前一样,回调生成一个b->IO并将其提供给它的参数,并且参数将其转换为a->IO,然后生成a,并且一切都在继续。