代码之家  ›  专栏  ›  技术社区  ›  softshipper

为什么函子实现是可能的?

  •  7
  • softshipper  · 技术社区  · 6 年前

    我读了下面的文章 https://www.schoolofhaskell.com/user/commercial/content/covariance-contravariance 在剖面上 正负位置 有一个例子:

    newtype Callback a = Callback ((a -> IO ()) -> IO ())
    

    它是协变的还是逆变的 a ?

    这就是问题所在。
    解释如下:

    但现在,我们将整个函数包装为新 功能,通过: (a -> IO ()) -> IO () . 作为一个整体,这个功能 使用 Int ,还是会产生 内景 ? 要获得直觉,让我们 查看的实现 Callback Int 对于随机数:

    supplyRandom :: Callback Int
    supplyRandom = Callback $ \f -> do
        int <- randomRIO (1, 10)
        f int
    

    从这个实现中可以清楚地看到 supplyRandom 事实上, 生成 内景 . 这类似于 Maybe ,这意味着我们有一个坚实的 这个论点也是协变的。那么让我们回到我们的 积极/消极的术语,看看它是否解释了原因。

    对我来说,功能 供应随机 生产 int <- randomRIO (1, 10) 一个Int,同时它消耗Int f int . 我不明白,为什么作者的意思是,它只产生一个 内景 .

    一位作者进一步解释如下:

    在里面 a -> IO () , 处于负位置。在里面 (a->IO())->IO() , a->IO() 处于负位置。现在我们只需遵循乘法规则:当你将两个负数相乘时,得到一个正数。作为一个 导致 (a -> IO ())-> IO () ,a位于正位置,这意味着回调在a上是协变的,我们可以定义一个函子实例。事实上,GHC同意我们的观点。

    我理解解释,但我不明白为什么 处于正位置,以及为什么它是协变的。

    考虑函子定义:

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

    如何转换类型变量 在里面 (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)
    

    目前尚不清楚转型从何而来 a -> b 发生。

    3 回复  |  直到 6 年前
        1
  •  8
  •   Aadit M Shah    6 年前

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

    实际上,在这条线上 int<-randomRIO(1,10) 它是 randomRIO 这就产生了 内景 而且是 供应随机 那是在消耗它。同样,在 f整数 它是 供应随机 生产(即供应) 内景 而且是 f 那是在消耗它。

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

    produceIntOutOfThinAir :: Callback Int
    produceIntOutOfThinAir = Callback $ \f -> f 42 -- produced 42 out of thin air
    

    在作者的例子中, 供应随机 不会产生 内景 稀薄的空气。相反,它需要 内景 那个 随机数 生产并反过来提供 内景 f . 那很好。

    的类型签名 供应随机 (即。 (Int -> IO ()) -> IO () 打开包装时)只告诉我们 供应随机 产生一些 内景 . 它没有具体说明 内景 必须生产。


    原始答案:

    让我们看看 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 . 你可以想到 未定义 作为实际值的占位符(即在空白处填充)。那么,我们如何填补这个空白呢?我们有两个选择。我们可以申请 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 我们都没有类型的值 我们也没有任何生成类型值的函数 . 所以,应用 h类 不是选项。回到目标1。我们的另一个选择是 g级 . 那么,让我们试试看:

    fmap f g h = g (undefined :: a -> IO ()) -- goal 4
    

    我们的新目标是 a -> IO () . 类型的值是什么 a->IO() 看起来像因为这是一个函数,我们知道它看起来像λ:

    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级 . 你看,那种类型 是协变的 g级 也就是说 g级 生产 . 实际上,当我们创建lambda来填补目标4的空白时,我们引入了一个新变量 x个 进入我们的环境中,从 g级 .

    无论如何,我们再次需要生成类型为 IO() 但现在我们可以回到选项1(即应用 h类 )因为我们最终得到了一个类型为 . 我们不想回到选项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 -> IO ()) -> IO () . 因此,可以定义 Functor 的实例 回调函数 .

        2
  •  2
  •   amalloy    6 年前

    类型的函数 a -> IO () 是需要 a :如果没有 在某处听起来你已经知道了。,但为了让下一点更清楚,需要重复一下。

    那么,a呢 Callback a ,一个愿意对类型的值进行操作的函数 a->IO() ? 它对这样一个值进行操作的唯一方法是传递一些 这正是我们在上一段中确定的。所以虽然你不知道 怎样 它产生了这个 ,它必须能够以某种方式生产一个,否则它不能用它的 a->IO() .

    因此,您可以 fmap 在那上面 ,生成 b ,并产生总体a Callback b ,该值可用于任何 b -> IO () .

        3
  •  2
  •   Aadit M Shah    6 年前

    所以我们有这个:

    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 () ,并返回 IO () .

      transformCallback f g = \h -> ????
    

    很好,现在我们有一个函数 f 的类型 a->b ,一个函数 h 的类型 b->IO () ,以及原始回调 g 的类型 ((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() 独立地。但是如果调用了它,回调就会调用它,它会得到 a 为了满足这个需求 a->IO() 从某处。重要的是要重复: 回调生成 并将其纳入其论点 .

    现在,如果我们为原始回调提供一个函数,该函数将转换 b 然后将结果提供给类型为 b->IO ,回调函数与任何其他类型的函数一样乐于使用它 a->IO . 现在和以前一样, 回调生成 并将其纳入其论点 ,参数将其转换为 b ,然后生成 IO ,一切照常进行。