【问题标题】:What would an idiomatic, monadic version of maximumBy look like?maximumBy 的惯用单子版本会是什么样子?
【发布时间】:2018-04-26 21:00:18
【问题描述】:

如何获得有效容器的最大元素,其中要比较的计算属性也会触发效果?

必须有更易读的方式来做这样的事情:

latest dir = Turtle.fold (z (ls dir)) Fold.maximum

z :: MonadIO m => m Turtle.FilePath -> m (UTCTime, Turtle.FilePath)
z mx = do
    x <- mx
    d <- datefile x
    return (d, x)

我使用重载版本而不是非重载版本maximumBy,但后者似乎更适合临时属性选择。

我怎样才能更有条理地解决类似的问题?

【问题讨论】:

  • 你的一元maximumBy的类型签名是什么?
  • 从非单子函数转换为单子函数通常很有用,尤其是当所讨论的函数以某种有趣的方式惰性时。例如,ifM 很有用并且必须手动编写(与 liftA3 if' 相反),因为它会避免在两个分支中执行其中一种或另一种效果。但是maximumBy 必须查看整个列表;所以没有有趣的懒惰需要保留。只需使用像 mapM 这样的标准单子组合器来预先完成所有效果,然后再进行纯粹的计算。
  • 我可能会误解,但这种方法是否允许严格折叠运行而不会出现空间泄漏?
  • @sevo 好点,这是另一个考虑因素,它可能值得手动推出一元版本。

标签: haskell monads


【解决方案1】:

所以我对 Turtle 一无所知;不知道这是否适合 Turtle 生态系统的其他部分。但是,既然您在 cmets 中说服了我 maximumByM 值得手写,那么我将这样做:

maximumOnM :: (Monad m, Ord b) => (a -> m b) -> [a] -> m a
maximumOnM cmp [x] = return x -- skip the effects if there's no need for comparison
maximumOnM cmp (x:xs) = cmp x >>= \b -> go x b xs where
    go x b [] = return x
    go x b (x':xs) = do
        b' <- cmp x'
        if b < b' then go x' b' xs else go x b xs

我通常更喜欢 *On 版本的事物——它采用一个映射到 Orderable 元素的函数——而不是 *By 版本——它采用一个直接进行比较的函数。 maximumByM 会很相似,但有类似 Monad m =&gt; (a -&gt; a -&gt; m Ordering) -&gt; [a] -&gt; m a 的类型,但这可能会迫使您为每个 a 重做效果,我猜这不是您想要的。我发现 *On 更常与我想做的事情和我想要的性能特征相匹配。

【讨论】:

  • 为什么不从Fold升级到FoldM
  • @dfeuer 我不太明白这个问题。
  • 问题中的Fold.maximum来自foldl-Control.Foldl,它提供了一个Fold类型(其fold操作被Turtle重新导出)但也是一个FoldM类型.这两个都有 Applicative 实例以“并行”组合折叠。应该可以将有效最大值表示为FoldM
  • @dfeuer 听起来很有趣。你应该添加它!
  • 我刚做了。它与您的非常相似,尽管我返回 Maybe
【解决方案2】:

由于您已经熟悉Fold,您可能想了解FoldM,它与此类似。

data FoldM m a b =
  -- FoldM step initial extract
  forall x . FoldM (x -> a -> m x) (m x) (x -> m b)

你可以写:

maximumOnM ::
  (Ord b, Monad m)
  => (a -> m b) -> FoldM m a (Maybe a)
maximumOnM f = FoldM combine (pure Nothing) (fmap snd)
  where
    combine Nothing a = do
      f_a <- f a
      pure (Just (f_a, a))
    combine o@(Just (f_old, old)) new = do
      f_new <- f new
      if f_new > f_old
        then pure $ Just (f_new, new)
        else pure o

现在您可以使用Foldl.foldM 在列表(或其他Foldable 容器)上运行折叠。与 Fold 一样,FoldM 也有一个 Applicative 实例,因此您可以将多个有效折叠组合成一个,将每个折叠的效果交错并组合它们的结果。

【讨论】:

  • 不错! f a第一个模式的效果懒得评价了!不过,这是有保证的吗?
  • 应用版很厉害,但是在组合函数的眼里,如果相等,会不会破坏结果的“稳定性”?
  • @sevo,我想我修复了类型错误。还没试过编译。稳定性是什么意思?
  • 由于 Applicative 可以并行运行效果,因此 IO 效果的完成顺序可能与它们在输入中出现的顺序不同。在这种情况下,在两个“相等”的元素中,应始终选择输入中第一个出现的元素。我想知道 foldM 是否有这个属性。
  • @sevo,FoldMApplicative 实例是完全确定的。它按从左到右的顺序运行元素的效果,然后移动到下一个元素。
【解决方案3】:

可以使用 reducers 包在可折叠设备上运行效果。

我不确定它是否正确,但它利用了现有的组合器和实例(Bounded (Maybe a) 除外)。

import Data.Semigroup.Applicative (Ap(..))
import Data.Semigroup.Reducer (foldReduce)
import Data.Semigroup (Max(..))
import System.IO (withFile, hFileSize, IOMode(..))

-- | maxLength
--
-- >>> getMax $ maxLength ["abc","a","hello",""]
-- 5
maxLength :: [String] -> (Max Int)
maxLength = foldReduce . map (length)

-- | maxLengthIO
--
-- Note, this runs IO...
--
-- >>> (getAp $ maxLengthIO ["package.yaml", "src/Lib.hs"]) >>= return . getMax
-- Just 1212
--
-- >>> (getAp $ maxLengthIO []) >>= return . getMax
-- Nothing
maxLengthIO :: [String] -> Ap IO (Max (Maybe Integer))
maxLengthIO xs = foldReduce (map (fmap Just . f) xs) where
    f :: String -> IO Integer
    f s = withFile s ReadMode hFileSize

instance Ord a => Bounded (Maybe a) where
    maxBound = Nothing
    minBound = Nothing

【讨论】:

  • 孤立实例是邪恶的,那个实例甚至没有意义。你的意思可能是minBound = Nothing; maxBound = Just maxBound
  • 根据我对您其他问题的回答,我会推荐 maxLengthIO = foldMap (Ap . fmap Finite . f),使用 Data.Monoid.Inf.Finite。所需的体操更少,而且它使用了现代的 Foldable 成语,已经取代了 reducers 包。
  • 请给我一些时间,为什么我的大脑试图理解,但这看起来像是完美的答案!我不知道 Foldable 已经取代了减速器包!
猜你喜欢
  • 1970-01-01
  • 2011-08-15
  • 2011-09-09
  • 2010-09-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-04
  • 2011-04-01
相关资源
最近更新 更多