Spec-Zone.ru › Haskell 9

6.8.7. Функциональные зависимости

FunctionalDependencies
Подразумевает:

MultiParamTypeClasses

С момента:

6.8.1

Разрешает использование функциональных зависимостей в объявлениях классов.

Функциональные зависимости реализованы так, как описал Марк Джонс в [Jones2000].

Функциональные зависимости вводятся вертикальной чертой в синтаксисе объявления класса; например:

class (Monad m) => MonadState s m | m -> s where ...

class Foo a b c | a b -> c where ...

Дополнительную документацию можно найти на вики Haskell.

[Jones2000]

“Type Classes with Functional Dependencies”, Марк П. Джонс, В Трудах 9-й Европейской конференции по программированию, ESOP 2000, Берлин, Германия, март 2000 г., Springer-Verlag LNCS 1782, .

6.8.7.1. Правила для функциональных зависимостей

В объявлении класса все типы переменных класса должны быть достижимы (в смысле, упомянутом в Снятие ограничений на контексты классов) из свободных переменных каждого типа метода. Например:

class Coll s a where
  empty  :: s
  insert :: s -> a -> s

неверно, потому что тип empty не упоминает a. Функциональные зависимости могут сделать переменную типа достижимой:

class Coll s a | s -> a where
  empty  :: s
  insert :: s -> a -> s

Альтернативно, Coll можно переписать как

class Coll s a where
  empty  :: s a
  insert :: s a -> a -> s a

что устанавливает связь между типом коллекции a (а именно (s a)) и типом элемента a. Иногда это не срабатывает, в этом случае можно разделить класс следующим образом:

class CollE s where
  empty  :: s

class CollE s => Coll s a where
  insert :: s -> a -> s

6.8.7.2. Общие сведения о функциональных зависимостях

Следующее описание мотивации и использования функциональных зависимостей взято из руководства пользователя Hugs, воспроизведено здесь (с небольшими изменениями) с любезного разрешения Марка Джонса.

Рассмотрим следующий класс, предназначенный как часть библиотеки типов коллекций:

class Collects e ce where
    empty  :: ce
    insert :: e -> ce -> ce
    member :: e -> ce -> Bool

Переменная типа e здесь представляет тип элемента, а ce — тип контейнера. В рамках этой структуры мы можем определить экземпляры этого класса для списков или характеристических функций (обе из которых могут использоваться для представления коллекций любого типа равенства), битовых наборов (которые могут использоваться для представления коллекций символов) или хеш-таблиц (которые могут использоваться для представления любой коллекции, элементы которой имеют функцию хеширования). Опуская стандартные детали реализации, это приведет к следующим объявлениям:

instance Eq e => Collects e [e] where ...
instance Eq e => Collects e (e -> Bool) where ...
instance Collects Char BitSet where ...
instance (Hashable e, Collects a ce)
           => Collects e (Array Int ce) where ...

Все это выглядит многообещающе; у нас есть класс и ряд интересных реализаций. К сожалению, есть некоторые серьезные проблемы с объявлением класса. Во-первых, пустая функция имеет неоднозначный тип:

empty :: Collects e ce => ce

Под «неоднозначным» мы подразумеваем, что имеется переменная типа e, которая появляется слева от символа =>, но не справа. Проблема в том, что, согласно теоретическим основам перегрузки Haskell, мы не можем гарантировать хорошо определенную семантику для любого члена с неоднозначным типом.

Мы можем обойти эту конкретную проблему, удалив пустой член из объявления класса. Однако, хотя оставшиеся члены, insert и member, не имеют неоднозначных типов, мы все равно сталкиваемся с проблемами при попытке использовать их. Например, рассмотрим следующие две функции:

f x y = insert x . insert y
g     = f True 'a'

для которых GHC выводит следующие типы:

f :: (Collects a c, Collects b c) => a -> b -> c -> c
g :: (Collects Bool c, Collects Char c) => c -> c

Обратите внимание, что тип для f позволяет двум параметрам x и y иметь разные типы, даже если он пытается вставить каждое из двух значений, одно за другим, в одну и ту же коллекцию. Если мы пытаемся смоделировать коллекции, содержащие только один тип значения, то это явно неточный тип. Что еще хуже, определение для g принимается без вывода ошибки типа. В результате ошибка в этом коде не будет отмечена в месте ее появления. Вместо этого она появится только при попытке использования g, которая может даже находиться в другом модуле.

6.8.7.2.1. Попытка использования классов конструкторов

Столкнувшись с описанными выше проблемами, некоторые программисты Haskell могут быть искушены использовать что-то вроде следующей версии объявления класса:

class Collects e c where
   empty  :: c e
   insert :: e -> c e -> c e
   member :: e -> c e -> Bool

Ключевое отличие здесь заключается в том, что мы абстрагируемся от конструктора типа c, который используется для формирования типа коллекции c e, а не от самого типа этой коллекции, представленного ce в исходном объявлении класса. Это позволяет избежать немедленных проблем, о которых мы упомянули выше: у empty тип Collects e c => c e, который не неоднозначен.

Функция f из предыдущего раздела имеет более точный тип:

f :: (Collects e c) => e -> e -> c e -> c e

Функция g из предыдущего раздела теперь отклоняется с ошибкой типа, как и ожидалось, поскольку тип f не позволяет двум аргументам иметь разные типы. Это пример класса с несколькими параметрами, который на практике действительно работает довольно хорошо, без проблем с неоднозначностью. Однако есть подвох. Эта версия класса Collects ни в коем случае не так обща, как первоначальный класс, по-видимому: только один из четырех экземпляров для Collects выше может быть использован с этой версией Collects, поскольку только один из них — экземпляр для списков — имеет тип коллекции, который можно записать в форме c e, для некоторого конструктора типов c, и тип элемента e.

6.8.7.2.2. Добавление функциональных зависимостей

Для получения более полезной версии класса Collects, GHC предоставляет механизм, который позволяет программистам указывать зависимости между параметрами класса с несколькими параметрами (Для читателей, интересующихся теоретическими основами и предыдущими работами: использование информации о зависимостях можно рассматривать как обобщение предложения «параметрические типы классов», выдвинутого Чэном, Худаком и Одеромски, или как частный случай более поздней схемы Марка Джонса для «улучшения» квалифицированных типов. Основные идеи также обсуждаются в более теоретической и абстрактной обстановке в рукописи [Jones1999], где они определяются как одна точка в общем пространстве дизайна для систем неявной параметризации). Начнем с абстрактного примера, рассмотрим объявление, такое как:

class C a b where ...
[Jones1999]

«Exploring the Design Space for Type-based Implicit Parameterization», Mark P. Jones, Oregon Graduate Institute of Science & Technology, Технический отчет, июль 1999 года.

Это говорит нам просто, что C может быть представлен как бинарное отношение на типах (или конструкторах типов, в зависимости от видов a и b). Дополнительные пункты могут быть включены в определение классов, чтобы добавить информацию о зависимостях между параметрами, как в следующих примерах:

class D a b | a -> b where ...
class E a b | a -> b, b -> a where ...

Используемая здесь запись a -> b между символами | и where — не следует путать с типом функции — указывает, что параметр a однозначно определяет параметр b, и может читаться как «a определяет b». Таким образом, D — это не просто отношение, а фактически (частичная) функция. Аналогично, из двух зависимостей, включенных в определение E, мы видим, что E представляет (частичное) взаимно однозначное соответствие между типами.

В более общем случае зависимости принимают вид x1 ... xn -> y1 ... ym, где x1, …, xn, и y1, …, yn являются переменными типа с n>0 и m>=0, что означает, что параметры y однозначно определяются параметрами x. Пробелы могут использоваться в качестве разделителей, если на одной стороне зависимости появляется более одной переменной, как в t -> a b. Обратите внимание, что класс может быть помечен несколькими зависимостями, используя запятые в качестве разделителей, как в определении E выше. Некоторые зависимости, которые мы можем записать в этой нотации, избыточны и будут отклонены, потому что они не служат никакой полезной цели, а могут вместо этого указывать на ошибку в программе. Примеры таких зависимостей включают a -> a, a -> a a, a ->, и т. д. Также может быть избыточность, если задано несколько зависимостей, как в a->b, b->c, a->c, и в которых некоторый подмножество подразумевает оставшиеся зависимости. Такие примеры не рассматриваются как ошибки. Обратите внимание, что зависимости появляются только в объявлениях классов, а не в любой другой части языка. В частности, синтаксис для объявлений экземпляров, ограничений классов и типов полностью не изменяется.

Включая зависимости в объявление класса, мы предоставляем программисту механизм для более точного описания каждого класса с несколькими параметрами. С другой стороны, компилятор отвечает за обеспечение того, чтобы набор экземпляров, присутствующих в области видимости в любой момент программы, соответствовал объявленным зависимостям. Например, следующая пара объявлений экземпляров не может появиться вместе в одной области видимости, потому что они нарушают зависимость для D, хотя каждый из них по отдельности был бы приемлем:

instance D Bool Int where ...
instance D Bool Char where ...

Также обратите внимание, что следующее объявление запрещено даже само по себе:

instance D [a] b where ...

Проблема здесь в том, что этот экземпляр позволит одному конкретному выбору [a] быть связанным с более чем одним выбором для b, что противоречит зависимости, указанной в определении D.

Более общо, это означает, что в любом экземпляре вида:

instance D t s where ...

для некоторых конкретных типов t и s, единственными переменными, которые могут появляться в s являются те, которые появляются в t, и следовательно, если тип t известен, то s будет однозначно определен.

Преимущества включения информации о зависимостях заключаются в том, что они позволяют определить более общие классы с несколькими параметрами без проблем с неоднозначностью и с преимуществом более точных типов. Чтобы проиллюстрировать это, мы возвращаемся к примеру класса коллекции и помечаем исходное определение Collects простой зависимостью:

class Collects e ce | ce -> e where
   empty  :: ce
   insert :: e -> ce -> ce
   member :: e -> ce -> Bool

Зависимость ce -> e здесь указывает, что тип e элементов однозначно определяется типом коллекции ce . Обратите внимание, что оба параметра Collects имеют вид Type; здесь нет конструкторских классов. Также обратите внимание, что все экземпляры Collects , которые мы предоставили ранее, могут использоваться вместе с этим новым определением.

Что насчёт проблем неоднозначности, с которыми мы столкнулись с исходным определением? Пустая функция по-прежнему имеет тип Collects e ce => ce, но её больше не нужно рассматривать как неоднозначный тип: хотя переменная e не появляется справа от символа =>, зависимость для класса Collects говорит нам, что она однозначно определяется ce, которая появляется справа от символа => . Таким образом, контекст, в котором используется пустая функция, по-прежнему может предоставить достаточно информации для определения типов для ce и e без неоднозначности. В более общем случае тип нужно рассматривать как неоднозначный только в том случае, если он содержит переменную слева от символа =>, которая не однозначно определяется (непосредственно или косвенно) переменными справа.

Зависимости также помогают получить более точные типы для определённых пользователем функций, а следовательно, позволяют обнаруживать ошибки раньше и предоставляют программисту менее громоздкие типы для работы. Вспомните предыдущее определение функции f:

f x y = insert x y = insert x . insert y

для которого мы изначально получили тип:

f :: (Collects a c, Collects b c) => a -> b -> c -> c

Однако, с учётом информации о зависимостях для Collects, мы можем вывести, что a и b должны быть равны, поскольку оба они появляются как второй параметр в ограничении Collects с тем же первым параметром c . Поэтому мы можем вывести более короткий и точный тип для f:

f :: (Collects a c) => a -> a -> c -> c

Аналогичным образом, предыдущее определение g теперь будет отмечено как ошибка типа.

Хотя мы привели здесь лишь несколько примеров, должно быть ясно, что добавление информации о зависимостях может помочь сделать классы с несколькими параметрами более полезными на практике, устраняя проблемы неоднозначности и позволяя создавать более обширные наборы объявлений экземпляров.

© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/9.12.1/docs/users_guide/exts/functional_dependencies.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API