haskell-notes

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

мы действительно пользуемся операциями сложения и умножения. В классе VectorSpace мы видим новую

конструкцию, объявление типа. Мы говорим, что есть производный тип, который следует из v. Далее через

двойное двоеточие мы указываем его вид. В данном случае это простой тип без параметров.

Вид (kind) это тип типа. Простой тип без параметра обозначается звёздочкой. Тип с параметром обозна-

чается как функция * -> *. Если бы тип принимал два параметра, то он обозначался бы * -> * -> *. Также

параметры могут быть не простыми типами а типами с параметрами, например тип, который обозначает

композицию типов:

newtype O f g a = O { unO :: f (g a) }

имеет вид (* -> *) -> (* -> *) -> * -> *.

Определим класс векторов на двумерной сетке и сделаем его экземпляром класса VectorSpace. Для нача-

ла создадим новый модуль с активным расширением TypeFamilies и запишем в него классы для линейного

пространства

{-# Language TypeFamilies #-}

module Point2D where

class AdditiveGroup v where

Теперь определим новый тип:

data V2 = V2 Int Int

deriving (Show, Eq)

Сделаем его экземпляром класса AdditiveGroup:

instance AdditiveGroup V2 where

zeroV

= V2 0 0

(V2 x y)

^+^ (V2 x’ y’)

= V2 (x+x’) (y+y’)

negateV (V2 x y)

= V2 (x) (y)

Мы складываем и вычитаем значения в каждом из элементов кортежа. Нейтральным элементом от-

носительно сложения будет кортеж, состоящий из двух нулей. Теперь определим экземпляр для класса

VectorSpace. Поскольку кортеж состоит из двух целых чисел, скаляр также будет целым числом:

instance VectorSpace V2 where

type Scalar V2 = Int

s *^ (V2 x y) = V2 (s*x) (s*y)

Попробуем вычислить что-нибудь в интерпретаторе:

258 | Глава 17: Дополнительные возможности

*Prelude> :l Point2D

[1 of 1] Compiling Point2D

( Point2D. hs, interpreted )

Ok, modules loaded: Point2D.

*Point2D> let v =

V2 1 2

*Point2D> v ^+^ v

V2 2 4

*Point2D> 3 *^ v ^+^ v

V2 4 8

*Point2D> negateV $ 3 *^ v ^+^ v

V2 (4) (8)

Семейства функций дают возможность организовывать вычисления на типах. Посмотрим на такой клас-

сический пример. Реализуем в типах числа Пеано. Нам понадобятся два типа. Один для обозначения нуля,

а другой для обозначения следующего элемента:

{-# Language TypeFamilies, EmptyDataDecls #-}

module Nat where

data Zero

data Succ a

Значения этих типов нам не понадобятся, поэтому мы воспользуемся расширением EmptyDataDecls, ко-

торое позволяет определять типы без значенеий. Значениями будут комбинации типов. Мы определим опе-

рации сложения и умножения для чисел. Для начала определим сложение:

type family Add a b :: *

type instance Add a Zero

= a

type instance Add a (Succ b)

= Succ (Add a b)

Первой строчкой мы определили семейство функций Add, у которого два параметра. Определение семей-

ства типов начинается с ключевой фразы type family. За двоеточием мы указали тип семейства. В данном

случае это простой тип без параметра. Далее следуют зависимости типов для семейства Add. Зависимости

типов начинаются с ключевой фразы type instance. В аргументах мы словно пользуемся сопоставлением с

образцом, но на этот раз на типах. Первое уравнение:

type instance Add a Zero

= a

Говорит о том, что если второй аргумент имеет тип ноль, то мы вернём первый аргумент. Совсем как в

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

составляем рекурсивное уравнение:

type instance Add a (Succ b)

= Succ (Add a b)

Точно также мы можем определить и умножение:

type family Mul a b :: *

type instance Mul a Zero

= Zero

type instance Mul a (Succ b)

= Add a (Mul a b)

При этом нам придётся подключить ещё одно расширение UndecidableInstances, поскольку во втором

уравнении мы подставили одно семейство типов в другое. Этот флаг часто используется в сочетании с рас-

ширением TypeFamilies. Семейства типов фактически позволяют нам определять функции на типах. Это

ведёт к тому, что алгоритм вывода типов становится неопределённым. Если типы правильные, то компиля-

тор сможет это установить, но если они окажутся неправильными, может возникнуть такая ситуация, что

компилятор зациклится и будет бесконечно долго искать соответствие одного типа другому. Теперь про-

верим результаты. Для этого мы создадим специальный класс, который будет переводить значения-типы в

обычные целочисленные значения:

class Nat a where

toInt :: a -> Int

instance Nat Zero where

toInt = const 0

instance Nat a => Nat (Succ a) where

toInt x = 1 + toInt (proxy x)

proxy :: f a -> a

proxy = undefined

Расширения | 259

Мы определили для каждого значения-типа экземпляр класса Nat, в котором мы можем переводить типы

в числа. Функция proxy позволяет нам извлечь значение из типа-конструктора Succ, так мы поясняем ком-

пилятору тип значения. При этом мы нигде не пользуемся значениями типов Zero и Succ, ведь у этих типов

нет значений. Поэтому в экземпляре для Zero мы пользуемся постоянной функцией const.

Теперь посмотрим, что у нас получилось:

Prelude> :l Nat

*Nat> let x = undefined :: (Mul (Succ (Succ (Succ Zero))) (Succ (Succ Zero)))

Страницы: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162