haskell-notes

Этот класс устарел, было бы лучше сделать отельный класс для сложения и вычитания и отдельный

класс для умножения. Также контекст класса, часто становится помехой. Есть объекты, которые нет смысла

печатать но, есть смысл определить на них сложение и умножение. Но пока в целях совместимости с уже

написанным кодом, класс 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 (n1))

Зачем он нужен? Попробуйте узнать тип числа 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 (n1))

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, то к нему подспудно будет применена

функция fromInteger, а если 2.0, то будет применена функция fromRational.

Стандартные числа

В этом подразделе мы рассмотрим несколько стандартных типов для чисел в Haskell. Все эти числа явля-

ются экземплярами основных численных классов. Тех, которые мы рассмотрели, и многих-многих других.

Целые числа

В Haskell предусмотрено два типа для целых чисел. Это Integer и Int. Чем они отличаются? Значения

типа Integer не ограничены, мы можем проводить вычисления с очень-очень-очень большими числами, если

памяти на нашем компьютере хватит. Числа из типа Int ограничены. Каждое число занимает определённый

размер в памяти компьютера. Диапазон значений для Int составляет от ? 229 до 229 ? 1. Вычисления с Int

более эффективны.

Действительные числа

Действительные числа бывают дробными (тип Rational), с ординарной точностью Float и с двойной

точностью Double. Числа из типа Float занимают меньше места, но они не такие точные как Double. Если вы

сомневаетесь, чем пользоваться, выбирайте Double, обычно Float используется только там, где необходимо

хранить огромные массивы чисел. В этом случае мы экономим много памяти.

Преобразование численных типов

Во многих языках программирования при сложении или умножении чисел разных типов проводится ав-

томатическое приведение типов. Обычно целые числа становятся действительными, Float превращается в

Double и так далее. Это противоречит строгой типизации, поэтому в Haskell этого нет:

Prelude> (1::Int) + (1::Double)

< interactive>:2:13:

Couldn’t match expected type Int’ with actual type Double’

In the second argument of ‘(+)’, namely ‘(1 :: Double)’

In the expression: (1 :: Int) + (1 :: Double)

In an equation for ‘it’: it = (1 :: Int) + (1 :: Double)

Любое преобразование типов контролируется пользователем. Мы должны вызвать специальную функ-

цию.

От целых к действительным: Часто возникает необходимость приведения целых чисел к действитель-

ным при делении. Для этого можно воспользоваться функцией: fromIntegral

Prelude> :i fromIntegral

fromIntegral :: (Integral a, Num b) => a -> b

— Defined in ‘GHC.Real’

Определим функцию поиска среднего между двумя целыми числами:

meanInt :: Int -> Int -> Double

meanInt a b = fromIntegral (a + b) / 2

Арифметика | 35

В этой функции двойка имеет тип Double. Обратите внимание на скобки: составной синоним всегда при-

Читай продолжение на следующей странице
Добавить комментарии