делением (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 почти ничего не делают:
abs
x
= x
signum Zero = Zero
signum _
= Succ Zero
Арифметика | 33
Перегрузка чисел
Остался последний метод fromInteger. Он конструирует значение нашего типа из стандартного:
fromInteger 0 = Zero
fromInteger n = Succ (fromInteger (n-1))
Зачем он нужен? Попробуйте узнать тип числа 1 в интерпретаторе:
*Nat> :t 1
1 :: (Num t) => t