Определим экземпляр для чисел Пеано, но давайте сначала разберём функции по частям.
Сложение
Начнём со сложения:
instance Num Nat where
(+) a Zero
= a
(+) a (Succ b) = Succ (a + b)
Первое уравнение говорит о том, что, если второй аргумент равен нулю, то мы вернём первый аргумент
в качестве результата. Во втором уравнении мы “перекидываем” конструктор Succ из второго аргумента за
пределы суммы. Схематически вычисление суммы можно представить так:
3+2
1 + (1 + 3)
Все наши числа имеют вид 0 или 1+
составить число в этом же виде, для этого мы последовательно перекидываем $(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
1 + (3+2)
1 + (1 + 1 + 3)
Операции 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
Интерпретатор говорит о том, тип значения 1 является некоторым типом из класса Num. В Haskell обозна-
чения для чисел перегружены. Когда мы пишем 1 на самом деле мы пишем (fromInteger (1::Integer)).
Поэтому теперь мы можем не писать цепочку Succ-ов, а воспользоваться методом fromInteger, для этого
сохраним определение экземпляра для Num и загрузим обновлённый модуль в интерпретатор:
[1 of 1] Compiling Nat
( Nat. hs, interpreted )
Ok, modules loaded: Nat.
*Nat> 7 :: Nat
Succ (Succ (Succ (Succ (Succ (Succ (Succ Zero))))))
*Nat> (2 + 2) :: Nat
Succ (Succ (Succ (Succ Zero)))
*Nat> 2 * 3 :: Nat
Succ (Succ (Succ (Succ (Succ (Succ Zero)))))
Вы можете убедиться насколько гибкими являются числа в Haskell:
*Nat> (1 + 1) :: Nat
Succ (Succ Zero)
*Nat> (1 + 1) :: Double
2.0
*Nat> 1 + 1
2
Мы выписали три одинаковых выражения и получили три разных результата, меняя объявление типов. В
последнем выражении тип был приведён к Integer. Это поведение интерпретатора по умолчанию. Если мы
напишем:
*Nat> let q = 1 + 1
*Nat> :t q
q :: Integer
Мы видим, что значение q было переведено в Integer, это происходит лишь в интерпретаторе, если такая
переменная встретится в программе и компилятор не сможет определить её тип из контекста, произойдёт
ошибка проверки типов, компилятор скажет, что он не смог определить тип. Помочь компилятору можно,
добавив объявление типа с помощью конструкции (v :: T).
Посмотрим ещё раз на определение экземпляра Num для Nat целиком:
instance Num Nat where
(+) a Zero
= a
(+) a (Succ b) = Succ (a + b)
(*) a Zero
= Zero
(*) a (Succ b) = a + (a * b)
fromInteger 0 = Zero
fromInteger n = Succ (fromInteger (n-1))
abs
x
= x
signum Zero = Zero
signum _
= Succ Zero
negate _ = error ”negate is undefined for Nat”
34 | Глава 2: Первая программа
Класс Fractional. Деление
Деление определено в классе Fractional:
*Nat>:m Prelude
Prelude> :i Fractional
class Num a => Fractional a where
(/) :: a -> a -> a
recip :: a -> a
fromRational :: Rational -> a
-- Defined in ‘GHC.Real’
instance Fractional Float -- Defined in ‘GHC.Float’
instance Fractional Double -- Defined in ‘GHC.Float’
Функция recip, это аналог negate для Num. Она делит единицу на данное число. Функция fromRational
строит число данного типа из дробного числа. Если мы пишем 2, то к нему подспудно будет применена