【问题标题】:Iterating over a Data.Set until success迭代 Data.Set 直到成功
【发布时间】:2021-06-02 09:26:03
【问题描述】:

假设我有一个函数表示一些可能失败的计算

f :: a -> Maybe b

如果我有一个列表l,我可以使用findList f l 在列表中找到f 成功的第一个(从左到右扫描时)项目,其中findList 是以下函数

findList :: (a -> Maybe b) -> [a] -> Maybe b
findList f [] = Nothing
findList f (x : xs) = case f x of
  Nothing -> findList f xs
  Just y -> Just y

(或使用,例如,extra 包中的 firstJust)。

问题。

如果我想用来自containersSet 做同样的事情该怎么办?

也就是说,我想要一个带签名的函数

findSet :: (a -> Maybe b) -> Set a -> Maybe b

相当于

findSet f s = findList f (Set.toList s)

上面的行作为一个实现。但是,我不想创建中间列表Set.toList s(即使使用惰性求值,仍然存在一些不必要的开销,对吧?)。

一旦找到成功的项目,我也不想遍历整个集合,如以下实现所示:

findSet f s = foldl g Nothing s
  where
    g Nothing x = f x
    g (Just y) _ = Just y

那么有没有一种好方法可以遍历一个“从左到右”的集合,对每个成员应用一个可能失败的计算,然后在第一个成功的结果处停止?

【问题讨论】:

  • 当使用第二个参数不严格的 reducer 函数调用时,foldr 从左到右工作。你的第一个函数是\f -> listToMaybe . mapMaybe f :: (a1 -> Maybe a) -> [a1] -> Maybe a,第二个函数是\f -> listToMaybe . mapMaybe f . toList :: Foldable t => (a1 -> Maybe a) -> t a1 -> Maybe a。如果您的数据类型的 Foldable 实例定义了惰性的 toList,则开销应该很小,如果有的话。

标签: haskell set maybe


【解决方案1】:

我想分享一个 IRC 上有人曾经向我展示过的小技巧:成功时提前退出只是错误时提前退出的另一种解释。考虑到这一点,考虑使用Either b () 而不是Maybe b;那么

findOne :: (a -> Either b ()) -> Set a -> Either b ()
findOne = traverse_

几乎不值得命名。如果你必须有Maybe,你可以简单地用明显的同构包装它。

findOneMaybe :: (a -> Maybe b) -> Set a -> Maybe b
findOneMaybe f = to . traverse_ (from . f) where
    from = maybe (Right ()) Left
    to = either Just (const Nothing)

【讨论】:

  • 感谢丹尼尔的建议!这确实是一个很好的技巧。我之前考虑过将它用于我的用例,但遇到了我在其他地方依赖于 Maybe 的单性的问题,因此需要权衡是否必须重写一些 do 块而不是什么。不过,这是traverse 的一个很好的用法!
  • @ArtemMavrin 在您将其交给traverse 之前,将a -> Maybe b 转换为a -> Either b () 很容易;只需用maybe (Right ()) Left 撰写。同样,使用either Just (const Nothing) 很容易转换回来。
  • 另一个问题:Set(至少来自collections)不是Traversable :(
  • @ArtemMavrin traverse_(相对于traverse!)不需要Traversable,尽管它的名字。
【解决方案2】:

您可以使用SetFoldable 的实例这一事实。因此,您可以使用以下方法实现Fold 函数:

import Control.Applicative((<|>))

findSet :: (a -> Maybe b) -> Set a -> Maybe b
findSet f = foldr ((<|>) . f) Nothing

这实际上可以与Foldable 的所有实例一起工作:

import Control.Applicative((<|>))

findSet :: (Alternative g, Foldable f) => (a -> g b) -> f a -> g b
findSet f = foldr ((<|>) . f) Nothing

或者作为@DanielWagner says,我们可以使用First,因为First会选择第一个Just,因此可以使用:

import Control.Applicative((<|>))

findSet :: Foldable f => (a -> Maybe b) -> f a -> Maybe b
findSet f = getFirst . foldMap (First . f)

toList的实现也是按照Foldable来实现的:

toList :: Foldable f => f a -> [a]
toList = foldr (:) []

所以我们在这里所做的不是先将它包装在一个 cons 中然后再打开它,而是立即应用 f 并使用 (&lt;|&gt;) :: Alternate f =&gt; f a -&gt; f a -&gt; f a 的实现。

【讨论】:

  • 啊,太好了!我有一种感觉会对Foldable 进行概括,但这给出了对(Foldable t, Alternative f) =&gt; (a -&gt; f b) -&gt; t a -&gt; f b 的概括。整洁的。谢谢!
  • 比起foldr,我更喜欢foldMap 作为一个原始人。考虑如何使用它,您实际上得到了一个相当不错的替代实现:findSet f = foldMap (First . f),出于性能考虑,它可以只是findSet = coerce foldMap,在coerce 上有一个适当的痛苦类型签名。
  • Alternate是什么,import Control.Applicative((&lt;|&gt;))在使用First的代码sn-p中做了什么?
  • @WillNess:它是 阿特伍德的鸭子
  • 根据this 所说,“鸭子”是您对公司主管 所做的事情,而不是寻求帮助的人(或未来的读者)。和原来的故事不同,这里的鸭子打破了画面。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-07-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多