在Haskell中,takeWhile
允许用户从(可能是无限的)列表中获取条目,直到某个条件不成立为止。
但是,此条件不能取决于列表的先前条目。
如果我遇到第一个副本(如本例中所述),我怎样take
来自(可能是无限的)列表中的条目?
*Main> takeUntilDuplicate [1,2,3,4,5,1,2,3,4]
[1,2,3,4,5]
答案 0 :(得分:8)
解决此问题的一种方法是在遍历列表时更新一段状态,类似于您在命令式语言中所做的操作。这需要与State
monad一起工作,这可能需要一些学习和玩耍来获得它,如果这是你第一次,但相信我,这是值得学习的。让我们从导入开始:
import Control.Monad.State
import Data.Set (Set)
import qualified Data.Set as Set
我们要保留的状态是在列表中看到的那些元素Set
。首先,让我们编写一对简单的State
动作来管理一组看到的元素:
-- Add an element to the context Set
remember :: Ord a => a -> State (Set a) ()
remember a = modify (Set.insert a)
-- Test if the context set contains an element
haveSeen :: Ord a => a -> State (Set a) Bool
haveSeen a = do seen <- get
return (a `Set.member` seen)
现在我们要将这两者合并为一个检查重复的动作:
isDuplicate :: Ord a => a -> State (Set a) Bool
isDuplicate a = do seen <- haveSeen a
remember a
return seen
您已经提到了takeWhile
功能。我们将按照类似的方针构建我们的解决方案。这是takeWhile
的定义:
-- different name to avoid collision
takeWhile' :: (a -> Bool) -> [a] -> [a]
takeWhile' _ [] = []
takeWhile' p (a:as)
| p a = a : takeWhile p as
| otherwise = []
我们可以修改此函数以使用包含在monad中的Bool
的谓词:
takeWhileM :: Monad m => (a -> m Bool) -> [a] -> m [a]
takeWhileM _ [] = return []
takeWhileM p (a:as) =
do test <- p a
if test
then do rest <- takeWhileM p as
return (a:rest)
else return []
但这里的关键区别是因为takeWhileM
中的测试是monadic,我们可以使用上面的有状态isDuplicate
。因此,每次我们使用isDuplicate
测试列表的元素时,我们还会在Set
中记录该元素,该元素正在通过计算。所以现在我们可以这样写takeUntilDuplicate
:
takeUntilDuplicate :: Ord a => [a] -> [a]
takeUntilDuplicate as = evalState (takeUntilDuplicate' as) Set.empty
where takeUntilDuplicate' :: Ord a => [a] -> State (Set a) [a]
takeUntilDuplicate' = takeWhileM (fmap not . isDuplicate)
使用示例(带有无限列表参数):
>>> takeUntilDuplicate (cycle [1..5])
[1,2,3,4,5]
而且很简单的是,这些代码中的一些可以重用于类似的问题。
答案 1 :(得分:5)
假设您正在处理Ord
个实例,您可以这样做。这与Luis Casillas's answer基本相同,但使用折叠代替State
表示。我们的每个答案都使用了不同的普遍适用的技术。路易斯包括对他的一个很好的解释;我的经典解释是格雷厄姆赫顿的“折叠的普遍性和表现力的教程”。
import Data.Set (member)
import qualified Data.Set as S
takeUntilDuplicate :: Ord a => [a] -> [a]
takeUntilDuplicate xs = foldr go (const []) xs S.empty
where
go x cont set
| x `member` set = []
| otherwise = x : cont (S.insert x set)
如果您实际上正在处理Int
(或任何可以快速转换为Int
的内容),您可以使用Data.IntSet
或{{1}大幅改善效果而不是Data.HashSet
。
答案 2 :(得分:4)
您的观点takeWhile
不起作用,因为您没有针对各个元素的上下文信息,这表明了以下策略:获取它。
This我的回答引用了decorate-with-context操作,我称之为picks
(因为它向您展示了选择要聚焦的一个元素的所有方法)。这是我们应该为每个容器事物免费提供的一般装饰与其上下文操作。对于列表,它是
picks :: [x] -> [(x, ([x], [x]))] -- [(x-here, ([x-before], [x-after]))]
picks [] = []
picks (x : xs) = (x, ([], xs)) : [(y, (x : ys, zs)) | (y, (ys, zs)) <- picks xs]
它适用于无限列表,而我们就是这样。
现在有了
takeUntilDuplicate :: Eq x => [x] -> [x]
takeUntilDuplicate = map fst . takeWhile (\ (x, (ys, _)) -> not (elem x ys)) . picks
(奇怪的是,如果没有给出上述类型的签名,我会因为Eq
的歧义而拒绝上述单行。我很想在这里问一个关于它的问题。哦,这是单态限制。多么烦人。)
备注。 picks
使用snoc-lists(右侧增长的列表)代表{{1}}提供的“元素之前”组件是很有意义的。 ),更好地保持共享和视觉从左到右。
答案 3 :(得分:2)
您可以使用this duplicate removal function的修改版本:
takeUntilDuplicate :: Eq a => [a] -> [a]
takeUntilDuplicate = helper []
where helper seen [] = seen
helper seen (x:xs)
| x `elem` seen = seen
| otherwise = helper (seen ++ [x]) xs
请注意,对于大型列表,elem
效率非常低。假设a
(列表中的数据类型)是Ord
类型,可以使用Data.Set
进行成员资格查找来改进此功能:
import qualified Data.Set as Set
takeUntilDuplicate' :: (Eq a, Ord a) => [a] -> [a]
takeUntilDuplicate' = helper Set.empty []
where helper seenSet seen [] = seen
helper seenSet seen (x:xs)
| x `Set.member` seenSet = seen
| otherwise = helper (Set.insert x seenSet) (seen ++ [x]) xs
如果您不关心返回结果的顺序,可以通过返回Set
来进一步提高函数的效率:
导入限定数据。设置为Set import Data.Set(Set)
takeUntilDuplicateSet :: (Eq a, Ord a) => [a] -> Set a
takeUntilDuplicateSet = helper Set.empty
where helper seenSet [] = seenSet
helper seenSet (x:xs)
| x `Set.member` seenSet = seenSet
| otherwise = helper (Set.insert x seenSet) xs