根据Control.Arrow
文档,对于许多monad(>>=
操作严格的那些),instance MonadFix m => ArrowLoop (Kleisli m)
不符合正确的法律(loop (f >>> first h) = loop f >>> h
)ArrowLoop
类所要求的。为什么会这样?
答案 0 :(得分:6)
这是一个具有多个不同角度的多方面问题,它可以追溯到Haskell中的值递归(mfix
/ mdo
)。有关背景信息,请参阅here。我将在这里详细讨论正确的紧缩问题。
这是mfix
的正确紧缩属性:
mfix (λ(x, y). f x >>= λz. g z >>= λw. return (z, w))
= mfix f >>= λz. g z >>= λw. return (z, w)
这是图片形式:
虚线表示"打结"正在发生。这基本上与问题中提到的法律相同,不同之处在于它以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
表示法的示例,这可能更容易阅读。)
不满足此法律的monad的典型示例包括Maybe
,List
,IO
或任何其他基于monad的monad具有多个构造函数的代数类型。 做满足此法则的monad的典型示例是State
和Environment
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。)本案例中的图片如下所示:
此属性来自滑动属性,它是值递归的dinaturality版本,并且已知为许多感兴趣的monad满足:Section 2.4。
这项法律的失败会影响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)
有人可能会天真地认为上面的lhs
和rhs
应该是相同的,但是由于正确缩小的法律的失败,它们不是。就像之前一样,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
- 表示法处理此问题并将递归放在最小的块上而无需用户干预。
在这漫长的绕道之后,现在让我们回到关于箭头相应法则的原始问题。与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
实例中满足正确的紧缩法律。