Вы находитесь на странице: 1из 20

Лекция 7.

Доказательство корректности программ

Введение в Функциональное программирование

Джон Харрисон

Университет Кембриджа

10 сентября 2008 г.

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Темы

Проблема корректности
Тестирование и верификация
Завершимость и полнота
Вычисление степени и НОД
Конкатенация и обращение списков

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Проблема корректности

Программы пишут, чтобы они выполняли определённую задачу.


Однако, программисты знают, что зачастую достаточно сложно
написать программу, которая делает то, что требуется.
На практике, в большинстве объёмных программ есть «ошибки».
Некоторые ошибки безобидны, другие — раздражают.
Они могут вызвать финансовые катастрофы или проблемы
отношений между компаниями (например ошибка Pentium FDIV).
Иногда ошибки могут быть смертельно опасными.
Питер Ньюман: «Компьютерные риски»

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Опасные ошибки

Вот несколько задач, где ошибки могут быть смертельно опасными:


Электрокардиостимулятор
Авиационные автопилоты
Системы управления двигателем и антиблокировочные
тормозные системы
Приборы радиационной терапии
Системы управления ядерных реакторов
Про такие задачи говорят, что они критичны к наличию ошибок.

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Тестирование и верификация

Один из хороших способов отыскания ошибок это всестороннее


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

N(N + 1)
Σn=N
n=0 n =
2
Мы можем проверить его для множества определённых значений N,
но проще и надёжнее доказать её (по индукции).

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Ограничения верификации
Верификацию можно представить в виде диаграммы:

Требования к системе
6

Математическая спецификация требований


6

Математическая модель системы


6

Реализация системы

Лишь центральная связь математически точна. Остальные остаются


неформальными — всё, что мы можем сделать, это постараться
сохранять их максимально простыми и прозрачными.
Джон Харрисон Введение в Функциональное программирование
Лекция 7. Доказательство корректности программ

Верификация функциональных программ

Мы уже заметили раньше, что для функциональных программ легче


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

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Вычисление степени (1)

Вспомним следующее простое определение функции потенцирования


натуральных чисел:
#l e t r e c exp x n =
i f n = 0 then 1
e l s e x ∗ exp x ( n − 1 ) ; ;

Мы докажем, что она удовлетворяет следующей спецификации:


Для любого n ≥ 0 и произвольного x функция exp x n завершается и
exp x n = x n
Функция определена посредством (примитивной) рекурсии.
Доказательство проводится по (математической) индукции.

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Вычисление степени (2)

Если n = 0, то по определению exp x n = 1. Так как для любого


целого x, мы имеем x 0 = 1, таким образом базис индукции
выполняется.
Допустим что exp x n = x n верно. Так как n ≥ 0, то n + 1 6= 0
тоже верно. Поэтому:

exp x (n + 1) = x ∗ exp x ((n + 1) − 1)


= x ∗ exp x n
= x ∗ xn
= x n+1

Заметим, что мы приняли 00 = 1. Пример того, как точно надо


составлять техническую спецификацию!

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Вычисление НОД (1)

Определим функцию для вычисления НОД двух целых чисел,


используя алгоритм Евклида.
#l e t r e c gcd x y =
i f y = 0 then x
e l s e gcd y ( x mod y ) ; ;

Мы хотим доказать что:


Для любых x и y , вычисление gcd x y завершается с результатом
равным НОД(x, y ).
Тут нам надо быть аккуратными со спецификацией. Каков НОД
двух отрицательных чисел?

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Вычисление НОД (2)

Мы пишем x|y и говорим «alertx делит alerty », что означает что y


является целочисленным делителем x, то есть существует такое
целое d , что y = dx.
Мы говорим что d это общий делитель x и y если d |x и d |y .
Мы говорим что d это наибольший общий делитель если:
Верно d |x и d |y
Для любого другого d 0 , если d 0 |x и d 0 |y то d 0 |d .
Заметим, что мы не указываем знак у НОД за исключением случая,
когда оба x и y нули. Спецификация совершенно не привязывает
реализацию.

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Вычисление НОД (3)

Функция gcd уже не определена через примитивную рекурсию.


На шаге алгоритма Евклида gcd x y определена в терминах gcd y
(x mod y).
Поэтому мы не прибегаем к пошаговой математической индукции, а
используем трансфинитную индукцию по |y |.
Идея в том, что эта величина (часто называемая мера) убывает в
ходе рекурсии. Мы можем использовать это свойство, чтобы
доказать что вызов функции завершается, а также как опору для
трансфинитной индукции.
В запутанных рекурсиях сложно найти подходящее обоснованное
упорядочение аргументов. Но в большинстве случаев можно
использовать этот простой подход «меры».

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Вычисление НОД (4)

Теперь мы представим доказательство. Возьмём некоторого n, что


для произвольного значения x и y такого, что |y | < n теорема
справедлива. Основываясь на этом предположении, попробуем
доказать, что она справедлива также для произвольного x
при |y | = n. Здесь два частных случая.
Возьмём произвольное n. Предположим что теорема верна для всех
аргументов x и y , таких что |y | < n, и мы попробуем доказать её для
всех x и y , таких что |y | = n. Здесь два случая.
Во-первых, предположим что y = 0. Тогда gcd x y = x по
определению. Очевидно что x|x и x|0, поэтому x и есть общий
делитель.
Предположим что d является ещё одним общим делителем, т.е. d |x
и d |0. Тогда мы сразу получаем d |x, поэтому x и есть наибольший
общий делитель.
Мы установили первую часть доказательства по индукции.

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Вычисление НОД (5)

Теперь предположим, что y 6= 0. Мы хотим применить индуктивное


предположение к gcd y (x mod y ).
Для краткости мы будем писать r = x mod y . Основное свойство
функции mod, которое мы будем использовать это, что для y 6= 0,
найдётся такое целое q, что x = qy + r и |r | < |y |.
Поскольку |r | < |y |, то согласно индуктивному предположению
d = gcd y (x mod y ) является НОД для y и r .
Нам осталось лишь показать, что он является НОД также и для x и
y . Конечно же он общий делитель ибо при d |y и d |r имеем d |x так
как x = qy + r .
Теперь допустим d 0 |x и d 0 |y . Согласно тому же уравнению мы
видим, что d 0 |r . Таким образом d 0 является общим делителем y и r ,
но в соответствии с индуктивным предположением получаем d 0 |d
что и требовалось.

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Конкатенация списков (1)


Теперь лучше рассмотрим пример касательно списков, а не чисел.
Определим:
#l e t r e c append l 1 l 2 =
match l 1 with
[ ] −> l 2
| ( h : : t ) −> h : : ( append t l 2 ) ; ;

Предполагается, что эта функция для конкатенации (сцепления)


двух списков. Мы хотим доказать, что эта операция ассоциативна,
т.е. для любых трёх списков l1 , l2 и l3 имеем свойство:
append l1 (append l2 l3 ) = append (append l1 l2 ) l3
Мы можем использовать математическую индукцию по длине l1 , но
так как функция была задана с помощью структурной рекурсии над
списками, то естественнее доказывать теорему используя
структурную индукцию.
Принцип такой: если свойство верно для пустого списка и при
условии что оно верно для t, то оно также верно и для любого h :: t
то оно верно вообще для любого списка.
Джон Харрисон Введение в Функциональное программирование
Лекция 7. Доказательство корректности программ

Конкатенация списков (2)

Используем структурную индукцию по l1 . Нам надо рассмотреть два


случая. Сначала, предположим что l1 = []. Тогда мы имеем:

append l1 (append l2 l3 )
= append [] (append l2 l3 )
= append l2 l3
= append (append [] l2 ) l3
= append (append l1 l2 ) l3

Что и требовалось.

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Конкатенация списков (3)

Теперь допустим, что l1 = h :: t. Мы в праве полагать, что для


любого l2 и l3 выполняется:

append t (append l2 l3 ) = append (append t l2 ) l3

Поэтому:

append l1 (append l2 l3 )
= append (h :: t) (append l2 l3 )
= h :: (append t (append l2 l3 ))
= h :: (append (append t l2 ) l3 )
= append (h :: (append t l2 )) l3
= append (append (h :: t) l2 ) l3
= append (append l1 l2 ) l3

Теорема доказана.

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Обращение списков (1)


Для последнего примера определим функцию обращения списков.
#l e t r e c r e v =
fun [ ] −> [ ]
| ( h : : t ) −> append ( r e v t ) [ h ] ; ;
r e v : ’ a l i s t −> ’ a l i s t = <fun>
#r e v [ 1 ; 2 ; 3 ] ; ;
− : int l i s t = [ 3 ; 2; 1]

Мы докажем, что для любого списка l верно следующее:


rev(rev l ) = l
Опять же, тут структурная индукция. Однако, нам потребуются две
леммы, которые тоже можно доказать с помощью структурной
индукции (подробности в конспектах):
append l [] = l
rev(append l1 l2 ) = append (rev l2 ) (rev l1 )

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Обращение списков (2)

Сначала рассмотрим случай, когда l = []. Доказательство просто:

rev(rev l ) = rev(rev [])


= rev []
= []
= l

Теперь рассмотрим случай, когда l = h :: t, и мы знаем, что

rev(rev t) = t

Джон Харрисон Введение в Функциональное программирование


Лекция 7. Доказательство корректности программ

Обращение списков (3)

rev(rev l )
= rev(rev (h :: t))
= rev(append (rev t) [h])
= append (rev [h]) (rev(rev t))
= append (rev [h]) t
= append (rev (h :: [])) t
= append (append [] [h]) t
= append [h] t
= append (h :: []) t
= h :: (append [] t)
= h :: t
= l

Джон Харрисон Введение в Функциональное программирование

Вам также может понравиться