Почему эта первая функция Haskell FAIL обрабатывает бесконечные списки, в то время как этот второй фрагмент УСПЕШНО с бесконечными списками? - PullRequest
11 голосов
/ 12 мая 2009

У меня есть две функции Haskell, обе из которых кажутся мне очень похожими. Но первый не справляется с бесконечными списками, а второй - с бесконечными списками. Я часами пытался понять, почему это так, но безрезультатно.

Оба фрагмента представляют собой повторную реализацию функции "words" в 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"]

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

Выводы:

  1. Самым большим виновником в моей первой попытке были 2 уравнения, которые начинались с "step space []" и "step char []". Сопоставление второго параметра функции шага с [] - нет-нет , поскольку оно заставляет оценивать весь 2-й аргумент (но с оговоркой, поясняемой ниже).
  2. В какой-то момент я думал, что (++) может оценить свой правый аргумент позже, чем минусы, так или иначе. Итак, я подумал, что мог бы решить проблему, изменив "= (char: x): xs" на "= [char: x] ++ xs". Но это было неверно .
  3. В какой-то момент я подумал, что шаблон, соответствующий второму аргументу против (x: xs), вызовет сбой функции для бесконечных списков. Я был почти прямо об этом, но не совсем! Оценивая второй аргумент против (x: xs), как я делал в приведенном выше сопоставлении с шаблоном, WILL вызывает некоторую рекурсию. Он будет «поворачивать рукоятку», пока не достигнет «:» (иначе, «минусы»). Если этого никогда не произойдет, моя функция не будет работать с бесконечным списком. Тем не менее, , в данном конкретном случае , все в порядке, потому что моя функция в конечном итоге столкнется с пробелом, и в этот момент произойдут "минусы". И оценка, вызванная сопоставлением с (x: xs), остановится прямо здесь, избегая бесконечной рекурсии. В этот момент, «х» будет соответствовать, но х останется символом, так что нет проблем. (Спасибо Ганешу за то, что он действительно помог мне понять это).
  4. В общем, вы можете упомянуть второй аргумент, сколько захотите, если только вы не принудительно оцените его . Если вы сравнили с 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"

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

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

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

Однако, когда я добавил уравнение "step _ []", все было хорошо! По-прежнему не было проблем с бесконечными списками! . Зачем?

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

На самом деле, рассмотрите следующую версию 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

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

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

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

Ответы [ 4 ]

7 голосов
/ 12 мая 2009

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

 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

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

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

7 голосов
/ 12 мая 2009

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

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

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

3 голосов
/ 12 мая 2009

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

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

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

1 голос
/ 12 мая 2009

Функция библиотеки 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

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

...