如何在MonadPlus/Alternative中进行合并和分支

16

我最近写了一篇文章

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)

但即便如此,这一切仍感觉有点繁琐。我是否还有其他优雅的方法可以让它更加简洁明了?

3个回答

15

我刚刚注意到OP在评论中已经表达了相同的观点in a comment。不管怎样,我还是要发表我的想法。


Coyoneda是一个很好的技巧,但对于这个特定的问题来说有点过头了。我认为你只需要普通的continuations。
让我们给那些...命名:
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

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


1
不错的解决方案,它让我想起了“返回命令”技巧,该技巧也使用了嵌套操作:https://www.haskellforall.com/2021/10/the-return-command-trick.html - danidiaz
3
这两段代码片段有所区别:在第一段中,fx可以清晰地引用由“更多操作”绑定的变量。 - Daniel Wagner
2
@DanielWagner 您绝对是正确的。可以让 fx :: TypeOfM -> More -> Types -> m (),并且类似地为 fy。然后,最后一行将是 e p q 或类似的内容。如果在 more actions 中绑定了很多变量,则这会很快变得丑陋,但对于仅有一两个变量的情况,它可能会很好地解决问题。 - DDub
@DanielWagner,没错;在某些时候,我的原始解决方案可能看起来还不错。但是在我原始的上下文中,没有绑定变量。 - dfeuer

10

这有点过于深入思考问题了,但是...

在你的代码中,Either 的每个分支的类型可能是不同的,但它们不会逃离 do 块,因为它们被 LeftRight 的延续“擦除”了。

这看起来有点像存在类型。也许我们可以声明一个类型,将初始操作与其延续打包在一起,并给该类型一个 Alternative 实例。

实际上,我们不必声明它,因为这样的类型已经存在于 Hackage 中:它是来自 kan-extensionsCoyoneda

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

其中包含有用的例子

Alternative f => Alternative (Coyoneda f)
MonadPlus f => MonadPlus (Coyoneda f)
在我们的情况下,“返回值”本身将是一个单子操作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]

4
我有些喜欢它!它似乎还表明了一种更基础的方法,我认为我更喜欢:do { final <- (m <&> xCont) <|> (n <&> yCont); more; actions; final } - dfeuer
@dfeuer 是的,Coyoneda 在这里有些过头了。使用 Coyoneda 将初始动作和继续操作分开是有效的,但嵌套它们更简单,而且不需要额外的类型。 - danidiaz
1
这看起来真的很酷 - 终于有了一个应用Yoneda约简的明确目的,而不仅仅是“琐碎但难以理解”!这是否有一个良好的范畴论解释?Alternative/MonadPlus似乎总是在Haskell函子层次结构的阴暗面。 - leftaroundabout
2
@leftaroundabout,如果我没记错的话,Coyoneda 的另一个用途是将像 IORef 这样没有实例的类型转换为函子,通过在 Coyoneda 的函数组件中“隐藏”fmappings。 https://www.reddit.com/r/haskell/comments/5v33qk/forall_a_ioref_a_lens_a_b_is_a_useful_type_does/ddyxp6x/ - danidiaz
@leftaroundabout,使用(Co)Yoneda和类似类型与泛型遍历相结合可以实现一些非常酷的功能。不幸的是,我无法说服最近的 GHC 对它们进行优化,但这并不是它们的错。(连接点 vs 内联。叹气 - dfeuer
1
顺便说一句,在这里即使使用Coyoneda,也没有必要使用hoistCoyoneda。您可以先降低,此时您回到了基本解决方案轨道上。 - dfeuer

6

也许可以这样做:

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

我不知道这看起来对你是否更好。

(当然,如果那些“更多操作”绑定了许多变量,这并不容易解决)


4
我相信这只等同于满足左分配律的情况,因为它会保留判断来查看中间操作是否失败。 - dfeuer
1
在某些情况下可能很好,但我的(愚蠢未言)上下文被忽略了。 - dfeuer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接