我正在研究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
价值等。)
答案 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
这有助于理解所涉及的结构,并使您熟悉所需的实际步骤。它不是最高性能的版本,但它易于编写,而且通常足以完成手头的任务。
下一步是使用尾递归使代码更高效。这是一个简单的,几乎是机械的转变。
确定累加器类型。这通常也是返回类型;在这种情况下,列表。这为你提供了新的第一行
filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate = go []
where ...
现在使用原始函数并将其转换为内部go
函数,方法是将每个递归调用替换为累加器,然后将结果放入对go
的递归调用中。
go acc ((DbDate time):items) = go (time:acc) items
go acc ( _ :items) = go acc items
添加最终案例的处理。请注意操作顺序将颠倒过来。
go acc [] = reverse acc
将结束案例的处理移动到原始呼叫中。如果你想停在这里,这不是必要的,但它有助于前往折叠。
filterDbDate = reverse . go []
where
go acc [] = acc
...
现在将其变为折叠。累加器与折叠将使用的相同,转换也几乎是机械的。
通过调用折叠来取代对go
的通话。
filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate = reverse . foldl f []
通过删除模式匹配,结束案例和递归调用的列表部分,将go
转换为f
。
where f acc (DbDate time) = time:acc
f acc _ = acc
考虑是否更好地扭转递归的方向。
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
答案 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
!