от знака равно левая часть уравнения, а справа – правая. В первом уравнении мы говорим, что сочетание (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:
ifThenElse :: Bool -> a -> a -> a
ifThenElse True
t
_ = t
ifThenElse False
_
e = e
Эта функция первым аргументом принимает значение типа Bool, а вторым и третьим – альтернативы
некоторого типа a. Если первый аргумент – True, возвращается второй аргумент, а если – False, то третий.
Интересно, что в Haskell ничего не происходит, мир Haskell-значений стоит на месте. Мы просто даём
имена разным комбинациям слов. Определяем новые термины. Потом на этих терминах определяем новые
термины, и так далее. Кажется, если ничего не меняется, то зачем язык? И что мы собираемся программиро-
вать без вычислений?
Значения | 17
Разгадка кроется в функциях not, and и or. До того как мы их определили, у нас было четыре имени, но
после их определения имён стало бесконечное множество. Три синонима пополнили наш язык бесконечным
набором комбинаций. В этом суть. Мы определяем базовые элементы и способы составления новых, потом
мы просим ”вычислить’ комбинацию из них. Мы не определяли явно, чему равна комбинация not (and true
False), это сделал за нас вычислитель Haskell1.
Вычислить стоит в кавычках, потому что на самом деле вычислений нет, есть замена синонимов на ком-
бинации простейших элементов.
Ещё один пример, положим у нас есть тип:
data Status = Work | Rest
Он определяет, что делать в данный день: работать (Work) или отдыхать (Rest). У разных рабочих разный