Как узнать, бесконечен ли список?

Есть ли способ узнать, бесконечен ли список в Haskell? Причина в том, что я не хочу применять такие функции, как length, к бесконечным спискам.


person alexraasch    schedule 10.09.2011    source источник
comment
Что касается вашего вопроса, не делают ли мои программы бесконечными структуры очень уязвимыми в комментариях: да и нет. С таким же успехом можно сказать и по-другому: алгоритмы, которые полагаются на конечность ваших структур, делают ваши программы очень уязвимыми. Но на самом деле, оба вполне в порядке, если обращаться с ними по отдельности. Ошибки обычно легко обнаружить с помощью простых тестовых программ: в такой программе структуры будут либо очень маленькими (например, 3 элемента =› программа должна завершиться очень быстро), либо бесконечной.   -  person leftaroundabout    schedule 10.09.2011


Ответы (6)


Применение length к неизвестным спискам, как правило, является плохой идеей, как практически из-за бесконечных списков, так и концептуально, потому что часто оказывается, что вам все равно на самом деле не нужна длина.

Вы сказали в комментарии:

Я новичок в Haskell, так что теперь, разве бесконечные структуры не делают мои программы очень уязвимыми?

Не совсем. Хотя некоторые из нас хотели бы иметь лучшие способы различать обязательно конечные и обязательно бесконечные данные, вы всегда в безопасности, когда создаете, обрабатываете и проверяете< /em> ленивые структуры инкрементно. Вычисление длины явно не инкрементальное, а проверка того, является ли длина выше или ниже некоторого порогового значения, равного, и очень часто это все, что вам нужно сделать!

Тривиальный случай — проверка непустых списков. isNonEmpty xs == length xs > 0 — плохая реализация, потому что она проверяет неограниченное количество элементов, тогда как проверки одного будет достаточно! Сравните это:

isNonEmpty [] = False
isNonEmpty (_:_) = True

Мало того, что это безопасно применять к бесконечному списку, это также намного эффективнее для конечных списков - это занимает только постоянное время, а не время, линейное по длине списка. Это также как стандарт реализована библиотечная функция null.

Чтобы обобщить это для тестирования длины относительно отсечения, вам, очевидно, потребуется изучить столько же списка, сколько и длину, с которой вы сравниваете. Мы можем сделать именно это и не более того, используя стандартную библиотечную функцию drop:

longerThan :: Int -> [a] -> Bool
longerThan n xs = isNonEmpty $ drop n xs

Учитывая длину n и (возможно, бесконечный) список xs, он отбрасывает первые n элементов xs, если они существуют, а затем проверяет, не является ли результат пустым. Поскольку drop создает пустой список, если n больше длины списка, это работает корректно для всех положительных n (увы, в стандартных библиотеках нет неотрицательных целочисленных типов, например, натуральных чисел).


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

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

person C. A. McCann    schedule 10.09.2011
comment
Вычисление длины явно не является инкрементным, но проверка того, является ли длина выше или ниже некоторого порогового значения, — чтобы было ясно, if length xs < n then ... — неправильный способ сделать это :) - person MatrixFrog; 12.09.2011

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

isInfinite :: [a] -> Bool
isInfinite ls = {- Magic! -}

Теперь мы хотим создать список impossibleList, который делает противоположное тому, что isInfinite говорит, что должен. Итак, если impossibleList бесконечно, то на самом деле это [], а если не бесконечно, то something : impossibleList.

-- using a string here so you can watch it explode in ghci
impossibleList :: [String]
impossibleList =
    case isInfinite impossibleList of
        True -> []
        False -> "loop!" : impossibleList

Попробуйте это сами в ghci с isInfinite = const True и isInfinite = const False.

person hpc    schedule 10.09.2011
comment
Нет ничего лучше, чем спор о диагонализации, чтобы навсегда все разрушить. Я виню Кантора в том, что он все это начал. - person C. A. McCann; 10.09.2011
comment
Хм, но предположим, что isInfinite были бы строгими (просто строгими, а не строгими по позвоночнику)... это было бы разумно, верно? Тогда impossibleList это просто _|_ и нет противоречия. - person luqui; 11.09.2011
comment
Разве вы не можете использовать подобное доказательство, чтобы показать, что невозможно проверить, не пуст ли список? Просто замените isInfinite на isNonEmpty. Но isNonEmpty, очевидно, можно реализовать. - person interjay; 11.09.2011
comment
@interjay прав, это просто круговое определение... использование любой строгой функции типа [a] -> Bool вместо isInfinite сделает impossibleList = ⊥. Я не уверен, что это доказывает, помимо того, что Haskell завершен по Тьюрингу: P - person Tom Crockett; 14.09.2011
comment
@luqui: Как вы думаете, что тогда означает ⊥? Кодирование логических противоречий с помощью соответствия Карри-Ховарда обычно приводит к незавершенности, и весь смысл нестрогой семантики состоит в том, чтобы не убегать от парадоксов и допускать возникновение противоречий, пока они не имеют отношения к конечному результату. - person C. A. McCann; 14.09.2011
comment
@pelotom: Разве не в этом дело? Вот что делают аргументы диагонализации. Это просто показывает, что никакая возможная реализация isInfinite не может работать со всеми списками. В нем ничего не говорится о реализациях, которые работают для некоторых входных данных, и потребуются дополнительные аргументы, чтобы показать, что в чистом коде реализация в ответе sclv является наилучшей из возможных. - person C. A. McCann; 14.09.2011
comment
Я думаю, что этот ответ правильный, этот вопрос является примером проблемы остановки (?), но приведенный пример не имеет для меня смысла, если он определяет функцию невозможного списка в терминах свойств невозможного списка - доказательство проблемы остановки показывает, КАК сделать это. Конечно, правильная аналогия состоит в том, чтобы определить isInfinite в терминах списка, определенного в терминах одного параметра и этого параметра. Затем определите absoluteList(s) = isInfinite(s,s) ? [] : бесконечный список. Затем спросите, что такоеIfinite (невозможный список, невозможный список)? Тогда у вас есть противоречие, но только с квином. - person Jack V.; 14.09.2011
comment
@С. А. Макканн: Этот код не показывает, что никакая возможная реализация isInfinite не может работать со всеми списками, потому что impossibleList не является списком (это ⊥). В противном случае вы могли бы сказать, что это также показывает, что никакая возможная реализация isNonEmpty не может работать со всеми списками, как я сказал в своем предыдущем комментарии. - person interjay; 14.09.2011
comment
@interjay: На самом деле не имеет смысла говорить, что ⊥ - это не список, это путает значения с типами. И да, единственные функции, определенные для всех входных данных, — это функции вида const x для некоторого определенного x. - person C. A. McCann; 14.09.2011
comment
@ Джек В.: Да, это более явная (и, вероятно, лучшая) кодировка. Версия ответа полагается на синтаксическую рекурсию, чтобы сделать это неявно, что несколько запутывает дело. - person C. A. McCann; 14.09.2011
comment
@pelotom: Именно так, как вы сказали ранее. Это довольно тривиальная демонстрация того, как можно доказать неразрешимость проблемы остановки. В нем ничего существенного не говорится конкретно о isInfinite, но, учитывая полноту по Тьюрингу, строить списки с расходящимися хвостами в любой точке тривиально. Вам также понадобится какой-то индуктивный аргумент, чтобы показать, что реализация sclv является наилучшей из возможных. - person C. A. McCann; 14.09.2011
comment
@С. А. Макканн: Это ничего не говорит не только о isInfinite, но и о разрешимости или проблеме остановки. Рассмотрим функции вида f::Int32->Bool. Поскольку существует конечное число возможных входных данных, все они разрешимы. Но было бы легко использовать конструкцию, почти идентичную приведенной выше, чтобы якобы доказать, что любое нетривиальное f не может быть вычислено для всех значений. Например, для f x = x>0 используйте impossibleValue = if (f impossibleValue) then 0 else 1 - person interjay; 14.09.2011
comment
@С. А. Макканн: Предпосылка состоит в том, что isInfinite определяет бесконечность данного списка, т. е. возвращает True для любого бесконечного списка и False для любого конечного списка... то, что он делает, если вы даете ему ⊥, не здесь и не там. Мы можем вывести противоречие, только передав ему конечный список и показав, что он не возвращает False (оценивается как True или ⊥), или передав ему бесконечный список и показав, что он не возвращает True (оценивается как False или ⊥). Присвоение ему ⊥ и демонстрация того, что он производит ⊥, показывает только, что isInfinite является строгим. - person Tom Crockett; 15.09.2011
comment
-1. Неправильно: проблема остановки заключается в проверке текста программы, чтобы решить, завершается она или нет. Haskell не рефлексивен, нет способа проверить определение списка (например, "[1..]"), только значение [1,2,3,...], созданное им, независимо от того, упаковано ли оно в преобразователь или нет. - person Will Ness; 31.03.2012
comment
-1 от меня тоже, у пелетома и интерджея есть вполне разумные и совершенно фатальные критические замечания. Этот аргумент ошибочен до такой степени, что вводит в заблуждение. - person Ben Millwood; 12.05.2012

Нам не нужно решать проблему остановки, чтобы безопасно вызывать 'length'. Нам просто нужно быть консервативными; принять все, что имеет доказательство конечности, отвергнуть все, что нет (включая множество конечных списков). Именно для этого и нужны системы типов, поэтому мы используем следующий тип (t — это тип нашего элемента, который мы игнорируем):

terminatingLength :: (Finite a) => a t -> Int
terminatingLength = length . toList

Класс Finite будет содержать только конечные списки, поэтому средство проверки типов гарантирует, что у нас есть конечный аргумент. принадлежность к Конечному будет нашим доказательством конечности. Функция toList просто превращает конечные значения в обычные списки Haskell:

class Finite a where
  toList :: a t -> [t]

Теперь, что наши экземпляры? Мы знаем, что пустые списки конечны, поэтому мы создаем тип данных для их представления:

-- Type-level version of "[]"
data Nil a = Nil
instance Finite Nil where
  toList Nil = []

Если мы «включаем» элемент в конечный список, мы получаем конечный список (например, «x:xs» конечен, если «xs» конечен):

-- Type-level version of ":"
data Cons v a = Cons a (v a)

-- A finite tail implies a finite Cons
instance (Finite a) => Finite (Cons a) where
  toList (Cons h t) = h : toList t -- Simple tail recursion

Любой, кто вызывает нашу функцию termatingLength, теперь должен доказать, что его список конечен, иначе его код не скомпилируется. Это не устранило проблему остановки, но мы перенесли ее на время компиляции, а не на время выполнения. Компилятор может зависнуть при попытке определить принадлежность к Finite, но это лучше, чем зависание рабочей программы, когда она получает какие-то неожиданные данные.

Небольшое предостережение: «специальный» полиморфизм Haskell позволяет объявлять в значительной степени произвольные экземпляры Finite в других точках кода, и terminatingLength примет их как доказательства конечности, даже если это не так. Это не так уж плохо; если кто-то пытается обойти механизмы безопасности вашего кода, он получает заслуженные ошибки ;)

person Warbo    schedule 19.09.2012
comment
Для вдохновения, стоящего за этим, взгляните на Conor McBride Faking It: Simulating Dependent Types in Haskell. - person Warbo; 19.09.2012

Нет - вы можете в лучшем случае оценить. См. Проблему остановки.

person Lars    schedule 10.09.2011
comment
Хорошо, это грустно. Не понимал, что это был пример проблемы остановки. - person alexraasch; 10.09.2011
comment
ну, это вот-вот скажет, если ваша программа будет работать бесконечно - что было бы верно для бесконечного списка. Может быть, у кого-то есть лучший ответ для вас. - person Lars; 10.09.2011
comment
Действительно? Есть ли у вас сокращение от проблемы остановки? Как вы утверждаете, что эта проблема так же сложна, как и проблема остановки? (Конечно, есть очевидное сокращение к проблеме остановки — вы можете рассматривать это как пример проблемы остановки — но это неправильное направление и ничего не значит.) Вам нужно знать, что если вы можете проверить, бесконечен ли список, тогда вы также можете решить проблему остановки. - person ShreevatsaR; 10.09.2011
comment
ну, вы можете создать список, который возвращает текущий элемент на ленте в машине Тьюринга, на которую указывает голова. если вы можете сказать, является ли список конечным, вы можете решить проблему остановки. - person Karoly Horvath; 10.09.2011
comment
@yi_H: значит, ваша модель заключается в том, что список передается программе Haskell оракулом? (В этом случае вам даже не нужно обращаться к проблеме остановки; вы можете просто представить себе противника, который ждет, пока программа Haskell объявит вывод, и решит, что список имеет противоположный тип.) - person ShreevatsaR; 10.09.2011
comment
Я не понимаю, что вы подразумеваете под списком, который возвращает элемент. Список ничего не возвращает. - person alexraasch; 10.09.2011
comment
@ Александр Рааш: Нет Оракула. Вы создаете симулятор машины Тьюринга на Haskell, и он предоставляет список, показывающий, как движется голова. Конец списка: программа завершена. - person Karoly Horvath; 10.09.2011
comment
Ах хорошо. Я новичок в Haskell, так что теперь, разве бесконечные структуры не делают мои программы очень уязвимыми? - person alexraasch; 10.09.2011
comment
@ShreevatsaR: возьмите произвольную общую рекурсивную функцию, перепишите ее как операцию развертывания, которая создает список последовательных значений, взятых на каждом рекурсивном шаге, и одноэлементный список при достижении базового случая. Список будет конечным, если и только если исходная функция завершается, но произвольные общерекурсивные функции — это в точности те, которые вычисляются машиной Тьюринга. Таким образом, функция isFinite, которая работает с произвольными списками, обязательно является останавливающимся оракулом. - person C. A. McCann; 10.09.2011

Существует также возможность разделения конечных и бесконечных списков по дизайну и использования для них разных типов.

К сожалению, Haskell (в отличие, например, от Agda) не позволяет структура данных всегда конечна, вы можете использовать методы полного функционального программирования, чтобы гарантировать это.

И вы можете объявить бесконечные списки (потоки AKA) как

data Stream a = Stream a (Stream a)

у которого нет способа завершить последовательность (в основном это список без []).

person Petr    schedule 23.05.2014

person    schedule
comment
Я не понимаю, с этим определением isInfinite [1,2,3]дает False, а isInfinite [1..] также дает False. - person alexraasch; 11.09.2011
comment
@alexraasch: Нет, seq там гарантирует, что он вычисляет длину, что, конечно, никогда не закончится, если список бесконечен. Таким образом, он либо дает False, если список конечен (что правильно), либо никогда не завершается и, следовательно, вообще никогда не отвечает. А ответ, которого не существует, нельзя назвать неправильным, не так ли? :] И да, это шутка, если это не очевидно. - person C. A. McCann; 12.09.2011