【问题标题】:Standard combinator to get first "non-empty" value from a set of monadic actions从一组单子动作中获取第一个“非空”值的标准组合器
【发布时间】:2020-01-18 07:14:29
【问题描述】:

我确定我在这里遗漏了一些非常明显的东西。以下是我试图在概念层面实现的目标:

action1 :: (MonadIO m) => m [a]
action1 = pure []

action2 :: (MonadIO m) => m [a]
action2 = pure [1, 2, 3]

action3 :: (MonadIO m) => m [a]
action3 = error "should not get evaluated"

someCombinator [action1, action2, action3] == m [1, 2, 3]

这个假设的someCombinator 存在吗?我尝试过使用<|>msum,但无法得到我想要的。

我想,这可以概括为两种方式:

-- Will return the first monadic value that is NOT an mempty 
-- (should NOT blindly execute all monadic actions)
-- This is something like the msum function

someCombinator :: (Monoid a, Monad m, Traversable t, Eq a) => t m a -> m a

-- OR

-- this is something like the <|> operator

someCombinator :: (Monad m, Alternative f) => m f a -> m f a -> m f a

【问题讨论】:

  • 严格来说,Monoid a 不足以测试给定值是否为“空”。您还需要Eq a,以便与mempty 进行比较。
  • @FyodorSoikin 感谢您的提示。更新了问题中的代码 sn-p。

标签: haskell monads alternative-functor


【解决方案1】:

我不知道提供这个的库,但实现起来并不难:

someCombinator :: (Monoid a, Monad m, Foldable t, Eq a) => t (m a) -> m a 
someCombinator = foldr f (pure mempty)
    where
        f item next = do
            a <- item
            if a == mempty then next else pure a

请注意,您甚至不需要TraversableFoldable 就足够了。

【讨论】:

  • 刚刚在stackoverflow.com/questions/59798509/… 写了类似的东西——我很惊讶标准库中还没有像filterFirstM 这样的东西。
  • 实际上,我无法理解您基于foldlr 的实现。这会给我从 L 到 R 或 R 到 L 的第一个“非空”值吗?
  • 从左到右。请记住,foldr f z [x1, x2, x3] == f x1 (f x2 (f x3 z))
  • 基于 foldr 的实现比我基于 foldl 的实现要好。我将把这个问题开放几天,看看是否有人想出了一个内置/标准组合器。
  • @SaurabhNanda 请记住foldr f z [x1, x2, x3 ...] == f x1 (foldr f z [x2, x3 ...])。所以当f 用它的两个参数——x1(foldr f z [x2, x3 ...]) 被调用时,列表[x2, x3 ...] 的其余部分仍然没有被探索。并且 if f 足够懒惰并且在返回结果之前不会要求其第二个参数的值,那么,当返回结果时,列表 [x2, x3 ...] 的其余部分将保持未探索状态。这意味着当 f 在其第二个参数中是非严格的时,从左到右探索输入列表。
【解决方案2】:

在抽象级别上,第一个非空值是一个名为 FirstMonoid。然而,事实证明,如果你只是天真地将IO 值提升为First,那么action3since the default monoidal append operation is strict under IO 就会出现问题。

您可以使用来自this answerFirstIO 类型获得惰性幺半群计算。它不会比 Fyodor Soikin 的回答更好,但它强调(我希望)你可以如何从通用抽象中组合行为。

除了上面提到的FirstIO wrapper,你可能会发现这个函数很有用:

guarded :: Alternative f => (a -> Bool) -> a -> f a
guarded p x = if p x then pure x else empty

我基本上只是从Protolude 复制它,因为我在 base 中找不到具有所需功能的一个。您可以使用它将列表包装在Maybe 中,以便它们适合FirstIO

> guarded (not . null) [] :: Maybe [Int]
Nothing
> guarded (not . null) [1, 2, 3] :: Maybe [Int]
Just [1,2,3]

对您的操作列表中的每个操作执行此操作,并将它们包装在 FirstIO 中。

> :t (firstIO . fmap (guarded (not . null))) <$> [action1, action2, action3]
(firstIO . fmap (guarded (not . null))) <$> [action1, action2, action3]
  :: Num a => [FirstIO [a]]

在上面的 GHCi sn-p 中,我只显示了带有:t 的类型。我无法显示该值,因为 FirstIO 没有 Show 实例。然而,关键是您现在有一个 FirstIO 值列表,mconcat 将从其中选择第一个非空值:

> getFirstIO $ mconcat $ (firstIO . fmap (guarded (not . null))) <$> [action1, action2, action3]
Just [1,2,3]

如果要解压Maybe,可以使用fromMaybe from Data.Maybe:

answer :: IO [Integer]
answer =
  fromMaybe [] <$>
  (getFirstIO $ mconcat $ (firstIO . fmap (guarded (not . null))) <$> [action1, action2, action3])

这显然比 Fyodor Soikin 的回答更复杂,但我对 Haskell 如何让您通过“点击”现有事物(几乎就像乐高积木)来组装所需功能感到着迷。

那么,对于这个组合器是否已经存在的问题?答案是它确实存在,但需要一些组装。

【讨论】:

  • 谢谢你的详细回答,马克。我从您的回答中了解到FirstFirstIO。但是,正如您所指出的,Fyodor 的答案具有较少的活动部分,并且更易于实施/理解。
猜你喜欢
  • 2012-06-24
  • 2020-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-12
  • 1970-01-01
  • 2015-12-23
  • 2020-08-24
相关资源
最近更新 更多