正确收紧ArrowLoop法

时间:2017-01-16 11:29:48

标签: haskell typeclass arrows

根据Control.Arrow文档,对于许多monad(>>=操作严格的那些),instance MonadFix m => ArrowLoop (Kleisli m)不符合正确的法律(loop (f >>> first h) = loop f >>> hArrowLoop类所要求的。为什么会这样?

1 个答案:

答案 0 :(得分:6)

这是一个具有多个不同角度的多方面问题,它可以追溯到Haskell中的值递归(mfix / mdo)。有关背景信息,请参阅here。我将在这里详细讨论正确的紧缩问题。

右键紧缩mfix

这是mfix的正确紧缩属性:

mfix (λ(x, y). f x >>= λz. g z >>= λw. return (z, w))
    = mfix f >>= λz. g z >>= λw. return (z, w)

这是图片形式:

Right shrinking law, diagrammatically

虚线表示"打结"正在发生。这基本上与问题中提到的法律相同,不同之处在于它以mfix和值递归的形式表达。如Section 3.1 of this work所示,对于具有严格绑定运算符的monad,您始终可以编写一个表达式来区分此等式的左侧与右侧,从而使此属性失败。 (请参阅下面的Haskell中的实际示例。)

当使用mfix的monad通过Kleisli构造创建箭头时,相应的loop运算符会以相同的方式失败相应的属性。

域理论和近似

在领域理论术语中,不匹配将始终是近似值。

也就是说,左手边的位置总是比右手边少。 (更准确地说,lhs将低于rhs,在PCPOs领域,我们用于Haskell语义的典型域。)在实践中,这意味着右侧将更频繁地终止,并且在这是一个问题。有关详细信息,请参阅this的第3.1节。

在实践中

这听起来可能都是抽象的,在某种意义上说它是。更直观地说,左侧有机会对递归值进行操作,因为g位于"循环内,#34;因此能够干扰定点计算。这是一个实际的Haskell程序来说明:

import Control.Monad.Fix

f :: [Int] -> IO [Int]
f xs = return (1:xs)

g :: [Int] -> IO Int
g [x] = return x
g _   = return 1

lhs = mfix (\(x, y) -> f x >>= \z -> g z >>= \w -> return (z, w))
rhs = mfix f >>= \z -> g z >>= \w -> return (z, w)

如果您评估lhs它将永远不会终止,而rhs将为您提供无限的1个列表:

*Main> :t lhs
lhs :: IO ([Int], Int)
*Main> lhs >>= \(xs, y) -> return (take 5 xs, y)
^CInterrupted.
*Main> rhs >>= \(xs, y) -> return (take 5 xs, y)
([1,1,1,1,1],1)

我在第一种情况下中断了计算,因为它是非终止的。虽然这是一个人为的例子,但最简单的说明一点。 (请参阅下文,了解使用mdo表示法的示例,这可能更容易阅读。)

示例monads

满足此法律的monad的典型示例包括MaybeListIO或任何其他基于monad的monad具有多个构造函数的代数类型。 满足此法则的monad的典型示例是StateEnvironment monad。有关汇总这些结果的表格,请参见Section 4.10

纯右收缩

请注意"较弱"右上紧的形式,其中上式中的函数g是纯的,遵循价值递归定律:

mfix (λ(x, y). f x >>= λz. return (z, h z))
  = mfix f >>= λz. return (z, h z)

这与以前的法律相同,g = return . h。那是g无法执行任何效果。在这种情况下,没有办法像你期望的那样区分左手边和右手边。结果确实来自价值递归公理。 (有关证据,请参阅Section 2.6.3。)本案例中的图片如下所示:

Pure-right shrinking

此属性来自滑动属性,它是值递归的dinaturality版本,并且已知为许多感兴趣的monad满足:Section 2.4

对mdo-notation的影响

这项法律的失败会影响mdo notation在GHC中的设计方式。翻译包括所谓的"细分"正是为了避免缩小法律的失败。有些人认为有点争议,因为GHC会自动选择细分市场,主要是采用正确的法律。如果需要明确控制,GHC提供rec keyword将决定留给用户。

使用mdo - 表示法和显式do rec,上面的示例呈现 如下:

{-# LANGUAGE RecursiveDo #-}

f :: [Int] -> IO [Int]
f xs = return (1:xs)

g :: [Int] -> IO Int
g [x] = return x
g _   = return 1


lhs :: IO ([Int], Int)
lhs = do rec x <- f x
             w <- g x
         return (x, w)

rhs :: IO ([Int], Int)
rhs = mdo x <- f x
          w <- g x
          return (x, w)

有人可能会天真地认为上面的lhsrhs应该是相同的,但是由于正确缩小的法律的失败,它们不是。就像之前一样,lhs被卡住了,而rhs成功地产生了值:

*Main> lhs >>= \(x, y) -> return (take 5 x, y)
^CInterrupted.
*Main> rhs >>= \(x, y) -> return (take 5 x, y)
([1,1,1,1,1],1)

在视觉上检查代码时,我们发现递归只是函数f,这证明了由mdo - 符号自动执行的分段。

如果首选rec符号,程序员需要将其放在最小的块中以确保终止。例如,lhs的上述表达式应写成如下:

lhs :: IO ([Int], Int)
lhs = do rec x <- f x
         w <- g x
         return (x, w)

mdo - 表示法处理此问题并将递归放在最小的块上而无需用户干预。

Kleisli Arrows失败

在这漫长的绕道之后,现在让我们回到关于箭头相应法则的原始问题。与mfix情况类似,我们也可以为Kleisli箭头构建一个失败的例子。事实上,上面的例子或多或少直接翻译:

{-# LANGUAGE Arrows #-}
import Control.Arrow

f :: Kleisli IO ([Int], [Int]) ([Int], [Int])
f = proc (_, ys) -> returnA -< (ys, 1:ys)

g :: Kleisli IO [Int] Int
g = proc xs -> case xs of
                 [x] -> returnA -< x
                 _   -> returnA -< 1

lhs, rhs :: Kleisli IO [Int] Int
lhs = loop (f >>> first g)
rhs = loop f >>> g

就像mfix一样,我们有:

*Main> runKleisli rhs []
1
*Main> runKleisli lhs []
^CInterrupted.

IO-monad mfix的右侧紧缩失败也阻止了Kleisli IO箭头在ArrowLoop实例中满足正确的紧缩法律。