тор сможет это установить, но если они окажутся неправильными, может возникнуть такая ситуация, что
компилятор зациклится и будет бесконечно долго искать соответствие одного типа другому. Теперь про-
верим результаты. Для этого мы создадим специальный класс, который будет переводить значения-типы в
обычные целочисленные значения:
class Nat
a wheretoInt ::
a -> Intinstance Nat Zero where
toInt =
const 0instance Nat
a => Nat (Succ a) wheretoInt x =
1 + toInt (proxy x)proxy ::
f a -> aproxy =
undefinedРасширения | 259
Мы определили для каждого значения-типа экземпляр класса Nat
, в котором мы можем переводить типыв числа. Функция proxy позволяет нам извлечь значение из типа-конструктора Succ
, так мы поясняем ком-пилятору тип значения. При этом мы нигде не пользуемся значениями типов Zero
и Succ, ведь у этих типовнет значений. Поэтому в экземпляре для Zero
мы пользуемся постоянной функцией const.Теперь посмотрим, что у нас получилось:
Prelude> :
l Nat*Nat> let
x = undefined :: (Mul (Succ (Succ (Succ Zero))) (Succ (Succ Zero)))*Nat>
toInt x6
Видно, что с помощью класса Nat
мы можем извлечь значение, закодированное в типе. Зачем нам этистранные типы-значения? Мы можем использовать их в двух случаях. Мы можем кодировать значения в типе
или проводить более тонкую проверку типов.
Помните когда-то мы определяли функции для численного интегрирования. Там точность метода была
жёстко задана в тексте программы:
dt :: Fractional
a => adt =
1e-3-- метод Эйлера
int :: Fractional
a => a -> [a] -> [a]int x0 ~
(f:fs) = x0 : int (x0 + dt * f) fsВ этом примере мы можем создать специальный тип потоков, у которых шаг дискретизации будет зако-
дирован в типе.
data Stream
n a = a :& Stream n aПараметр n кодирует точность. Теперь мы можем извлекать точность из типа:
dt ::
(Nat n, Fractional a) => Stream n a -> adt xs =
1 / (fromIntegral $ toInt $ proxy xs)where
proxy :: Stream n a -> nproxy =
undefinedint ::
(Nat n, Fractional a) => a -> Stream n a -> Stream n aint x0 ~
(f:& fs) = x0 :& int (x0 + dt fs * f) fsТеперь посмотрим как мы можем сделать проверку типов более тщательной. Представим, что у нас есть
тип матриц. Известно, что сложение определено только для матриц одинаковой длины, а для умножения
матриц число столбцов одной матрицы должно совпадать с числом колонок другой матрицы. Мы можем
отразить все эти зависимости в целочисленных типах:
data Mat
n m a = ...instance Num
a => AdditiveGroup (Mat n m a) wherea ^+^
b= ...
zeroV
= ...
negateV a
= ...
mul :: Num
a => Mat n m a -> Mat m k a -> Mat n k aПри таких определениях мы не сможем сложить матрицы разных размеров. Причём ошибка будет вычис-
лена до выполнения программы. Это освобождает от проверки границ внутри алгоритма умножения матриц.
Если алгоритм запустился, то мы знаем, что размеры аргументов соответствуют.
Скоро в ghc появится поддержка чисел на уровне типов. Это будет специальное расширение
TypeLevelNats
, при включении которого можно будет пользоваться численными литералами в типах,также будут определены операции-семейства типов на численных типах с привычными именами +
, *.Классы с несколькими типами
Рассмотрим несколько полезных расширений, относящихся к определению классов и экземпляров клас-
сов. Расширение MultiParamTypeClasses
позволяет объявлять классы с несколькими аргументами. Напримервзгляните на такой класс:
class Iso
a b whereto
::
a -> bfrom
::
b -> aТак мы можем определить изоморфизм между типами a и b
260 | Глава 17: Дополнительные возможности
Экземпляры классов для синонимов
Расширение TypeSynonymInstances
позволяет определять экземпляры для синонимов типов. Мы ужепользовались этим расширением, когда определяли рекурсивные типы через тип Fix
, там нам нужно бы-ло определить экземпляр Num
для синонима Nat:type Nat = Fix N
instance Num Nat where
В рамках стандарта все суперклассы должны быть простыми. Все они имеют вид T
a. Если мы хотим хотимиспользовать суперклассы с составными типами, нам придётся подключить расширение FlexibleContexts
.Этим расширением мы пользовались, когда определяли экземпляр Show
для Fix:instance Show
(f (Fix f)) => Show (Fix f) whereshow x =
”(” ++ show (unFix x) ++ ”)”Функциональные зависимости
Класс можно представить как множество типов, для которых определены данные операции. С появлением
расширения MultiParamTypeClasses
мы можем определять операции класса для нескольких типов. Так нашемножество классов превращается в отношение. Наш класс связывает несколько типов между собой. Если из