одной компоненты отношения однозначно следует другая, такое отношение принято называть функцией.
Например обычную функцию одного аргумента можно представить как множество пар (x, f x). Для того
чтобы множество таких пар было функцией необходимо, чтобы выполнялось свойство:
forall x, y.
x ==
y => f x == f yДля одинаковых входов мы получаем одинаковые выходы. С функциональными зависимостями мы мо-
жем ввести такое ограничение на классы с несколькими аргументами. Рассмотрим практический пример.
Библиотека Boolean
определяет обобщённые логические значения,class Boolean
b wheretrue, false ::
bnotB
::
b -> b(&&*
), (||*) :: b -> b -> bЛогические значения определены в терминах простейших операций, теперь мы можем обобщить связку
if-then-else
и классы Eq и Ord:class Boolean
bool => IfB bool a | a -> bool whereifB ::
bool -> a -> a -> aclass Boolean
bool => EqB bool a | a -> bool where(==*
), (/=*) :: a -> a -> boolclass Boolean
bool => OrdB bool a | a -> bool where(<*
), (>=*), (>*), (<=*) :: a -> a -> boolКаждый из классов определён на двух типах. Один из них играет роль обычных логических значений, а
второй тип~– это такой же параметр как и в обычных классах из модуля Prelude
. В этих определениях намвстретилась новая конструкция: за переменными класса через разделитель “или” следует что-то похожее на
тип функции. В этом типе мы говорим, что из типа a следует тип bool, или тип a однозначно определяет тип
bool. Эта информация помогает компилятору выводить типы. Если он встретит в тексте выражение v =
a <*b и тип одного из аргументов a или b известен, то тип v будет определён по зависимости.
Зачем нам может понадобиться такая система классов? Например, с ней мы можем определить экземпляр
Boolean
для предикатов или функций вида a -> Bool и затем определить три остальных класса для функцийвида a ->
b. Мы сравниваем не отдельные логические значения, а функции которые возвращают логическиезначения. Так в выражении ifB c t e функция c играет роль “маски”, если на данном значении функция c
вернт истину, то мы воспользуемся значением функции t, иначе возьмём результат из функции e. Например
так мы можем определить функцию модуля:
*Boolean> let
absolute = ifB (> 0) id negate*Boolean>
map absolute [-10 .. 10][10,9,8,7,6,5,4,3,2,1,0,1,2,3,4,5,6,7,8,9,10]
Расширения | 261
Мы можем указать несколько зависимостей (через запятую) или зависимость от нескольких типов (через
пробел, слева от стрелки):
class C
a b c | a -> b, b c -> a where...
Отметим, что многие функциональные зависимости можно выразить через семейства типов. Пример из
библиотеки Boolean
можно было бы записать так:class Boolean
a wheretrue, false
::
a(&&*
), (||*)::
a -> a -> aclass Boolean
(B a) => IfB a wheretype B
a :: *ifB ::
(B a) -> a -> a -> aclass IfB
a => EqB a where(==*
), (/=*) :: a -> a -> B aclass IfB
a => OrdB a where(<*
), (>*), (>=*), (<=*) :: a -> a -> B aИсторически первыми в Haskell появились функциональные зависимости. Поэтому некоторые пакеты на
Hackage
определены в разных вариантах. Семейства типов используются более охотно.Ограничение мономорфизма
В Haskell мы можем не писать типы функций. Они будут выведены компилятором автоматически. Но
написание типов функций считается признаком хорошего стиля. Поскольку по типам можно догадаться чем
функция занимается. Но есть в правиле вывода типов одно исключение. Если мы напишем:
f =
showТо компилятор сообщит нам об ошибке. Это выражение приводит к ошибке, которая вызвана ограничени-
ем мономорфизма. Мы говорили о нём в главе о типах. Часто в сильно обобщённых библиотеках, с больши-
ми зависимостями в типах выписывать типы крайне неудобно. Например в библиотеке создания парсеров
Parsec
. С этим ограничением приходится писать огромные объявления типов для крохотных выражений.Что-то вроде:
fun ::
(Stream s m t, Show t) => ParsecT s u m a -> ParsecT s u m [a]fun =
g . h (q x) yИ так для любого выражения. В этом случае лучше просто выключить ограничение, добавив в начало
файла:
{-# Language NoMonomorphismRestriction #-}
Полиморфизм высших порядков
Когда мы говорили об ST
нам встретилась функция с необычным типом:runST ::
(forall s. ST s a) -> aСлово forall обозначает для любых. Любой полиморфный тип в Haskell подразумевает, что он определён
для любых типов. Например, когда мы пишем:
reverse ::
[a] -> [a]map
::
(a -> b) -> [a] -> [b]На самом деле мы пишем:
reverse ::
forall a. [a] -> [a]map
::
forall a b. (a -> b) -> [a] -> [b]262 | Глава 17: Дополнительные возможности
По названию слова forall может показаться, что оно несёт в себе много свободы. Оно говорит о том, что
функция определена для любых типов. Но если присмотреться, то эта свобода оказывается жёстким огра-