В Haskell takeWhile позволяет брать записи из (потенциально бесконечного) списка до тех пор, пока не будет выполнено определенное условие.

Однако это условие не может зависеть от предыдущих записей в списке.

Как мне take записей из (потенциально бесконечного) списка, пока я не встречу первый дубликат, как показано в этом примере?

*Main> takeUntilDuplicate [1,2,3,4,5,1,2,3,4]
[1,2,3,4,5]
3
Uli Köhler 27 Фев 2015 в 03:39

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]

И что приятно, так это то, что некоторые из этих фрагментов кода можно повторно использовать для решения аналогичных задач.

9
Luis Casillas 27 Фев 2015 в 19:15

Вы можете использовать модифицированную версию этой функции удаления дубликатов:

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
2
Community 23 Май 2017 в 11:45

Ваша точка зрения, что 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 доставляет с помощью списков снок-списков (списков, которые растут справа), лучше сохранить совместное использование и визуальное отображение слева направо.

4
Community 23 Май 2017 в 12:24

Предполагая, что вы имеете дело с экземплярами 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.

5
Community 23 Май 2017 в 12:30