【问题标题】:How to get only a particular type of elements from a list in Haskell?如何从 Haskell 的列表中仅获取特定类型的元素?
【发布时间】:2016-08-06 14:44:05
【问题描述】:

我正在学习Haskell Book,在第 10 章(折叠列表)中,我正在尝试解决一个关于从包含不同类型元素的列表中仅获取一种特定类型元素的练习。

作者给出以下代码:

import Data.Time

data DatabaseItem = DbString String
                  | DbNumber Integer
                  | DbDate   UTCTime
                  deriving (Eq, Ord, Show)

theDatabase :: [DatabaseItem]
theDatabase = [ DbDate (UTCTime
                        (fromGregorian 1911 5 1)
                        (secondsToDiffTime 34123))
              , DbNumber 9001
              , DbString "Hello, world!"
              , DbDate (UTCTime
                        (fromGregorian 1921 5 1)
                        (secondsToDiffTime 34123))
              ]

第一个问题是:

编写一个过滤 DbDate 值并返回一个列表的函数 其中的 UTCTime 值。

filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate = undefined

由于这一章都是关于折叠列表的,我假设它可以使用例如foldr 来完成。

我最初的尝试是先写一些辅助函数并在foldr中使用它们,例如:

getDbDate1 :: DatabaseItem -> UTCTime
getDbDate1 (DbDate utcTime) = utcTime

isDbDate :: DatabaseItem -> Bool
isDbDate (DbDate _) = True
isDbDate _ = False

filterDbDate1 :: [DatabaseItem] -> [UTCTime]
filterDbDate1 database = foldr ((:) . getDbDate1) [] (filter isDbDate database)

这似乎可以完成这项工作,因为:

λ> filterDbDate1 theDatabase
[1911-05-01 09:28:43 UTC,1921-05-01 09:28:43 UTC]

但我对这个解决方案并不满意,因为首先它会给出以下警告:

/Users/emre/code/haskell/chapter10_folding_lists/database.hs:36:1: Warning: …
    Pattern match(es) are non-exhaustive
    In an equation for ‘getDbDate1’:
        Patterns not matched:
            DbString _
            DbNumber _

我正在使用两个辅助函数,一个用于帮助过滤掉不是 DbDate 的值,另一个用于获取 UTCTime 组件。

因此,为了摆脱非详尽的模式匹配警告并使用单个辅助函数,我决定将其编写为:

getDbDate2 :: DatabaseItem -> Maybe UTCTime
getDbDate2 (DbDate utcTime) = Just utcTime
getDbDate2 _ = Nothing

filterDbDate2 :: [DatabaseItem] -> [UTCTime]
filterDbDate2 database = foldr ((:) . getDbDate2) [] database

但是,当然,上面没有编译,因为它没有类型检查,因为,例如:

λ> foldr ((:) . getDbDate2) [] theDatabase
[Just 1911-05-01 09:28:43 UTC,Nothing,Nothing,Just 1921-05-01 09:28:43 UTC]

换句话说,它可以返回Just UTCTime 值的列表以及Nothing 值,而不仅仅是UTCTime 值的列表。

所以我的问题是:我怎样才能编写一个(帮助器?)函数,一次(这样我就不必使用filter)检查它的值是否为DbNumber,如果所以返回UTCTime 组件? (如果不是......它还必须返回一些东西(例如Nothing?),这就是我遇到麻烦的地方,即使用Maybe UTCTime,然后获取Just UTCTime值等)

【问题讨论】:

  • catMaybes :: [Maybe a] -> [a] 可以在这里提供帮助。
  • 函数f = maybe [] (:[]) . getDbDate2返回一个时间的单例列表,如果有,则返回一个空列表。 filterDbDate 就是 foldMap f

标签: haskell


【解决方案1】:

这里还有其他几个答案,涵盖了关于其他思考问题的方法的好建议:在挑选出Maybe UTCTimes 后第二次使用catMaybes 处理数据;使用列表推导和它们用于过滤不匹配模式的便捷语法;使用列表的一元结构来包含或跳过结果;并编写一个定制的递归函数。在这个答案中,我将解决您的直接问题,展示如何使用您已有的程序结构,而无需完全重新考虑您的列表操作方法 - 调用 foldr 并使用一个帮助函数一次性完成您需要的一切。

首先,我观察到您现有的所有尝试都会发送foldr 一个无条件调用(:) 的函数:

foldr ((:) . getDbDate1) [] (filter isDbDate database)
foldr ((:) . getDbDate2) [] database

关于这种模式的事情是,这意味着您从foldr 中得到的列表将与您传入的函数具有相同的长度——因为输入列表中的每个(:) 都会变成@987654329 @ 在输出列表中。在您的第一个解决方案中,您通过从输入列表中删除一些您不关心的条目来处理此问题;在您的第二个解决方案中,您通过在输出列表中添加额外的无趣元素来处理此问题。

第三种解决方案是在决定是否调用(:) 之前查看列表元素。以下是如何做到这一点:

conditionalCons :: DatabaseItem -> [UTCTime] -> [UTCTime]
conditionalCons (DbDate t) ts = t:ts
conditionalCons _          ts =   ts

特别注意,在第二个子句中,我们没有调用(:)——这会过滤掉列表中不匹配的元素。我们也不担心缺少模式。现在我们可以写了

filterDbDate3 :: [DatabaseItem] -> [UTCTime]
filterDbDate3 = foldr conditionalCons []

在 ghci 中测试它:

> filterDbDate3 theDatabase
[1911-05-01 09:28:43 UTC,1921-05-01 09:28:43 UTC]

完美!

【讨论】:

  • 出于教学原因,我选择此作为“最佳”答案。我也喜欢@Phyx 的答案,因为它非常接近本书迄今为止所解释的内容(也迫使关注类型)。 Nikita-Volkov 也给出了一个我或多或少理解的答案,但我仍然没有来到书中的 Monad 章节(并且不知道 MonadPlus 是什么)。 Franky 的列表理解答案也很棒,读起来就像 Python ;) (但同样,我对折叠的理解不太好)。
【解决方案2】:

一个简单的列表理解就可以了

filterDbDate xs = [ x | DbDate x <- xs ]

【讨论】:

  • 如果xs 也包含DbStrings 或DbNumbers 怎么办?
  • @Code-Apprentice,这个列表理解仍然有效。
  • 列表推导只是单子计算的语法糖。当DbDate x匹配时,返回[x];否则,Monad 类中的 fail 函数将返回一个空列表。每个列表的串联产生最终列表,仅包含来自DbDate 值的xs。
【解决方案3】:

有一些很好的答案,但我想添加另一种方法来找到此类任务的解决方案。

首先,编写最简单的解决方案,即直接递归的解决方案。

filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate ((DbDate time):items) = time:(filterDbDate items)
filterDbDate ( _           :items) =       filterDbDate items

这有助于了解所涉及的结构并让您熟悉所需的实际步骤。它不是性能最高的版本,但它很容易编写,而且通常足以完成手头的任务。

下一步是使用尾递归使代码性能更高。这是一个简单的,几乎是机械的转换。

  1. 确定累加器类型。这通常也是返回类型;在这种情况下,一个列表。这为您提供了新的第一行

    filterDbDate :: [DatabaseItem] -> [UTCTime]
    filterDbDate = go []
      where ...
    
  2. 现在将原始函数转换为内部go 函数,方法是将每个递归调用替换为累加器,然后将结果放入对go 的递归调用中。

        go acc ((DbDate time):items) = go (time:acc) items
        go acc ( _           :items) = go       acc  items
    
  3. 添加对最终情况的处理。注意操作顺序会颠倒。

        go acc  []                   = reverse acc
    
  4. 将结束情况的处理移到原始调用中。如果你想在这里停下来,这不是必需的,但它有助于弃牌。

    filterDbDate = reverse . go []
      where 
        go acc  [] = acc
        ...
    

现在把它变成弃牌。累加器与折叠将使用的相同,并且转换几乎是机械的。

  1. 将调用go 替换为调用折叠。

    filterDbDate :: [DatabaseItem] -> [UTCTime]
    filterDbDate = reverse . foldl f []
    
  2. 通过删除模式匹配的列表部分、结束情况和递归调用,将 go 转换为 f

      where f acc (DbDate time) = time:acc
            f acc  _            =      acc
    
  3. 考虑是否最好将递归的方向反转。

    filterDbDate :: [DatabaseItem] -> [UTCTime]
    filterDbDate = foldr f []
      where f (DbDate time) = (time:)
            f _             = id
    

现在进行最后的清理,额外的布朗尼积分并激怒 Haskell 老师,使其尽可能通用而不破坏任何东西。

{-# LANGUAGE NoImplicitPrelude, GADTs #-}
import ClassyPrelude

filterDbDate :: ( MonoFoldable items, Element items ~ DatabaseItem
                , Monoid times, SemiSequence times, Element times ~ UTCTime)
             => items -> times
filterDbDate = foldr f mempty
   where f (DbDate time) = cons time
         f _             = id

【讨论】:

  • 另一个优秀而有启发性的答案! (虽然,现在,我对最后一段中提到的 NoImplicitPrelude、ClassPrelude 等一无所知。值得深思。深思熟虑!:)
  • @EmreSevinç 默认的前奏必须做出一些妥协。 ClassyPrelude 是几个替代方案之一,它试图为不同的用例选择一组不同的折衷方案。你总是可以不用它(例如 cons 最初来自 Data.Sequences),它不是图书馆或教学的最佳选择,但它可以方便更大的、独立的应用程序。我在这里选择它的主要原因是它的作者对实践中最常使用哪些库进行了长期而艰苦的思考,所以我可以依靠他们给我一个有用的缺点
【解决方案4】:

列表是一个单子。所以我们可以使用Monad类型类的功能。

utcTimes :: [UTCTime]
utcTimes =
  theDatabase >>=
  \ item ->
    case item of
      DbDate utcTime -> [utcTime]
      _ -> []

这里的(&gt;&gt;=) 函数是关键。它与其他语言中的“flatMap”本质上是一样的,如果这敲响了警钟的话。

以下与Do-notation中表达的内容相同:

utcTimes :: [UTCTime]
utcTimes =
  do
    item <- theDatabase
    case item of
      DbDate utcTime -> [utcTime]
      _ -> []

事实上,我们甚至可以将其推广到一个函数,它适用于UTCTime 上的任何 monad(嗯,MonadPlus,真的):

pickUTCTime :: MonadPlus m => DatabaseItem -> m UTCTime
pickUTCTime item =
  case item of
    DbDate utcTime -> return utcTime
    _ -> mzero

utcTimes :: [UTCTime]
utcTimes =
  theDatabase >>= pickUTCTime

【讨论】:

  • 这提出了一个问题,即是否有一种既定的方法可以为任意 ADT 通用派生函数,如 pickUTCTme。也许镜头库中有一些东西?
  • 你只需要makePrisms,你就会得到_DbDate;然后(^? _DbDate) :: DatabaseItem -&gt; Maybe UTCTimetoListOf (traverse . _DbDate) :: [DataBaseItem] -&gt; [UTCTime]等等。
【解决方案5】:

一个简单的方法如下

filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate db = filterDbDate' [] db
  where filterDbDate' :: [UTCTime] -> [DatabaseItem] -> [UTCTime]
        filterDbDate' rest ((DbDate utcTime):xs) = filterDbDate' (rest ++ [utcTime]) xs
        filterDbDate' rest (_:xs) = filterDbDate' rest xs
        filterDbDate' rest _      = rest

也就是说,您传递另一个参数,其中包含您要保留的值。如果您仔细观察,您会发现这正是 foldl 的类型所指示的 foldl :: Foldable t =&gt; (b -&gt; a -&gt; b) -&gt; b -&gt; t a -&gt; b(您也可以使用 foldr 来执行此操作,但我将把它留给您),除了它期望一个一次元素。所以让我们重写filterDbDate' 来做同样的事情。

filterDbDate2 :: [DatabaseItem] -> [UTCTime]
filterDbDate2 db = foldl filterDbDate'' [] db
   where filterDbDate'' :: [UTCTime] -> DatabaseItem -> [UTCTime]
         filterDbDate'' rest (DbDate utcTime) = (rest ++ [utcTime])
         filterDbDate'' rest _                = rest

这不是最有效的函数,但希望您能看到如何将函数转换为使用折叠。试试foldr

【讨论】:

    【解决方案6】:

    让我给我的 50cent,即使这个帖子太旧了! 所以我做了一个函数来检查值是否是 DbDate:

     isDbDate:: DatabaseItem -> Bool
     isDbDate (DbDate x) = True
     isDbDate  _         = False
    

    之后事情就简单了。

    文件夹解决方案

    foldr (\x start -> if isDbDate x then x:start else start)  [] theDatabase
    

    列表理解解决方案。

    [x |  x <- theDatabase, isDbDate x ]
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多