Анализ завершения
def f(n):
while n > 1:
if n % 2 == 0:
n = n / 2
else:
n = 3 * n + 1
|
| По состоянию на 2025 год, всё ещё неизвестно, завершается ли эта программа на языке Питон для любого входного значения; см. Гипотеза Коллатца. |
Анализ завершения — это разновидность анализа программ, целью которого является определение, завершится ли выполнение данной программы для каждого входного значения (вычисляет ли программа тотальную функцию). Тесно связан с задачей об остановке, которая заключается в определении, завершится ли выполнение программы для конкретного входа, и которая является неразрешимой. В модели машин Тьюринга как модели программ, реализующих вычислимые функции, задача анализа завершения сводится к определению, является ли данная машина Тьюринга тотальной машиной Тьюринга. Эта задача находится на уровне арифметической иерархии и, следовательно, строго сложнее задачи об остановке. Поскольку вопрос о тотальности вычислимой функции не является полуразрешимым[1], любой корректный анализатор завершения (то есть такой, который никогда не даёт положительного ответа для незавершающейся программы) будет неполным, то есть не сможет определить завершение для бесконечного множества завершающихся программ — либо будет работать бесконечно долго, либо завершится с неопределённым ответом.
Доказательство завершения
Доказательство завершения — это разновидность математического доказательства, играющая важную роль в формальной верификации, поскольку тотальная корректность алгоритма зависит от его завершения.
Простой и общий метод построения доказательств завершения заключается в сопоставлении мера каждому шагу алгоритма. Мера выбирается из области фундированного отношения, например, из ординальных чисел. Если мера уменьшается по этому отношению на каждом возможном шаге алгоритма, то алгоритм обязательно завершится, так как не существует бесконечно убывающих цепей относительно фундированного отношения.
Некоторые методы анализа завершения могут автоматически строить или подразумевать существование доказательства завершения.
Пример
Примером конструкции языка программирования, которая может завершаться или не завершаться, является цикл, поскольку он может выполняться многократно. Циклы, реализованные с помощью переменной-счётчика, как это обычно бывает в алгоритмах обработки данных, обычно завершаются, что демонстрируется на следующем примере псевдокода:
i := 0
цикл пока i ≠ РАЗМЕР_ДАННЫХ
process_data(data[i]) // обработать фрагмент данных с индексом i
i := i + 1 // перейти к следующему фрагменту данных
Если значение РАЗМЕР_ДАННЫХ неотрицательно, фиксировано и конечно, цикл в конечном итоге завершится, при условии, что process_data также завершается.
Некоторые циклы можно показать как всегда завершающиеся или никогда не завершающиеся при ручном анализе. Например, следующий цикл теоретически никогда не завершится. Однако на реальной машине он может завершиться из-за арифметического переполнения: либо произойдёт исключение, либо счётчик перейдёт в отрицательное значение, что позволит выполнить условие завершения цикла.
i := 1
цикл пока i ≠ 0
i := i + 1
В анализе завершения также может ставиться задача определить поведение программы относительно завершения в зависимости от неизвестного входного значения. Следующий пример иллюстрирует эту проблему.
i := 1
цикл пока i ≠ НЕИЗВЕСТНО
i := i + 1
Условие выхода из цикла определяется некоторым значением НЕИЗВЕСТНО, которое заранее неизвестно (например, задаётся пользователем при запуске программы). В этом случае анализ завершения должен учитывать все возможные значения НЕИЗВЕСТНО и установить, что, например, при НЕИЗВЕСТНО = 0 (как в исходном примере) завершение цикла не может быть гарантировано.
Однако не существует общего метода для определения, завершится ли выражение, содержащее циклические инструкции, даже при ручном анализе. Теоретической причиной этого является неразрешимость задачи об остановке: не существует алгоритма, который определяет, завершится ли любая произвольная программа за конечное число шагов вычисления.
На практике не удаётся доказать завершение (или незавершение) программы, поскольку любой алгоритм работает с конечным набором методов, способных извлекать релевантную информацию из программы. Один из методов может анализировать, как изменяются переменные относительно условия цикла (возможно, доказывая завершение для этого цикла), другие методы могут пытаться преобразовать вычисления программы в некоторую математическую конструкцию и анализировать её, получая информацию о завершении из свойств этой модели. Но поскольку каждый метод способен «видеть» только определённые причины (не)завершения, даже их комбинация не может охватить все возможные случаи.
Рекурсивные функции и циклы эквивалентны по выразительной мощности: любое выражение с циклами можно записать с помощью рекурсии, и наоборот. Поэтому завершение рекурсивных выражений также в общем случае неразрешимо. Большинство рекурсивных выражений, встречающихся на практике (то есть не патологические), можно доказать завершающимися различными способами, обычно исходя из определения самой функции. Например, аргумент функции в рекурсивном определении факториала ниже всегда уменьшается на 1; по свойству хорошей упорядоченности натуральных чисел аргумент в итоге достигнет 1, и рекурсия завершится.
функция факториал (аргумент как натуральное число)
если аргумент = 0 или аргумент = 1
вернуть 1
иначе
вернуть аргумент * факториал(аргумент - 1)
Зависимые типы
Проверка завершения имеет большое значение в языках программирования с зависимыми типами и в системах автоматического доказательства теорем, таких как Rocq и Agda. Эти системы используют изоморфизм Карри — Ховарда между программами и доказательствами. Доказательства над индуктивно определёнными типами данных традиционно строились с помощью индуктивных принципов. Однако позже было обнаружено, что описание программы через рекурсивную функцию с сопоставлением с образцом является более естественным способом доказательства, чем непосредственное использование индуктивных принципов. Разрешение незавершающихся определений приводит к логической противоречивости в теориях типов, поэтому в Agda и Rocq встроены средства проверки завершения.
Типы с размерами
Одним из подходов к проверке завершения в языках с зависимыми типами являются типы с размерами. Основная идея заключается в аннотировании типов, по которым возможна рекурсия, размерами, и разрешении рекурсивных вызовов только на меньших аргументах. Типы с размерами реализованы в Agda как синтаксическое расширение.
Современные исследования
Существует несколько исследовательских групп, разрабатывающих новые методы доказательства (не)завершения. Исследователи внедряют эти методы в программы[2], которые пытаются анализировать поведение программ относительно завершения автоматически (то есть без участия человека). Одним из направлений исследований является адаптация существующих методов для анализа завершения программ, написанных на «реальных» языках программирования. Для декларативных языков, таких как Haskell , Mercury и Prolog, существует множество результатов[3][4][5] (в основном благодаря сильной математической базе этих языков). Ведётся разработка новых методов анализа завершения программ, написанных на императивных языках, таких как C и Java.
Примечания
Литература
- Christoph Walther. Proc. 9th Conference on Automated Deduction. — Springer, 1988. — Vol. 310. — P. 602–621.
- Christoph Walther (1991). “On Proving the Termination of Algorithms by Machine”. Artificial Intelligence. 70 (1).
- Xi, Hongwei. Rewriting Techniques and Applications, 9th Int. Conf., RTA-98 / Tobias Nipkow. — Springer, 1998. — Vol. 1379. — P. 271–285.
- Jürgen Giesl. Automated Deduction - A Basis for Applications / Jürgen Giesl, Christoph Walther, Jürgen Brauburger. — Dordrecht : Kluwer Academic Publishers, 1998. — Vol. 3. — P. 135–164.
- Christoph Walther. Intellectics and Computational Logic / S. Hölldobler. — Dordrecht : Kluwer Academic Publishers, 2000. — P. 361–386.
- Christoph Walther. Proc. 11th Int. Conf. on Логика для программирования, искусственного интеллекта и рассуждений (LPAR) / Christoph Walther, Stephan Schweitzer. — Springer, 2005. — Vol. 3452. — P. 332–346.
- Adam Koprowski. Rewriting Techniques and Applications, 19th Int. Conf., RTA-08 / Adam Koprowski, Johannes Waldmann. — Springer, 2008. — Vol. 5117. — P. 202–216.
- Giesl, J. Rewriting Techniques and Applications, 6th Int. Conf., RTA-95 / Hsiang, Jieh. — Springer, 1995. — Vol. 914. — P. 426–431.
- Ohlebusch, E. Rewriting Techniques and Applications, 11th Int. Conf., RTA-00 / Ohlebusch, E., Claves, C., Marché, C.. — Springer, 2000. — Vol. 1833. — P. 270–273.
- Hirokawa, N. Rewriting Techniques and Applications, 14th Int. Conf., RTA-03 / Hirokawa, N., Middeldorp, A.. — Springer, 2003. — Vol. 2706. — P. 311–320.
- Giesl, J. Rewriting Techniques and Applications, 15th Int. Conf., RTA-04 / Giesl, J., Thiemann, R., Schneider-Kamp, P. … [и др.]. — Springer, 2004. — Vol. 3091. — P. 210–220.
- Hirokawa, N. Term Rewriting and Applications, 16th Int. Conf., RTA-05 / Hirokawa, N., Middeldorp, A.. — Springer, 2005. — Vol. 3467. — P. 175–184.
- Koprowski, A. Term Rewriting and Applications, 17th Int. Conf., RTA-06 / Pfenning, F.. — Springer, 2006. — Vol. 4098. — P. 257–266.
- Marché, C. Term Rewriting and Applications, 18th Int. Conf., RTA-07 / Marché, C., Zantema, H.. — Springer, 2007. — Vol. 4533. — P. 303–313.