Россия, Петерубрг, СПБ-ГПУ, 1998 |
Объявления и связывания имен
4.5 Статическая семантика связываний имен в функциях и образцах
В этом разделе рассматривается статическая семантика связываний имен в функциях и образцах в let -выражении или инструкции where.
4.5.1 Анализ зависимостей
Вообще статическая семантика задается обычными правилами вывода Хиндли-Милнера (Hindley-Milner). Преобразование на основе анализа зависимостей - первое, что выполняется для того, чтобы расширить полиморфизм. Две переменные, связанные посредством объявлений со значениями, находятся в одной группе объявлений, если
- они связаны одним и тем же связыванием имен в образце или
- их связывания имен взаимно рекурсивны (возможно, посредством некоторых других объявлений, которые также являются частью группы).
Применение следующих правил служит причиной того, что каждая let - или where -конструкция (включая where -конструкцию, которая задает связывание имен верхнего уровня в модуле) связывает переменные лишь одной группы объявлений, охватывая, таким образом, необходимый анализ зависимостей: (Сходное преобразование описано в книге Пейтона Джонса (Peyton Jones) [ "Спецификация производных экземпляров" ].)
- Порядок объявлений в where / let -конструкциях несущественен.
- let {d1; d2} in e = let {d1} in (let {d2} in e) (когда нет идентификатора, связанного в d2, d2 является свободным в d1 )
4.5.2 Обобщение
Система типов Хиндли-Милнера (Hindley-Milner) устанавливает типы в let -выражении в два этапа. Сначала определяется тип правой части объявления, результатом является тип без использования квантора всеобщности. Затем все переменные типа, которые встречаются в этом типе, помещаются под квантор всеобщности, если они не связаны со связанными переменными в окружении типа; это называется обобщением. В заключение определяется тип тела let -выражения.
Например, рассмотрим объявление
f x = let g y = (y,y) in ...
Типом определения g является a ->(a,a). На шаге обобщения g будет приписан полиморфный тип forall a. a -> (a,a), после чего можно переходить к определению типа части "...".
При определении типа перегруженных определений все ограничения на перегрузки из одной группы объявлений собираются вместе для того, чтобы создать контекст для типа каждой переменной, объявленной в группе. Например, в определении
f x = let g1 x y = if x>y then show x else g2 y x g2 p q = g1 q p in ...
Типом определений g1 и g2 является a -> a -> String, а собранные ограничения представлют собой Ord a (ограничение, возникшее из использования >) и Show a (ограничение, возникшее из использования show ). Переменные типа, встречающиеся в этой совокупности ограничений, называются переменными ограниченного типа.
На шаге обобщения g1 и g2 будет приписан тип
forall a. (Ord a, Show a) =>a ->a ->String
Заметим, что g2 перегружен так же, как и g1, хотя > и show находятся в определении g1.
Если программист укажет явные сигнатуры типов для более чем одной переменной в группе оъявлений, контексты этих сигнатур должны быть идентичны с точностью до переименования переменных типа.
4.5.3 Ошибки приведения контекста
Как сказано в разделе "Объявления и связывания имен" , контекст типа может ограничивать только переменную типа или применение переменной типа одним или более типами. Следовательно, типы, полученные при обобщении, должны иметь вид, в котором все ограничения контекста приведены к этой "главной нормальной форме". Рассмотрим, к примеру, определение
f xs y = xs == [y]
Его типом является
f :: Eq a => [a] -> a -> Bool
а не
f :: Eq [a] => [a] -> a -> Bool
Даже если равенство имеет место в типе списка, перед обобщением необходимо упростить контекст, используя объявление экземпляра для Eq на списках. Если в области видимости нет такого экземпляра - возникнет статическая ошибка.
Рассмотрим пример, который показывает необходимость ограничения вида C (m t), где m - одна из переменных типа, которая подвергается обобщению, то есть где класс C применяется к выражению с типами, которое не является переменной типа или конструктором типа. Рассмотрим
f :: (Monad m, Eq (m a)) => a -> m a -> Bool f x y = return x == y
Типом return является Monad m => a -> m a, типом(==) является Eq a => a -> a -> Bool. Следовательно, типом f должен являться (Monad m, Eq (m a)) => a -> m a -> Bool, и контекст не может быть более упрощен.
Объявление экземпляра, полученное из инструкции deriving типа данных (см. раздел "Объявления и связывания имен" ) должно, как любое объявление экземпляра, иметь простой контекст, то есть все ограничения должны иметь вид C a, где a - переменная типа. Например, в типе
data Apply a b = App (a b) deriving Show
выведенный экземпляр класса Show создаст контекст Show (a b), который нельзя привести и который не является простым контекстом, поэтому возникнет статическая ошибка.
4.5.4 Мономорфизм
Иногда невозможно выполнить обобщение над всеми переменными типа, используемыми в типе определения. Например, рассмотрим объявление
f x = let g y z = ([x,y], z) in ...
В окружении, где x имеет тип a, типом определения g является a -> b -> ([a],b). На шаге обобщения g будет приписан тип forall b. a -> b -> ([a],b) ; только b можно поставить под квантор всеобщности, потому что a встречается в окружении типа. Мы говорим, что тип g является мономорфным по переменной типа a.
Следствием такого мономорфизма является то, что первый аргумент всех применений g должен быть одного типа. Например, это выполняется, если "..." будет иметь тип
(g True, g False)
(это, кстати, привело бы к тому, что x будет иметь тип Bool ), но это не выполнится, если выражение будет иметь тип
(g True, g 'c')
Вообще, говорят, что тип forall u. cx => t является мономорфным по переменной типа a, если a является свободной в forall u. cx => t.
Стоит отметить, что предоставляемые Haskell явные сигнатуры типов не являются достаточно мощным средством для того, чтобы выразить типы, которые включают мономорфные переменные типов. Например, мы не можем записать
f x = let g :: a -> b -> ([a],b) g y z = ([x,y], z) in ...
потому что это утверждало бы, что g является полиморфным по a и b (раздел "Объявления и связывания имен" ). В этой программе для g можно задать сигнатуру типа, только если ее первый параметр ограничен типом, не содержащим переменные типа, например
g :: Int -> b -> ([Int],b)
Эта сигнатура также привела бы к тому, что x должен иметь тип Int.
4.5.5 Ограничение мономорфизма
Помимо стандартного ограничения Хиндли-Милнера (Hindley-Milner), описанного выше, Haskell устанавливает некоторые дополнительные ограничения на шаге обобщения, которые позволяют в отдельных случаях дальнейшее приведение полиморфизма.
Ограничение мономорфизма зависит от синтаксиса связывания переменной. Вспомним, что переменная связывается посредством связывания имен в функциях или связывания имен в образцах, и что связывание имен в простом образце - это связывание имен в образце, в котором образец состоит только из одной переменной (раздел "Объявления и связывания имен" ).
Следующие два правила определяют ограничение мономорфизма:
Ограничение мономорфизма
Правило 1. Мы говорим, что данная группа объявлений является неограниченной, если и только если:
(a): каждая переменная в группе связана посредством связывания имен в функциях или посредством связывания имен в простых образцах (раздел "Объявления и связывания имен" ), и
(b): для каждой переменной в группе, которая связана посредством связывания имен в простых образцах, явно указана сигнатура типа.
Обычное ограничение полиморфизма Хиндли-Милнера (Hindley-Milner) заключается в том, что только переменные типа, которые являются свободными в окружении, могут быть подвергнуты обобщению. Кроме того, переменные ограниченного типа из группы ограниченных объявлений нельзя подвергать обобщению на шаге обобщения для этой группы. (Вспомним, что переменная типа ограничена, если она должна принадлежать некоторому классу типа, см. раздел "Объявления и связывания имен" .)
Правило 2. Любые переменные мономорфного типа, которые остаются после завершения вывода типа для всего модуля, считаются неоднозначными, и разрешение неоднозначности с определением конкретных типов выполняется с использованием правил по умолчанию (раздел "Объявления и связывания имен" ).
Обоснование
Правило 1 требуется по двум причинам, обе из них довольно тонкие.
- Правило 1 предотвращает непредвиденные повторы вычислений. Например, genericLength является стандартной функцией (в библиотеке List) с типом
genericLength :: Num a => [b] -> a
Теперь рассмотрим следующее выражение:
let { len = genericLength xs } in (len, len)
Оно выглядит так, будто len должно быть вычислено только один раз, но без Правила 1 оно могло быть вычислено дважды, по одному разу при каждой из двух различных перегрузок. Если программист действительно хочет, чтобы вычисление было повторено, можно явно указать сигнатуру типа:
let { len :: Num a => a; len = genericLength xs } in (len, len)
- Правило 1 предотвращает неодназначность. Например, рассмотрим группу объявлений
[(n,s)] = reads t
Вспомним, что reads - стандартная функция, чей тип задается сигнатурой
reads :: (Read a) => String -> [(a,String)]
Без Правила 1 n был бы присвоен тип forall a. Read a => a, а s - тип forall a. Read a => String. Последний тип является неправильным, потому что по сути он неоднозначен. Невозможно определить, ни в какой перегрузке использовать s, ни можно ли это решить путем добавления сигнатуры типа для s. Поэтому, когда используется связывание имен в образце, не являющимся простым образцом (раздел "Объявления и связывания имен" ), выведенные типы всегда являются мономорфными по своим переменным ограниченного типа, независимо от того, была ли указана сигнатура типа. В этом случае n и s являются мономорфными по a.
То же ограничение применимо к связыванным с образцами функциям. Например, в
(f,g) = ((+),(-))
f и g мономорфны независимо от того, какая сигнатура типа будет указана для f или g.
Правило 2 требуется потому, что нет никакого иного способа предписать мономорфное использование экспортируемого связывания, кроме как выполняя вывод типов на модулях вне текущего модуля. Правило 2 устанавливает, что точные типы всех переменных, связанных в модуле, должны быть определены самим модулем, а не какими-либо модулями, которые импортируют его.
module M1(len1) where default( Int, Double ) len1 = genericLength "Здравствуйте" module M2 where import M1(len1) len2 = (2*len1) :: Rational
Когда вывод типа в модуле M1 закончится, len1 будет иметь мономорфный тип Num a => a (по Правилу 1). Теперь Правило 2 усатанавливает, что переменная мономорфного типа a является неоднозначной, и неоднозначность должна быть разрешена путем использования правил по умолчанию раздела "Объявления и связывания имен" . Поэтому len1 получит тип Int, и его использование в len2 является неправильным из-за типа. (Если вышеупомянутый код в действительности именно то, что требуется, то сигнатура типа для len1 решила бы проблему.)
Эта проблема не возникает для вложенных связываний, потому что их область видимости видна компилятору.
Следствия
Правило мономорфизма имеет множество последствий для программиста. Все, что определено с использованием функционального синтаксиса, обычно обобщается, поскольку ожидается функция. Таким образом, в
f x y = x+y
функция f может использоваться при любой перегрузке в классе Num. Здесь нет никакой опасности перевычисления. Тем не менее, та же функция, определенная с использованием синтаксиса образца
f = \x -> \y -> x+y
требует указания сигнатуры типа, если f должна быть полностью перегружена. Многие функции наиболее естественно определяются посредством использования связывания имен в простых образцах; пользователь должен быть внимателен, добавляя к ним сигнатуры типов, чтобы сохранить полную перегрузку. Стандартное начало (Prelude) содержит много таких примеров:
sum :: (Num a) => [a] -> a sum = foldl (+) 0
Правило 1 применяется к определениям верхнего уровня и к вложенным определениям. Рассмотрим пример:
module M where len1 = genericLength "Здравствуйте" len2 = (2*len1) :: Rational
Здесь с помощью вывода типа устанавливаем, что len1 имеет мономорфический тип (Num a => a) ; при выполнении вывода типа для len2 определяем, что переменная типа a имеет тип Rational.
4.6 Вывод вида
В этом разделе описываются правила, которые используются для того, чтобы выполнить вывод вида, т.е. вычислить подходящий вид для каждого конструктора типа и класса, фигурирующего в данной программе.
Первый шаг в процессе вывода вида заключается в разделении набора определений типов данных, синонимов и классов на группы зависимостей. Этого можно достичь почти таким же способом, как анализ зависимостей для объявлений значений, который был описан в разделе "Объявления и связывания имен" . Например, следующий фрагмент программы включает определение конструктора типа данных D, синонима S и класса C, все они будут включены в одну группу зависимостей:
data C a => D a = Foo (S a) type S a = [D a] class C a where bar :: a -> D a -> Bool
Виды, к которым относятся переменные, конструкторы и классы в пределах каждой группы, определяются с использованием стандартных методов вывода типа и сохраняющей вид унификации (объединения) [ "Основные операции ввода - вывода" ]. Например, в приведенном выше определении параметр a является аргументом конструктора функции (->) в типе bar и поэтому должен относиться к виду *. Из этого следует, что и D, и S должны относиться к виду *->* и что каждый экземпляр класса C должен относиться к виду *.
Возможно, что некоторые части выведенного вида не могут быть полностью определены исходя из соответствующих определений; в таких случаях принимается значение по умолчанию вида *. Например, мы могли принять произвольный вид для параметра a в каждом из следующих примеров:
data App f a = A (f a) data Tree a = Leaf | Fork (Tree a) (Tree a)
Тогда мы получили бы виды ( ->*) -> ->* и ->* соответственно для App и Tree для любого вида . Это также потребовало бы, чтобы расширение допускало полиморфные виды. Вместо этого, используя по умолчанию связывание = *, действительными видами для этих двух конструкторов являются соответственно (*->*) ->*->* и *->*.
Значения по умолчанию применяются к каждой группе зависимостей, независимо от того, как конкретные константы конструктора типа или классов используются в более поздних группах зависимостей или где-либо в другом месте в программе. Например, добавление следующего определения к приведенным выше не влияет на вид, выведенный для Tree (путем изменения его на (*->*) ->*, например), и вместо этого приводит к статической ошибке, потому что вид, которому принадлежит [], *->*, не соответствует виду *, который ожидается для аргумента Tree:
type FunnyTree = Tree [] - неправильно
Это важно, потому что гарантирует, что каждый конструктор и класс используются в соответствии с одним и тем же видом всякий раз, когда они находятся в области видимости.