Я фронтенд-инженер и математик. При написании кода я ежедневно полагаюсь на свою математическую подготовку. Я использую не статистику или исчисления, а, скорее, мое глубокое понимание булевой логики. Часто я превращал сложную комбинацию амперсандов, вертикальных черт, восклицательных знаков и знаков равенства в нечто более простое и более читаемое. Я хотел бы поделиться этими знаниями, поэтому написал эту статью. Это долго, но я надеюсь, что это принесет вам такую ​​же пользу, как и мне. Наслаждаться!

Значения истинности и ложности в JavaScript

Прежде чем изучать логические выражения, давайте разберемся, что в JavaScript «правдиво». Поскольку JavaScript типизирован слабо, он переводит значения в логические значения в логических выражениях. Операторы if, &&, || и тернарные условия переводят значения в логические значения. Обратите внимание: это не означает, что они всегда возвращают логическое значение из операции.

В JavaScript всего шесть ложных значений - false, null, undefined, NaN, 0 и "", а все остальное верно. Это означает, что [] и {} правдивы, что сбивает людей с толку.

Логические операторы

В формальной логике существует всего несколько операторов: отрицание, конъюнкция, дизъюнкция, импликация и двусловность. У каждого из них есть эквивалент в JavaScript: !, &&, ||, if (/* condition */) { /* then consequence */} и === соответственно. Эти операторы создают все остальные логические операторы.

Таблицы истинности

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

Таблица отрицания очень проста. Отрицание - единственный унарный логический оператор, действующий только на один вход. Это означает, что !A || B не то же самое, что !(A || B). Скобки действуют подобно обозначениям группировки, которые вы найдете в математике.

Например, первая строка в таблице истинности отрицания (ниже) должна читаться следующим образом: «если утверждение A истинно, то выражение! A ложно».

Опровергнуть простое утверждение несложно. Отрицание «идет дождь» означает «это не дождя», а отрицание примитива JavaScript true, конечно же, false. Однако отрицать сложные утверждения или выражения не так-то просто. Что означает отрицание слов «всегда дождь» или isFoo && isBar?

Таблица Соединение показывает, что выражение A && B истинно, только если оба A и B истинны. Это должно быть хорошо знакомо по написанию JavaScript.

Таблица Дизъюнкция также должна быть вам хорошо знакома. Дизъюнкция (логическое ИЛИ) истинно, если истинны либо, либо оба из A и B.

Таблица Последствия не так привычна. Поскольку A подразумевает B, истинность A означает истинность B. Однако B может быть истинным по причинам, отличным от A, поэтому последние две строки таблицы верны. Единственное временное значение является ложным, когда A истинно, а B ложно, потому что тогда A не подразумевает B.

Хотя операторы if используются для импликации в JavaScript, не все выражения if работают таким образом. Обычно мы используем if как контроль потока, а не как проверку правдивости, когда последствия также имеют значение при проверке. Вот типичное значение if:

function implication(A, B) {
  if (A) {
    return B;
  } else {
    /* if A is false, the implication is true */
    return true;
  }
}

Не волнуйтесь, что это несколько неловко. Есть более простые способы кодирования последствий. Однако из-за этой неудобства я буду продолжать использовать в качестве символа для обозначения смысла в этой статье.

Оператор Bicondition, иногда называемый if-and-only-if (IFF), принимает значение true только в том случае, если два операнда, A и B, имеют одинаковое значение истинности. Из-за того, как JavaScript обрабатывает сравнения, использование === для логических целей следует использовать только для операндов, приведенных к логическим значениям. То есть вместо A === B следует использовать !!A === !!B.

Предостережения

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

Короткое замыкание - это то, что движки JavaScript делают для экономии времени. То, что не изменит вывод всего выражения, не оценивается. Функция doSomething() в следующих примерах никогда не вызывается, потому что независимо от того, что она возвращает, результат логического выражения не изменится:

// doSomething() is never called
false && doSomething();
true || doSomething();

Напомним, что союзы (&&) истинны только если оба утверждения верны, а дизъюнкции (||) ложны, только если оба утверждения ложны. В каждом из этих случаев после считывания первого значения больше не нужно выполнять вычисления для оценки логического результата выражений.

Из-за этой особенности JavaScript иногда нарушает логическую коммутативность. Логически A && B эквивалентно B && A, но вы сломаете свою программу, если переключите window && window.mightNotExist на window.mightNotExist && window. Это не означает, что истинность замененного выражения отличается, просто JavaScript может выдать ошибку при его анализе.

Порядок операций в JavaScript застал меня врасплох, потому что меня не учили, что формальная логика имеет порядок операций, кроме группировки и слева направо. Оказывается, многие языки программирования считают, что && имеет более высокий приоритет, чем ||. Это означает, что && сначала группируется (не оценивается) слева направо, а затем || группируется слева направо. Это означает, что A || B && C не оценивается так же, как (A || B) && C, а скорее как A || (B && C).

true || false && false; // evaluates to true
(true || false) && false; // evaluates to false

К счастью, группировка, (), имеет наивысший приоритет в JavaScript. Мы можем избежать неожиданностей и двусмысленности, вручную связав утверждения, которые мы хотим вычислить, вместе в отдельные выражения. Вот почему многие линтеры кода запрещают иметь как &&, так и || в одной группе.

Расчет составных таблиц истинности

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

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

Затем создайте столбец для каждой из переменных и заполните их всеми возможными комбинациями истинных / ложных значений. Я рекомендую заполнить первую половину первого столбца T, а вторую половину F, затем четверть следующего столбца и так далее, пока это не будет выглядеть так:

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

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

Правила замен

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

Двойное отрицание

Логически A и !!A эквивалентны. Вы всегда можете удалить двойное отрицание или добавить двойное отрицание к выражению, не изменяя его истинности. Добавление двойного отрицания полезно, когда вы хотите отрицать часть сложного выражения. Единственное предостережение заключается в том, что в JavaScript !! также действует, чтобы преобразовать значение в логическое значение, что может быть нежелательным побочным эффектом.

A === !!A

Коммутация

Любая дизъюнкция (||), конъюнкция (&&) или двусмысленность (===) может поменять местами порядок своих частей. Следующие пары логически эквивалентны, но могут изменить вычисления программы из-за короткого замыкания.

(A || B) === (B || A)
(A && B) === (B && A)
(A === B) === (B === A)

Ассоциация

Дизъюнкции и союзы - это бинарные операции, то есть они работают только с двумя входами. Хотя они могут быть закодированы в более длинные цепочки - A || B || C || D - они неявно связаны слева направо - ((A || B) || C) || D. Правило ассоциации гласит, что порядок, в котором эти группировки происходят, не влияет на логический результат.

((A || B) || C) === (A || (B || C))
((A && B) && C) === (A && (B && C))

Распределение

Ассоциация не работает как для союзов, так и для дизъюнкций. То есть (A && (B || C)) !== ((A && B) || C). Чтобы отделить B и C в предыдущем примере, вы должны распространить соединение - (A && B) || (A && C). Этот процесс также работает в обратном направлении. Если вы найдете составное выражение с повторяющейся дизъюнкцией или конъюнкцией, вы можете нераспространять его, как если бы вы вычленили общий множитель в алгебраическом выражении.

(A && (B || C)) === ((A && B) || (A && C))
(A || (B && C)) === ((A || B) && (A || C))

Другой распространенный случай распределения - это двойное распределение (аналогично FOIL в алгебре):
1. ((A || B) && (C || D)) === ((A || B) && C) || ((A || B) && D)
2. ((A || B) && C) || ((A || B) && D) ===
((A && C) || B && C)) || ((A && D) || (B && D))

(A || B) && (C || D) === (A && C) || (B && C) || (A && D) || (B && D)
(A && B) ||(C && D) === (A || C) && (B || C) && (A || D) && (B || D)

Материальное значение

Выражения импликации (A → B) обычно переводятся в код как if (A) { B }, но это не очень полезно, если составное выражение имеет несколько значений. В итоге вы получите вложенные if инструкции - запах кода. Вместо этого я часто использую правило материальной импликации замены, которое гласит, что A → B означает, что либо A ложно, либо B истинно.

(A → B) === (!A || B)

Тавтология и противоречие

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

(A || !A) === true
(A || true) === true
(A && !A) === false
(A && false) === false

С этими эквивалентностями связаны дизъюнкция и соединение с другим логическим литералом. Их можно упростить до истинности переменной.

(A || false) === A
(A && true) === A

Транспонирование

При манипулировании импликацией (A → B) люди часто совершают ошибку, полагая, что отрицание первой части, A, подразумевает, что вторая часть, B, также отрицается - !A → !B. Это называется обратным импликацией, и это не обязательно верно. То есть наличие исходного значения не говорит нам, верно ли обратное, потому что A не является необходимым условием для B. (Если верно и обратное - по независимым причинам - тогда A и B являются двояковыми.)

Однако из первоначального вывода мы можем узнать, что контрапозитив верен. Поскольку B является необходимым условием для A (вспомните из таблицы истинности, подразумевающей, что если B истинно, A также должно быть истинным), мы можем утверждать, что !B → !A.

(A → B) === (!B → !A)

Материальная эквивалентность

Название biconditional происходит от того факта, что оно представляет собой два условных (подразумеваемых) утверждения: A === B означает, что A → B и B → A. Истинные значения A и B связаны друг с другом. Это дает нам первое правило материальной эквивалентности:

(A === B) === ((A → B) && (B → A))

Используя материальную импликацию, двойное распределение, противоречие и коммутацию, мы можем преобразовать это новое выражение во что-то, что проще кодировать:
1. ((A → B) && (B → A)) === ((!A || B) && (!B || A))
2. ((!A || B) && (!B || A)) ===
((!A && !B) || (B && !B)) || ((!A && A) || (B && A))

3. ((!A && !B) || (B && !B)) || ((!A && A) || (B && A)) ===
((!A && !B) || (B && A))

4. ((!A && !B) || (B && A)) === ((A && B) || (!A && !B))

(A === B) === ((A && B) || (!A && !B))

Экспорт

Вложенные if операторы, особенно если нет else частей, являются запахом кода. Простой вложенный оператор if может быть сокращен до единственного оператора, в котором условное выражение является комбинацией двух предыдущих условий:

if (A) {
  if (B) {
    C
  }
}
// is equivalent to
if (A && B) {
  C
}

(A → (B → C)) === ((A && B) → C)

Законы ДеМоргана

Законы ДеМоргана необходимы для работы с логическими утверждениями. Они говорят, как распределить отрицание через союз или дизъюнкцию. Рассмотрим выражение !(A || B). Законы ДеМоргана гласят, что при отрицании дизъюнкции или союза отрицайте каждое утверждение и меняйте && на || или наоборот. Таким образом, !(A || B) - это то же самое, что !A && !B. Точно так же !(A && B) эквивалентно !A || !B.

!(A || B) === !A && !B
!(A && B) === !A || !B

Тернарный (если-то-еще)

Тернарные операторы (A ? B : C) регулярно встречаются в программировании, но они не совсем подразумевают. Перевод из тернарной логики в формальную на самом деле представляет собой соединение двух импликаций, A → B и !A → C, которые мы можем записать как (!A || B) && (A || C), используя материальную импликацию.

(A ? B : C) === (!A || B) && (A || C)

XOR (Исключающее ИЛИ)

Исключительное ИЛИ, часто сокращенно xor, означает «одно или другое, но не оба». Это отличается от обычного оператора или только тем, что оба значения не могут быть истинными. Это часто то, что мы имеем в виду, когда используем «или» на простом английском языке. В JavaScript нет собственного оператора xor, как бы это представить?
1. «A или B, но не одновременно A и B»
2. (A || B) && !(A && B) прямой перевод
3. (A || B) && (!A || !B) Законы ДеМоргана
4. (!A || !B) && (A || B) коммутативность
5. A ? !B : B определение if-then-else

A ? !B : B является исключающим или (xor) в JavaScript

В качестве альтернативы,
1. «A или B, но не одновременно A и B»
2. (A || B) && !(A && B) прямой перевод
3. (A || B) && (!A || !B) Законы ДеМоргана
4. (A && !A) || (A && !B) || (B && !A) || (B && !B) двойное распределение
5. (A && !B) || (B && !A) замена противоречия
6. A === !B или A !== B эквивалент материала

A === !B или A !== B - это xor в JavaScript

Установить логику

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

функция-предикат - это функция, входом которой является значение из набора, а выходом - логическое значение. В следующих примерах кода я буду использовать массив чисел для набора и две функции-предикаты: isOdd = n => n % 2 !== 0; и isEven = n => n % 2 === 0;.

Универсальные заявления

Универсальный оператор применяется ко всем элементам в наборе, то есть его функция-предикат возвращает истину для каждого элемента. Если предикат возвращает false для любого одного (или нескольких) элементов, то универсальное утверждение ложно. Array.prototype.every принимает функцию предиката и возвращает true, только если каждый элемент массива возвращает истину для предиката. Он также завершается раньше (с false), если предикат возвращает false, не выполняя предикат для каких-либо других элементов массива, поэтому на практике избегайте побочных эффектов в предикатах.

В качестве примера рассмотрим массив [2, 4, 6, 8] и универсальное утверждение «каждый элемент массива четный». Используя isEven и встроенную универсальную функцию JavaScript, мы можем запустить [2, 4, 6, 8].every(isEven) и обнаружить, что это true.

Array.prototype.every - универсальное заявление JavaScript.

Экзистенциальные утверждения

Выражение экзистенциальное делает конкретное утверждение о наборе: по крайней мере, один элемент в наборе возвращает истину для функции предиката. Если предикат возвращает false для каждого элемента в наборе, то экзистенциальное утверждение ложно.

JavaScript также предоставляет встроенный экзистенциальный оператор: Array.prototype.some. Подобно every, some вернет раньше (с истиной), если элемент удовлетворяет своему предикату. Например, [1, 3, 5].some(isOdd) выполнит только одну итерацию предиката isOdd (потребляет 1 и возвращает true) и вернет true. [1, 3, 5].some(isEven) вернет false.

Array.prototype.some - это экзистенциальное утверждение JavaScript

Универсальное значение

После того, как вы проверили универсальный оператор против набора, скажем nums.every(isOdd), возникает соблазн подумать, что вы можете захватить элемент из набора, который удовлетворяет предикату. Однако есть одна загвоздка: в булевой логике истинный универсальный оператор не подразумевает, что набор не пуст. Универсальные утверждения о пустых наборах всегда истинны, поэтому, если вы хотите получить элемент из набора, удовлетворяющего какому-либо условию, используйте вместо этого экзистенциальную проверку. Чтобы доказать это, запустите [].every(() => false). Это будет правдой.

Универсальные утверждения о пустых наборах всегда верны .

Отрицание универсальных и экзистенциальных утверждений

Отрицание этих утверждений может вызывать удивление. Отрицание универсального утверждения, скажем, nums.every(isOdd), есть не nums.every(isEven), а скорее nums.some(isEven). Это экзистенциальное утверждение с отрицаемым предикатом. Точно так же отрицание экзистенциального утверждения - это универсальное утверждение с отрицаемым предикатом.

!arr.every(el => fn(el)) === arr.some(el => !fn(el))
!arr.some(el => fn(el)) === arr.every(el => !fn(el))

Установить перекрестки

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

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

A.some(el => B.includes(el)) && A.some(el => !B.includes(el)) && B.some(el => !A.includes(el)) описывает соединенную пару множеств

Один набор может содержать все элементы другого набора, но иметь элементы, не общие для второго набора. Это отношение подмножества, обозначенное как Subset ⊆ Superset.

B.every(el => A.includes(el)) описывает отношение подмножества B ⊆ A

Эти два набора могут использовать нет элементов. Это непересекающиеся множества.

A.every(el => !B.includes(el)) описывает непересекающуюся пару множеств

Наконец, два набора могут разделять каждый элемент. То есть они являются подмножествами друг друга. Эти наборы равны. В формальной логике мы бы написали A ⊆ B && B ⊆ A ⟷ A === B, но в JavaScript с этим возникают некоторые сложности. В JavaScript Array является упорядоченным набором и может содержать повторяющиеся значения, поэтому мы не можем предположить, что код двунаправленного подмножества B.every(el => A.includes(el)) && A.every(el => B.includes(el)) подразумевает, что массивы A и B равны. Если A и B являются наборами (что означает, что они были созданы с помощью new Set()), то их значения уникальны, и мы можем выполнить двунаправленную проверку подмножества, чтобы увидеть, A === B.

(A === B) === (Array.from(A).every(el => Array.from(B).includes(el)) && Array.from(B).every(el => Array.from(A).includes(el)), учитывая, что A и B построены с использованием new Set()

Перевод логики на английский язык

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

Ниже представлена ​​таблица логического кода (слева) и их английских эквивалентов (справа), в значительной степени заимствованная из прекрасной книги Основы логики.

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

Пример 1

Недавно, чтобы удовлетворить требованиям GDPR ЕС, мне пришлось создать модальное окно, которое показывало политику моей компании в отношении файлов cookie и позволяло пользователям устанавливать свои предпочтения. Чтобы сделать это максимально ненавязчивым, у нас были следующие требования (в порядке приоритета):

  1. Если пользователь не был из ЕС, никогда не показывать модальные настройки GDPR.
  2. 2. Если приложению программно необходимо отобразить модальное окно (если для действия пользователя требуется больше разрешений, чем разрешено в настоящее время), покажите модальное окно.
  3. Если пользователю разрешено иметь менее навязчивый баннер GDPR, не показывайте модальное окно.
  4. Если пользователь еще не установил свои предпочтения (по иронии судьбы сохранены в файле cookie), покажите модальное окно.

Я начал с серии if операторов, смоделированных непосредственно после этих требований:

const isGdprPreferencesModalOpen = ({
  shouldModalBeOpen,
  hasCookie,
  hasGdprBanner,
  needsPermissions
}) => {
  if (!needsPermissions) {
    return false;
  }
  if (shouldModalBeOpen) {
    return true;
  }
  if (hasGdprBanner) {
    return false;
  }
  if (!hasCookie) {
    return true;
  }
  return false;
}

Для ясности, приведенный выше код работает, но возвращение логических литералов - это запах кода. Итак, я проделал следующие шаги:

/* change to a single return, if-else-if structure */
let result;
if (!needsPermissions) {
  result = false;
} else if (shouldBeOpen) {
  result = true;
} else if (hasBanner) {
  result = false;
} else if (!hasCookie) {
  result = true
} else {
  result = false;
}
return result;
/* use the definition of ternary to convert to a single return */
return !needsPermissions ? false : (shouldBeOpen ? true : (hasBanner ? false : (!hasCookie ? true : false)))
/* convert from ternaries to conjunctions of disjunctions */
return (!!needsPermissions || false) && (!needsPermissions || ((!shouldBeOpen || true) && (shouldBeOpen || ((!hasBanner || false) && (hasBanner || !hasCookie))))
/* simplify double-negations and conjunctions/disjunctions with boolean literals */
return needsPermissions && (!needsPermissions || ((!shouldBeOpen || true) && (shouldBeOpen || (!hasBanner && (hasBanner || !hasCookie))))
/* DeMorgan's Laws */
return needsPermissions && (!needsPermissions || ((!shouldBeOpen || true) && (shouldBeOpen || ((!hasBanner && hasBanner) || (hasBanner && !hasCookie))))
/* eliminate tautologies and contradictions, simplify */
return needsPermissions && (!needsPermissions || (shouldBeOpen || (hasBanner && !hasCookie)))
/* DeMorgan's Laws */
return (needsPermissions && !needsPermissions) || (needsPermissions && (shouldBeOpen || (hasBanner && !hasCookie)))
/* eliminate contradiction, simplify */
return needsPermissions && (shouldBeOpen || (hasBanner && !hasCookie))

В итоге я получил что-то более элегантное и читаемое:

const isGdprPreferencesModalOpen = ({
  needsPermissions,
  shouldBeOpen,
  hasBanner,
  hasCookie,
}) => (
  needsPermissions && (shouldBeOpen || (!hasBanner && !hasCookie))
);

Пример 2

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

const isButtonDisabled = (isRequestInFlight, state) => {
  if (isRequestInFlight) {
    return true;
  }
  if (enabledStates.includes(state)) {
    return false;
  }
  return true;
};

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

// convert to if-else-if structure
let result;
if (isRequestInFlight) {
  result = true;
} else if (enabledStates.includes(state)) {
  result = false;
} else {
  result = true;
}
return result;
// convert to ternary
return isRequestInFlight
  ? true
  : enabledStates.includes(state)
    ? false
    : true;
/* convert from ternary to conjunction of disjunctions */
return (!isRequestInFlight || true) && (isRequestInFlight || ((!enabledStates.includes(state) || false) && (enabledStates.includes(state) || true))
/* remove tautologies and contradictions, simplify */
return isRequestInFlight || !enabledStates.includes(state)

Тогда я получаю:

const isButtonDisabled = (isRequestInFlight, state) => (
  isRequestInFlight || !enabledStates.includes(state)
);

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

Пример 3

При написании структуры A / B-тестирования для моей компании у нас было два основных списка включенных и отключенных экспериментов, и мы хотели проверить, что каждый эксперимент (каждый отдельный файл в папке) был записан в один или другой список но не оба. Это означает, что включенные и отключенные наборы не связаны, а набор всех экспериментов является подмножеством соединения двух наборов экспериментов. Причина, по которой набор всех экспериментов должен быть подмножеством комбинации двух списков, состоит в том, что не должно быть ни одного эксперимента, существующего вне двух списков.

const isDisjoint = !enabled.some(el => disabled.includes(el)) && 
  !disabled.some(el => enabled.includes(el));
const isSubset = allExperiments.every(
  el => enabled.concat(disabled).includes(el)
);
assert(isDisjoint && isSubset);

Заключение

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