如何从Haskell中的列表中仅获取特定类型的元素?

时间:2016-08-06 14:44:06

标签: haskell

我正在研究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值列表。

所以我的问题是:如何编写一个(helper?)函数,一次性(这样我不必使用filter),检查它的值是否为{{1} },如果是,则返回DbNumber组件? (如果不是......它也必须返回一些东西(例如UTCTime?),这就是我遇到麻烦的地方,即使用Nothing,然后获取Maybe UTCTime价值等。)

5 个答案:

答案 0 :(得分:11)

此处还有其他几个答案,其中包含有关其他方法来思考问题的好建议:在选择catMaybes后,使用Maybe UTCTime再次进行数据处理;使用列表推导和他们用来过滤掉不匹配模式的方便语法;使用列表的monadic结构来包含或跳过结果;并编写一个定制的递归函数。在这个答案中,我将解决你的直接问题,展示如何使用你已经拥有的程序结构,而不必完全重新思考列表操作的方法 - 使用辅助函数调用foldr,它可以一次完成所需的一切。

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

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

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

第三种解决方案是在决定是否调用(:)之前查看list元素。人们可以这样做:

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]

完美!

答案 1 :(得分:8)

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

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

答案 2 :(得分: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
        ...
    
  5. 现在将其变为折叠。累加器与折叠将使用的相同,转换也几乎是机械的。

    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
      
    4. 现在进行最后的清理工作,额外的布朗尼点并激怒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
      

答案 3 :(得分:2)

列表是monad。所以我们可以使用Monad类型类的函数。

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

这里的(>>=)功能是关键。它与其他语言中的“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

答案 4 :(得分:1)

一种简单的方法如下

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 => (b -> a -> b) -> b -> t a -> 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