В Haskell takeWhile
позволяет брать записи из (потенциально бесконечного) списка до тех пор, пока не будет выполнено определенное условие.
Однако это условие не может зависеть от предыдущих записей в списке.
Как мне take
записей из (потенциально бесконечного) списка, пока я не встречу первый дубликат, как показано в этом примере?
*Main> takeUntilDuplicate [1,2,3,4,5,1,2,3,4]
[1,2,3,4,5]
4 ответа
Один из способов сделать это - обновить часть состояния при просмотре списка, аналогично тому, что вы делаете в императивном языке. Это требует работы с монадой State
, что может потребовать некоторого изучения и экспериментов, чтобы получить ее, если вы это делаете впервые, но поверьте мне, это стоит изучить. Начнем с импорта:
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 = []
Мы можем изменить эту функцию для работы с предикатом, в котором 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
является монадическим, мы можем использовать наш 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]
И что приятно, так это то, что некоторые из этих фрагментов кода можно повторно использовать для решения аналогичных задач.
Вы можете использовать модифицированную версию этой функции удаления дубликатов:
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
:
Import Qualified Data.Set as Установить импорт Data.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
Ваша точка зрения, что takeWhile
не работает, потому что у вас нет контекстной информации для отдельных элементов, предлагает следующую стратегию: получить ее.
В этом моем ответе упоминается операция декорации в контексте, которую я назвал 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
доставляет с помощью списков снок-списков (списков, которые растут справа), лучше сохранить совместное использование и визуальное отображение слева направо.
Предполагая, что вы имеете дело с экземплярами Ord
, вы можете сделать это следующим образом. По сути, это та же идея, что и ответ Луиса Касильяса, но выраженная с помощью складки вместо 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
или Data.HashSet
вместо Data.Set
.
Похожие вопросы
Связанные вопросы
Новые вопросы
haskell
Haskell — это чисто функциональный язык программирования со строгой статической типизацией, отложенными вычислениями, обширной поддержкой параллелизма и параллелизма, а также уникальными возможностями абстракции.