Тверской государственный университет
Опубликован: 03.10.2011 | Доступ: свободный | Студентов: 3245 / 55 | Оценка: 4.33 / 3.83 | Длительность: 19:48:00
ISBN: 978-5-9963-0573-5
Лекция 6:

Логики вполне достаточно

Обращение импликации

Хотя два последних свойства не являются тавтологиями, однако имеется интересная тавтология того же общего стиля:

(a implies b) = ((not b) implies (not a))  — РЕВЕРС

Доказательство: Вспомним определение импликации. Тогда левая сторона дает (not a) or b, а справа имеем (not (not b)) or (not a). Из предыдущих тавтологий известно, что (not (not b)) = b. Учитывая коммутативность or, получаем нужный результат.

Другой способ. В таблице истинности для implies меняем столбцы для a и для b местами, одновременно выполняя инверсию значений. Приходим к исходной таблице

По свойству РЕВЕРС если b верно всякий раз, когда a верно, то: если b не выполняется, то не будет выполняться и a.

Неформальное доказательство от противного: если бы a было истинным, то импликация говорит нам, что и b было бы истинным, а оно ложно. Пришли к противоречию, значит, посылка неверна и a ложно.

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

  1. Все профессиональные политики коррупционеры. Она не коррупционер, поэтому она не является профессиональным политиком.
  2. Всегда, когда беру с собой зонтик, дождя не бывает. Хотя на сайте weather.com обещают столь нужный нам дождь, на всякий случай я оставлю зонтик дома.
  3. Все недавно построенные здания в этом районе имеют плохую термоизоляцию. В этой квартире, несмотря на жару, прохладно. Дом должен быть старым.

5.3. Полустрогие булевские операции

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

Почувствуй историю

Дорога к современной логике

Логика корнями уходит в прошлые века, в частности, к Аристотелю, который определил правила "Риторики", зафиксировав некоторые формы вывода. В 18-м веке Лейбниц установил, что вывод – это форма математики. В 19-м веке английский математик Джордж Буль определил исчисление истинностных значений (в его честь – тип "boolean"). В следующем столетии большим толчком в развитии логики послужило осознание того, что математика того времени имела шаткое основание и могла приводить к противоречиям. Цель, поставленная создателями современной математической логики, состояла в исправлении этой ситуации и построении прочного и строгого фундамента математики.

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

Поставим следующий вопрос, связанный с линией метро l и целым числом n:

"Является ли станция с номером n линии l пересадочной?"

Мы могли бы выразить это булевским выражением:

l.i_th (n).is_exchange              [S1]

где is_exchangeзапрос булевского типа в классе STATION, определяющий, является ли станция пересадочной. С запросом i_th мы познакомились в предыдущей лекции – он возвращает станцию на линии, идентифицируемую по ее номеру, здесь n.

Корректная форма этого запроса появится ниже в этой лекции.

Выражение [S1] появляется для выполнения предназначенной работы: l обозначает линию; l.i_th (n), обозначает ее n-ю станцию, экземпляр класса STATION; так что l.i_th (n).is_exchange, применяя запрос is_exchange к этой станции, говорит нам, используя булевское значение, является ли станция пересадочной.

Но мы ничего не сказали о значении n. Потому l.i_th (n) может быть не определено, поскольку запрос i_th имеет предусловие:

i_th (i: INTEGER): STATION
        -- i-я станция на этой линии
    require
        not_too_small: i >= 1
        not_too_big: i <= count

Как же мы можем написать корректное выражение, сохраняя наши намерения? Если n < 1 or n > l.count, то разумно полагать, что ответом на наш неформальный вопрос не может быть "Да", так как несуществующая станция не может использоваться для пересадки. Так как в булевском мире может быть всего два ответа, ответом на наш вопрос может быть только "Нет!" Формально, булевское выражение должно иметь значение False. Для достижения этого поведения мы могли бы попытаться выразить желаемое свойство не в виде[S1], но как

(n >= 1) and (n <= count) and l.i_th (n).is_exchange            [S2]

Все еще не корректная форма.

Но и это еще не достаточно хорошо. Проблема в том, что если n выходит за свои границы, например, n = 0, то последний терм l.i_th (n).is_exchange не определен. Если бы нас интересовало только значение [S2], то можно было бы не беспокоиться. Согласно принципу конъюнкции значение может быть только False, поскольку первый терм n >= 1 имеет значение False, а второй и третий термы не влияют на результат.

Предположим, однако, что выражение появляется в программе и начинает вычисляться в период ее выполнения. Операция and, как мы видели, является коммутативной, поэтому вполне законно при выполнении вычислить оба операнда a и b, а затем комбинировать их значения, используя таблицу истинности для and. Но тогда вычисление [S2] будет прервано из-за ошибки, возникающей при попытке вычисления последнего терма.

Если бы эти вычисления концептуально были необходимыми, то ничего сделать было бы нельзя. Попытка вычислить выражение с неопределенным значением должна приводить к краху. Это подобно попытке вычислить значение числового выражения 1 / 0. Но во многих случаях предпочтительнее заканчивать вычисление выражения, когда первый терм дает значение False, и вместо падения возвращать результат False, согласованный с определением and.

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

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

Первый состоит в том, чтобы пытаться исправить ситуацию после возникновения ошибки. Если операнд булевской операции не определен, то вычисление приведет к отказу. Мы могли бы иметь механизм "захвата" отказов и попытаться посмотреть, достаточно ли других термов для установления значения выражения в целом. Такой механизм означает, что отказ не является настоящей смертью. Он, скорее, напоминает смерть в компьютерных играх, где можно получить новые жизни (пока вы можете платить за них). Такой механизм существует: он называется обработкой исключений (exception handling) и позволяет вам планировать возможность несчастных случаев и попытки исправления ситуации. Однако в данном случае это было бы (отважимся употребить этот термин) "сверхубийственно" (overkill). Чрезмерно много специальных усилий пришлось бы применять для этой простой и общей ситуации.

Второй путь решения проблемы мог бы заключаться в принятии решения, что операция and, как она понимается в программировании, более не является коммутативной (это же верно и для двойственной операции or). При вычислении a and b мы бы гарантировали, что b не будет вычисляться, если a получило значение False. Результат в этом случае был бы False. Недостаток такого решения в несоответствии компьютерной версии с хорошо продуманной математической концепцией. Есть и прагматический недостаток, связанный с эффективностью вычислений. Дело в том, что коммутативность в случае, когда оба операнда определены, может помочь в более быстром вычислении конъюнкции, вычисляя сначала второй операнд или вычисляя их параллельно, что часто позволяет делать современная аппаратура компьютера.

Такие приемы ускорения вычислений, называемые оптимизацией, заботят обычно не программистов, а разработчиков трансляторов

Третий путь – на котором мы и остановимся – включить дополнительно полезные некоммутативные версии операций, дав им другие имена, во избежание каких-либо семантических недоразумений. Новый вариант для and будет называться and then; новый вариант для двойственной операции or будет называться or else. У новых операций имя состоит из двух ключевых слов, разделенных пробелом. Семантика следует из ранее приведенных обоснований.

Почувствуй семантику

Полустрогие булевские операции

Рассмотрим два выражения a и b, которые могут быть определены и иметь булевские значения или могут быть не определены. Тогда

  • a and then b совпадает со значением a and b, если как a, так и b определены, и в дополнение имеет значение False, если a определено и имеет значение False;
  • a or else b совпадает со значением a or b, если как a, так и b определены, и в дополнение имеет значение True, если a определено и имеет значение True.

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

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

Еще один способ определения семантики полустрогих операций состоит в задании расширенной таблицы истинности, где каждый операнд и результат будут иметь три возможных значения: True, False и Undefined

Всякий раз, когда a and b определено, a and then b также определено и имеет то же значение, но обратное не верно. То же самое верно и для or else относительно or.

С новой нотацией появляется корректный способ выражения условия нашего примера:

((n >= 1) and (n <= count)) and then l.i_th (n).is_exchange      [S3]

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

Почувствуй методологию

Выбор между обычными и полустрогими операциями

При записи контрактов и других условий:

  • используйте обычные булевские операции or и and, когда можно гарантировать, что оба операнда определены при выполнении программы в тот момент, когда требуется вычислить условие;
  • если же гарантировать можно только определенность одного операнда, то делайте его первым и используйте операции or else и and then.

Наш пример [S3] соответствует последнему случаю.

В первом случае не было бы ошибкой использовать полустрогую версию. Но в этом случае предписывался бы строгий порядок вычисления операндов. Стоит избегать излишней сверхспецификации. Это оставляет компиляторам свободу в оптимизации порядка вычисления операндов.

Понятие полустрогой операции применимо не только к математической логике и ПО.

Почувствуй практику

Полуограниченные операции и вы

Полустрогие операции отражают особенности вывода, применимые в повседневной жизни.

Всякий раз, когда вы встречаете фразу "если существует…", можно полагать, что речь идет о полустрогой операции. Кредитные операции могут требовать подписи супруга, если он существует, что можно выразить как is_single or else spouse_must_sign или в явном программистском стиле:

(spouse = Void) or else spouse.must_sign

где Void означает отсутствие объекта. В любой форме второй операнд or else не имеет смысла для строгого or, так как, когда первый операнд имеет значение True, понятие супруга не определено.

Ирина Калашникова
Ирина Калашникова

Добрый день, подскажите на тест после каждой лекции сколько дается попыток? 

Наталья Король
Наталья Король

Что это значит?) Зранее спасибо)