Почему эта первая функция Haskell FAIL обрабатывает бесконечные списки, а второй фрагмент SUCCEEDS с бесконечными списками?

У меня есть две функции Haskell, оба из которых кажутся мне очень похожими. Но первый ОТКАЗЫВАЕТСЯ против бесконечных списков, а второй - УНИЧТОЖЕТ против бесконечных списков. Я уже много часов пытаюсь понять, почему это так, но безрезультатно.

Оба фрагмента являются повторной реализацией функции "слов" в Prelude. Оба отлично работают с конечными списками.

Здесь версия, которая НЕ обрабатывает бесконечные списки:

myWords_FailsOnInfiniteList :: String -> [String]
myWords_FailsOnInfiniteList string = foldr step [] (dropWhile charIsSpace string)
   where 
      step space ([]:xs)      | charIsSpace space = []:xs    
      step space (x:xs)       | charIsSpace space = []:x:xs
      step space []           | charIsSpace space = []
      step char (x:xs)                            = (char : x) : xs
      step char []                                = [[char]] 

Здесь версия, которая обрабатывает бесконечные списки:

myWords_anotherReader :: String -> [String]
myWords_anotherReader xs = foldr step [""] xs
   where 
      step x result | not . charIsSpace $ x = [x:(head result)]++tail result
                    | otherwise             = []:result

Примечание: "charIsSpace" является просто переименованием Char.isSpace.

Следующий сеанс интерпретатора показывает, что первый из них терпит неудачу в отношении бесконечного списка, а второй - успешно.

*Main> take 5 (myWords_FailsOnInfiniteList  (cycle "why "))
*** Exception: stack overflow

*Main> take 5 (myWords_anotherReader (cycle "why "))
["why","why","why","why","why"]

РЕДАКТОР: Благодаря ответам ниже, я считаю, что понимаю сейчас. Вот мои выводы и пересмотренный код:

Выводы:

  • Самым большим виновником в моей первой попытке были 2 уравнения, которые начинались с "шагового пространства []" и "step char []". Согласование второго параметра шаговой функции с [] является no-no, поскольку он заставляет весь 2-й аргумент оцениваться (но с пояснением ниже).
  • В какой-то момент я подумал, что (++) может оценить его правомерный аргумент позже, чем против, каким-то образом. Итак, я подумал, что могу исправить проблему, изменив "= (char: x): xs" to "= [ char: x] ++ xs". Но это было неверно.
  • В какой-то момент я подумал, что шаблон, сопоставляющий второй аргумент arg (x: xs), приведет к сбою функции против бесконечных списков. Я был почти прав, но не совсем! Оценивая второй аргумент arg (x: xs), как и в предыдущем примере шаблона, вы получите некоторую рекурсию. Он "повернет кривошип" до тех пор, пока не ударит ":" (иначе, "минусы" ). Если бы этого не произошло, то моя функция не удалась бы против бесконечного списка. Однако в этом конкретном случае все в порядке, потому что моя функция в конечном итоге столкнется с пробелом, после чего произойдет "минус". И оценка, вызванная сопоставлением с (x: xs), остановится прямо там, избегая бесконечной рекурсии. В этот момент "x" будет сопоставлен, но xs останется тиком, поэтому проблем нет. (Спасибо Ганешу за то, что помогли мне понять это).
  • В общем, вы можете указать второй аргумент, который вам нужен, если вы не оцениваете его силу. Если вы сопоставлены с x: xs, вы можете указать xs все, что хотите, если вы не принудительно оцениваете его.

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

myWords :: String -> [String]
myWords string = foldr step [""] (dropWhile charIsSpace string)
   where 
      step space acc | charIsSpace space = "":acc
      step char (x:xs)                   = (char:x):xs
      step _ []                          = error "this should be impossible"

Это правильно работает против бесконечных списков. Обратите внимание, что в поле зрения нет оператора head, tail или (++).

Теперь, для важной оговорки: Когда я впервые написал исправленный код, у меня не было третьего уравнения, которое соответствует "step _ []". В результате я получил предупреждение о не исчерпывающих совпадениях шаблонов. Очевидно, что это хорошая идея, чтобы избежать этого предупреждения.

Но я думал, что у меня будет проблема. Я уже упоминал выше, что это не нормально, чтобы сопоставить второй аргумент против []. Но я должен был бы сделать это, чтобы избавиться от предупреждения.

Однако, когда я добавил уравнение "step_ []", все было в порядке! Не было никаких проблем с бесконечными списками!. Зачем?

Поскольку третье уравнение в исправленном коде НИКОГДА НЕ ДОСТИГЕТ!

На самом деле рассмотрим следующую версию BROKEN. Это ТОЧНО ТО ЖЕ, как правильный код, за исключением того, что я переместил шаблон для пустого списка выше других шаблонов:

myWords_brokenAgain :: String -> [String]
myWords_brokenAgain string = foldr step [""] (dropWhile charIsSpace string)
   where 
      step _ []                              = error "this should be impossible"
      step space acc | charIsSpace space     = "":acc
      step char (x:xs)                       = (char:x):xs

Мы вернемся к переполнению стека, потому что первое, что происходит при вызове шага, - это то, что интерпретатор проверяет, соответствует ли уравнение номер один. Чтобы сделать это, он должен увидеть, является ли второй arg []. Для этого он должен оценить второй аргумент.

Перемещение уравнения вниз НИЖЕ других уравнений гарантирует, что третье уравнение никогда не будет пытаться, потому что всегда совпадает либо первый, либо второй паттерн. Третье уравнение просто состоит в том, чтобы отказаться от предупреждения об отсутствии исчерпывающего шаблона.

Это был отличный опыт обучения. Спасибо всем за вашу помощь.

Ответ 1

Другие указали на проблему, которая заключается в том, что этот шаг всегда оценивает свой второй аргумент, прежде чем производить какой-либо вывод вообще, но его второй аргумент будет в конечном счете зависеть от результата другого вызова шага, когда foldr применяется к бесконечному списку.

Это не нужно писать таким образом, но ваша вторая версия выглядит отвратительно, потому что он полагается на исходный аргумент на шаг, имеющий конкретный формат, и довольно сложно понять, что голова/хвост никогда не пойдет не так, (Я даже не уверен на 100%, что они не будут!)

Что вам нужно сделать, это перестроить первую версию, чтобы она выдавала выходные данные, в зависимости от списка ввода, по крайней мере, в некоторых ситуациях. В частности, мы видим, что, когда символ не является пространством, всегда есть как минимум один элемент в выходном списке. Поэтому задержите соответствие шаблонов во втором аргументе до получения этого первого элемента. Случай, когда символ является пространством, будет по-прежнему зависеть от списка, но это прекрасно, потому что единственный способ, которым этот случай может бесконечно рекурсивно, - это пройти бесконечный список пространств, и в этом случае не производить какой-либо вывод и идти в цикл - ожидаемое поведение для слов (что еще он может сделать?)

Ответ 2

Попробуйте развернуть выражение вручную:

 take 5 (myWords_FailsOnInfiniteList  (cycle "why "))
 take 5 (foldr step [] (dropWhile charIsSpace (cycle "why ")))
 take 5 (foldr step [] (dropWhile charIsSpace ("why " ++ cycle "why ")))
 take 5 (foldr step [] ("why " ++ cycle "why "))
 take 5 (step 'w' (foldr step [] ("hy " ++ cycle "why ")))
 take 5 (step 'w' (step 'h' (foldr step [] ("y " ++ cycle "why "))))

Какое следующее расширение? Вы должны увидеть, что для соответствия шаблону для step вам нужно знать, является ли он пустым списком или нет. Чтобы это выяснить, вы должны оценить его, по крайней мере, немного. Но этот второй термин является сокращением foldr самой функцией, для которой вы используете шаблон. Другими словами, функция шага не может смотреть на свои аргументы, не вызывая себя, и поэтому у вас есть бесконечная рекурсия.

Контрастируйте это с расширением вашей второй функции:

myWords_anotherReader (cycle "why ")
foldr step [""] (cycle "why ")
foldr step [""] ("why " ++ cycle "why ")
step 'w' (foldr step [""] ("hy " ++ cycle "why ")
let result = foldr step [""] ("hy " ++ cycle "why ") in
    ['w':(head result)] ++ tail result
let result = step 'h' (foldr step [""] ("y " ++ cycle "why ") in
    ['w':(head result)] ++ tail result

Вероятно, вы увидите, что это расширение будет продолжаться до тех пор, пока не будет достигнуто пространство. Как только пространство будет достигнуто, "результат головы" получит значение, и вы получите первый элемент ответа.

Я подозреваю, что эта вторая функция будет переполняться для бесконечных строк, которые не содержат пробелов. Вы можете понять, почему?

Ответ 3

Вторая версия фактически не оценивает result до тех пор, пока не приступит к разработке части своего собственного ответа. Первая версия оценивает result сразу по шаблону.

Ключ с этими бесконечными списками состоит в том, что вам нужно что-то выпустить, прежде чем вы начнете требовать элементы списка, чтобы выход всегда "оставался впереди" ввода.

(Я чувствую, что это объяснение не очень понятно, но это лучшее, что я могу сделать.)

Ответ 4

Функция библиотеки foldr имеет эту реализацию (или аналогичную):

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f k (x:xs) = f x (foldr f k xs)
foldr _ k _ = k

Результат myWords_FailsOnInfiniteList зависит от результата foldr, который зависит от результата step, который зависит от результата внутреннего foldr, который зависит от... и так далее от бесконечного списка, myWords_FailsOnInfiniteList будет использовать бесконечное количество пространства и времени перед созданием своего первого слова.

Функция step в myWords_anotherReader не требует результата внутренней foldr до тех пор, пока она не произведет первую букву первого слова. К сожалению, как говорит Apocalisp, он использует пространство O (длина первого слова) до того, как оно произведет следующее слово, потому что при создании первого слова хвостовой кусок продолжает расти tail ([...] ++ tail ([...] ++ tail (...))).


В отличие от сравнения с

myWords :: String -> [String]
myWords = myWords' . dropWhile isSpace where
    myWords' [] = []
    myWords' string =
        let (part1, part2) = break isSpace string
        in part1 : myWords part2

используя библиотечные функции, которые могут быть определены как

break :: (a -> Bool) -> [a] -> ([a], [a])
break p = span $ not . p

span :: (a -> Bool) -> [a] -> ([a], [a])
span p xs = (takeWhile p xs, dropWhile p xs)

takeWhile :: (a -> Bool) -> [a] -> [a]
takeWhile p (x:xs) | p x = x : takeWhile p xs
takeWhile _ _ = []

dropWhile :: (a -> Bool) -> [a] -> [a]
dropWhile p (x:xs) | p x = dropWhile p xs
dropWhile _ xs = xs

Обратите внимание, что получение промежуточных результатов никогда не задерживается будущими вычислениями, и требуется только пространство O (1), поскольку каждый элемент результата становится доступным для потребления.


Добавление

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

myWords :: String -> [String]
myWords string = foldr step [""] (dropWhile charIsSpace string)
   where 
      step space acc | charIsSpace space = "":acc
      step char (x:xs)                   = (char:x):xs
      step _ []                          = error "this should be impossible"

(Помимо этого: вам может быть все равно, но words "" == [] из библиотеки, но ваш myWords "" = [""]. Аналогичная проблема с конечными пробелами.)

Выглядит намного лучше, чем myWords_anotherReader, и довольно хорошо подходит для решения на основе foldr.

\n -> tail $ myWords $ replicate n 'a' ++ " b"

Это невозможно сделать лучше, чем время O (n), но здесь myWords_anotherReader и myWords берут O (n). Это может быть неизбежно при использовании foldr.

Хуже того,

\n -> head $ head $ myWords $ replicate n 'a' ++ " b"

myWords_anotherReader был O (1), но новый myWords равен O (n), так как соответствие шаблону (x:xs) требует дальнейшего результата.

Вы можете обойти это с помощью

myWords :: String -> [String]
myWords = foldr step [""] . dropWhile isSpace
   where 
      step space acc | isSpace space = "":acc
      step char ~(x:xs)              = (char:x):xs

~ вводит "неопровержимый рисунок". Необычные шаблоны никогда не терпят неудачу и не заставляют немедленную оценку.