Параметры пишутся через пробел между новым именем и знаком равно:
not :: Bool -> Bool
not True
= False
not False = True
Мы определили новое имя not с типом Bool -> Bool. Оно определяется двумя уравнениями (clause). Слева
от знака равно левая часть уравнения, а справа – правая. В первом уравнении мы говорим, что сочетание (not
True) означает False, а сочетание (not False) означает True. Опять же, мы ничего не вычисляем, мы даём
новые имена нашим константам True и False. Только в этом случае имена составные.
Если вычислителю нужно узнать, что кроется за составным именем not False он последовательно про-
анализирует уравнения сверху вниз, до тех пор, пока левая часть уравнения не совпадёт со значением not
False. Сначала мы сверим с первым:
not True
== not False
-- нет, пошли дальше
not False
== not False
-- эврика, вернём правую часть
=> True
Определим ещё два составных имени
and :: Bool -> Bool -> Bool
and False
_
= False
and True
x
= x
or
:: Bool -> Bool -> Bool
or True
_ = True
or False
x = x
Эти синонимы определяют логические операции “и” и “или”. Здесь несколько новых конструкций, но вы
не пугайтесь, они не так трудны для понимания. Начнём с _:
and False
_
= False
16 | Глава 1: Основы
Здесь cимвол _ означает, что в этом уравнении, если первый параметр равен False, то второй нам уже не
важен, мы знаем ответ. Так, если в логическом “и” один из аргументов равен False, то всё выражение равно
False. Так же и в случае с or.
Теперь другая новая конструкция:
and True
x
= x
В этом случае параметр x служит для того, чтобы перетащить значение из аргумента в результат. Кон-
кретное значение нам также не важно, но в этом случае мы полагаем, что слева и справа от =, x имеет одно
и то же значение.
Итак у нас уже целых семь имён: True, False, true, false, not, and, or. Или не семь? На самом деле, их
уже бесконечное множество. Поскольку три из них составные, мы можем создавать самые разнообразные
комбинации:
not (and true False)
or (and true true) (or False False)
not (not true)
not (or (or True True) (or False (not True)))
...
Обратите внимание на использование скобок, они группируют значения. Так, если бы мы написали not
not true вместо not (not true), мы бы получили ошибку компиляции, потому что not ожидает один пара-
метр, а в выражении not not true их два. Параметры дописываются к имени через пробел.
Посмотрим, как происходят вычисления. В сущности, процесса вычислений нет, есть процесс замены
синонимов на основные понятия согласно уравнениям. Базовые понятия мы определили в типах. Так давайте
“вычислим” выражение not (and true False):
-- выражение
--
уравнение
not (and true False)
--
true
= True
not (and True False)
--
and True
x = x
=> and True False = False
not False
--
not False
= True
True
Слева в столбик написаны шаги “вычисления”, а справа уравнения, по которым синонимы заменяются
на комбинации слов. Процесс замены синонима (левой части уравнения) на комбинацию слов (правую часть
уравнения) называется редукцией (reduction).
Сначала мы заменили синоним true на правую часть его уравнения, тo есть на конструктор True. Затем
мы заменили выражение (and True False) на правую часть из уравнения для синонима and. Обратите вни-
мание на то, что переменная x была заменена на значение False. Последним шагом была замена синонима
not. В конце концов мы пришли к базовому понятию, а именно – к одному из двух конструкторов. В данном
случае True.
Интересно, что новые синонимы могут быть использованы в правых частях уравнений. Так мы можем
определить операцию “исключающее или”:
xor :: Bool -> Bool -> Bool
xor a b = or (and (not a) b) (and a (not b))
Этим выражением мы говорим, что xor a b это или отрицание a и b, или a и отрицание b. Это и есть
определение “исключающего или”.
Может показаться, что с типом Bool мы зациклены на двух конструкторах, и единственное, что нам оста-
ётся – это давать всё новые и новые имена словам True и False. Но на самом деле это не так. С помощью
типов-параметров мы можем выйти за эти рамки. Определим функцию ветвления ifThenElse: