如何合并然后在 MonadPlus/Alternative 中分支

How to combine and then branch in MonadPlus/Alternative

我最近写了

do
  e <- (Left <$> m) <|> (Right <$> n)
  more actions
  case e of
    Left x -> ...
    Right y -> ...

这看起来很尴尬。我知道 protolude (和其他一些包)定义

-- Called eitherP in parser combinator libraries
eitherA :: Alternative f => f a -> f b -> f (Either a b)

但即便如此,感觉还是有点手动。有没有我没见过的用于收紧它的漂亮图案?

你也许可以这样做:

do
  let acts = do more actions
  (do x <- m; acts; ...) <|> (do y <- n; acts; ...)

不知道你觉得这样好不好。

(当然,如果那些 more actions 绑定了很多变量,效果就不太好)

这是方式想多了这个问题,但是...

在您的代码中,Either 的每个分支的类型可能不同,但它们不会转义 do-block,因为它们被 LeftRight 延续。

这看起来有点像存在主义类型。或许我们可以声明一个类型,将初始操作和它的延续打包在一起,并为该类型提供一个 Alternative 实例。

其实我们不用声明,因为Hackage中已经存在这样的类型:Coyoneda from kan-extensions.

data Coyoneda f a where       
    Coyoneda :: (b -> a) -> f b -> Coyoneda f a  

哪些实例有用

Alternative f => Alternative (Coyoneda f)
MonadPlus f => MonadPlus (Coyoneda f)

在我们的例子中,“return 值”本身就是一个单子动作 m,因此我们要处理 Coyoneda m (m a) 类型的值,其中 m a 是总体类型 do-block.

了解所有这些,我们可以定义以下函数:

sandwich :: (Foldable f, MonadPlus m, Monad m) 
         => m x 
         -> f (Coyoneda m (m a)) 
         -> m a
sandwich more = join . lowerCoyoneda . hoistCoyoneda (<* more) . asum 

重新实现原始示例:

sandwich more [Coyoneda m xCont, Coyoneda n yCont]

我刚刚注意到 OP 表达了同样的想法 。无论如何我都会post我的想法。


Coyoneda 是一个巧妙的技巧,但对于这个特定问题来说有点矫枉过正。我想你所需要的只是常规的旧延续。

让我们将这些 ... 命名为:

do
  e <- (Left <$> m) <|> (Right <$> n)
  more actions
  case e of
    Left x -> fx x
    Right y -> fy y

那么,我们可以这样写:

do
  e <- (fx <$> m) <|> (fy <$> n)
  more actions
  e

这有点微妙——在这里使用 <$> 很重要,即使看起来您可能想使用 =<< 这样第一行的结果实际上是一个 monadic 动作稍后执行而不是立即执行。