мание на то, что переменная 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). У разных рабочих разный
график. Например, есть функции:
jonny :: Week -> Status
jonny x = ...
colin :: Week -> Status
colin x = ...
Конкретное определение сейчас не важно, важно, что они определяют зависимость статуса ( Status) от
дня недели ( Week) для работников Джонни (jonny) и Колина (colin).
Также у нас есть полезная функция:
calendar :: Date -> Week
calendar x = ...
Она определяет по дате день недели. И теперь, зная лишь эти функции, мы можем спросить у вычислителя
будет ли у Джонни выходной 8 августа 3043 года:
jonny (calendar ( Date( Year3043) August( Day8)))
=>jonny Saturday
=> Rest
Интересно, у нас опять всего лишь два значения, но, дав такое большое имя одному из значений, мы
смогли получить полезную нам информацию, ничего не вычисляя.
1.4 Классы типов
Если типы и значения – привычные понятия, которые можно найти в том или ином виде в любом языке
программирования, то термин класс типов встречается не часто. У него нет аналогов и в обычном языке,
поэтому я сначала постараюсь объяснить его смысл на примере.
В типизированном языке у каждой функции есть тип, но бывают функции, которые могут быть опреде-
лены на аргументах разных типов; по сути, они описывают схожие понятия, но определены для значений
разных типов. Например, функция сравнения на равенство, говорящая о том, что два значения одного типа
a равны, имеет тип a ->a -> Bool, или функция печати выражения имеет тип a -> String, но что такое
a в этих типах? Тип a является любым типом, для которого сравнение на равенство или печать (преобразо-
вание в строку) имеют смысл. Это понятие как раз и кодируется в классах типов. Классы типов(type class)
позволяют определять функции с одинаковым именем для разных типов.
У классов типов есть имена. Также как и имена классов, они начинаются с большой буквы. Например,
класс сравнений на равенство называется Eq(от англ. equals – равняется), а класс печати выражений имеет
Читать дальше