тора, которых тем не менее достаточно для описания множества натуральных чисел:
module Nat where
data Nat = Zero | Succ Nat
deriving (Show, Eq, Ord)
Конструктор Zero указывает на число ноль, а (Succ n) на число следующее за данным числом n. В
последней строчке мы видим новый класс Ord, этот класс содержит операции сравнения на больше/меньше:
Prelude> :i Ord
class (Eq a) => Ord a where
compare :: a -> a -> Ordering
(< ) :: a -> a -> Bool
(>=) :: a -> a -> Bool
(> ) :: a -> a -> Bool
(<=) :: a -> a -> Bool
max :: a -> a -> a
min :: a -> a -> a
Автоматический вывод экземпляров классов типов | 31
Тип Ordering кодирует результаты сравнения:
Prelude> :i Ordering
data Ordering = LT | EQ | GT
— Defined in GHC.Ordering
Он содержит конструкторы, соответствующие таким понятиям как меньше, равно и больше.
Класс Eq. Сравнение на равенство
Вспомним определение класса Eq:
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
a == b = not (a /= b)
a /= b = not (a == b)
Появились две детали, о которых я умолчал в предыдущей главе. Это две последние строчки. В них
мы видим определение == через /= и наоборот. Это определения методов по умолчанию. Такие определения
дают нам возможность определять не все методы класса, а лишь часть основных, а все остальные мы получим
автоматически из определений по умолчанию.
Казалось бы почему не оставить в классе Eq один метод а другой метод определить в виде отдельной
функции:
class Eq a where
(==) :: a -> a -> Bool
(/=) :: Eq a => a -> a -> Bool
a /= b = not (a == b)
Так не делают по соображениям эффективности. Есть типы для которых проще вычислить /= чем ==.
Тогда мы определим тот метод, который нам проще вычислять и второй получим автоматически.
Набор основных методов, через которые определены все остальные называют минимальным полным опре-
делением (minimal complete definition) класса. В случае класса Eq это метод == или метод /=.
Мы уже вывели экземпляр для Eq, поэтому мы можем пользоваться методами == и /= для значений типа
Nat:
*Calendar> :l Nat
[1 of 1] Compiling Nat
( Nat. hs, interpreted )
Ok, modules loaded: Nat.
*Nat> Zero == Succ (Succ Zero)
False
it :: Bool
*Nat> Zero /= Succ (Succ Zero)
True
it :: Bool
Класс Num. Сложение и умножение
Сложение и умножение определены в классе Num. Посмотрим на его определение:
*Nat> :i Num
class (Eq a, Show a) => Num a where
(+) :: a -> a -> a
(*) :: a -> a -> a
(—) :: a -> a -> a
negate :: a -> a
abs :: a -> a
signum :: a -> a
fromInteger :: Integer -> a
— Defined in GHC.Num
Методы (+), (*), (—) в представлении не нуждаются, метод negate является унарным минусом, его можно
определить через (—) так:
32 | Глава 2: Первая программа
negate x = 0 — x
Метод abs является модулем числа, а метод signum возвращает знак числа, метод fromInteger позволяет
создавать значения данного типа из стандартных целых чисел Integer.
Этот класс устарел, было бы лучше сделать отельный класс для сложения и вычитания и отдельный
класс для умножения. Также контекст класса, часто становится помехой. Есть объекты, которые нет смысла
печатать но, есть смысл определить на них сложение и умножение. Но пока в целях совместимости с уже
написанным кодом, класс Num остаётся прежним.
Определим экземпляр для чисел Пеано, но давайте сначала разберём функции по частям.
Сложение
Начнём со сложения:
instance Num Nat where
(+) a Zero
= a
(+) a (Succ b) = Succ (a + b)
Первое уравнение говорит о том, что, если второй аргумент равен нулю, то мы вернём первый аргумент
в качестве результата. Во втором уравнении мы “перекидываем” конструктор Succ из второго аргумента за
пределы суммы. Схематически вычисление суммы можно представить так:
3+2 > 1 + (3+1) > 1 + (1 + (3+0))
1 + (1 + 3) > 1 + (1 + (1 + (1 + (1 + 0)))) > 5
Все наши числа имеют вид 0 или 1+ n, мы принимаем на вход два числа в таком виде и хотим в результате
составить число в этом же виде, для этого мы последовательно перекидываем $(1+) в начало выражения из
второго аргумента.
Вычитание
Операция отрицания не имеет смысла, поэтому мы воспользуемся специальной функцией error ::
String -> a, она принимает строку с сообщением об ошибке, при её вычислении программа остановит-
ся с ошибкой и сообщение будет выведено на экран.
negate _ = error ”negate is undefined for Nat”
Умножение
Теперь посмотрим на умножение:
(*) a Zero
= Zero
(*) a (Succ b) = a + (a * b)
В первом уравнении мы вернём ноль, если второй аргумент окажется нулём, а во втором мы за каждый
конструктор Succ во втором аргументе прибавляем к результату первый аргумент. В итоге, после вычисле-
ния a * b мы получим аргумент a сложенный b раз. Это и есть умножение. При этом мы воспользовались
операцией сложения, которую только что определили. Посмотрим на схему вычисления:
3*2 > 3 + (3*1) > 3 + (3 + (3*0)) > 3 + (3+0) > 3+3 >
1 + (3+2) > 1 + (1 + (3+1)) > 1 + (1 + (1 + (3+0))) >
1 + (1 + 1 + 3) > 1 + (1 + (1 + (1 + (1 + (1 + 0))))) > 6
Операции abs и signum
Поскольку числа у нас положительные, то методы abs и signum почти ничего не делают: