class AdditiveGroup
v => VectorSpace v wheretype Scalar
v:: *
(*^
):: Scalar
v -> v -> vЛинейное пространство это математическая структура, объектами которой являются вектора и скаля-
ры. Для векторов определена операция сложения, а для скаляров операции сложения и умножения. Кроме
того определена операция умножения вектора на скаляр. При этом должны выполнятся определённые свой-
ства. Мы не будем подробно на них останавливаться, вкратце заметим, что эти свойства говорят о том, что
мы действительно пользуемся операциями сложения и умножения. В классе 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 ^+^ vV2
2 4*Point2D>
3 *^ v ^+^ vV2
4 8*Point2D>
negateV $ 3 *^ v ^+^ vV2
(-4) (-8)Семейства функций дают возможность организовывать вычисления на типах. Посмотрим на такой клас-
сический пример. Реализуем в типах числа Пеано. Нам понадобятся два типа. Один для обозначения нуля,
а другой для обозначения следующего элемента:
{-# Language TypeFamilies, EmptyDataDecls #-}
module Nat where
data Zero
data Succ
aЗначения этих типов нам не понадобятся, поэтому мы воспользуемся расширением EmptyDataDecls
, ко-торое позволяет определять типы без значенеий. Значениями будут комбинации типов. Мы определим опе-
рации сложения и умножения для чисел. Для начала определим сложение:
type
family Add a b :: *type instance Add
a Zero=
atype 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
. Семейства типов фактически позволяют нам определять функции на типах. Этоведёт к тому, что алгоритм вывода типов становится неопределённым. Если типы правильные, то компиля-