18

我最近写

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)

但即便如此,这一切都感觉有点手动。有没有一些我没见过的很好的模式来收紧它?

4

3 回答 3

15

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


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

这有点微妙——<$>尽管看起来你可能想要使用它,但在此处使用很重要,=<<这样第一行的结果实际上是稍后执行的一元动作,而不是立即执行的动作。

于 2022-01-25T05:39:04.413 回答
11

这个问题得太多了,但是...

在您的代码中,每个分支的类型Either可能是不同的,但它们不会逃脱 do-block,因为它们被LeftandRight延续“擦除”。

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

实际上,我们不必声明它,因为在 Hackage 中已经存在这样的类型:它Coyoneda来自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)

在我们的例子中,“返回值”本身就是一个 monadic action m,所以我们要处理 type 的值Coyoneda m (m a)wherem 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]
于 2022-01-24T22:57:13.280 回答
8

你也许可以这样做:

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

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

(当然,如果这些more actions绑定了许多变量,这不会很好地工作)

于 2022-01-24T21:44:48.817 回答