ничением. “Для любых” означает, что мы не можем делать никаких предположений о внутренней природе
значения. Мы не можем разбирать такие значения на составляющие части. Мы можем только подставлять
их в новые полиморфные функции (как в map), отбрасывать (как const) или перекладывать из одного ме-
ста в другое (как в swap или reverse). Мы можем немного смягчить ограничение, если укажем в контексте
функции какие классы определены для значений данного типа.
Все стандартные полиморфные типы имеют вид:
fun ::
forall a b .. z. Expr(a, b, ... , z)Причём Expr
не содержит forall, а только стрелки и применение новых типов к параметрам. Такой типназывают полиморфным типом первого порядка (rank). Если forall стоит справа от стрелки, то его можно
вынести из выражения, например, следующие выражения эквивалентны:
fun ::
forall a.a ->
(forall b. b -> b)fun ::
forall a b. a -> (b -> b)Так мы можем привести не стандартный тип к стандартному. Если же forall встречается слева от стрел-
ки, как в функции runST, то его уже нельзя вынести. Это приводит к повышению порядка полиморфизма.
Порядок полиморфизма определяется как самый максимум среди всех подвыражений, что стоят слева от
стрелки плюс один. Так в типе
runST ::
(forall s. ST s a) -> aСлева от стрелки стоит тип первого порядка, прибавив единицу, получим порядок для всего выражения.
Если вдруг нам захочется воспользоваться такими типами, мы можем включить одно из расширений:
{-# Language Rank2Types #-}
{-# Language RankNTypes #-}
В случае рангов произвольного порядка алгоритм вывода типов может не завершиться. В этом случае нам
придётся помогать компилятору расставляя типы сложных функций вручную.
Лексически связанные типы
Мы уже привыкли к тому, что когда мы пишем
swap ::
(a, b) -> (b, a)компилятор понимает, что a и b указывают на один и тот же тип слева и справа от стрелки. При этом типы
a и b не обязательно разные. Иногда нам хочется расширить действие контекста функции и распространить
его на всё тело функции. Например ранее в этой главе, когда мы имитировали числа через типы, для того
чтобы извлечь число из типа, мы пользовались трюком с функцией proxy:
instance Nat
a => Nat (Succ a) wheretoInt x =
1 + toInt (proxy x)proxy ::
f a -> aproxy =
undefinedЕдинственное назначение функции proxy~– это передача информации о типе. Было бы гораздо удобнее
написать:
instance Nat
a => Nat (Succ a) wheretoInt x =
1 + toInt (undefined :: a)Проблема в том, что по умолчанию любой полиморфный тип в Haskell имеет первый ранг, компилятор
читает нашу запись как (x ::
forall a. a), и получается, что мы говорим: x имеет любой тип, какойзахочешь! Не очень полезная информация. Компилятор заблудился и спрашивает у нас: “куда пойти?” А
мы ему: “да куда захочешь”. Как раз для таких случаев существует расширение ScopedTypeVariables
. Оносвязывает тип, объявленный в заголовке класса/функции с типами, которые встречаются в теле функции.
В случае функций есть одно отличие от случая с классами. Если мы хотим расширить действие переменной
из объявления типа функции, необходимо упомянуть её в слове forall в стандартном положении (как для
типа первого порядка). У нас был ещё один пример с proxy:
Расширения | 263
dt ::
(Nat n, Fractional a) => Stream n a -> adt xs =
1 / (fromIntegral $ toInt $ proxy xs)where
proxy :: Stream n a -> nproxy =
undefinedВ этом случае мы пишем:
{-# Language ScopedTypeVariables #-}
...
dt ::
forall n. (Nat n, Fractional a) => Stream n a -> adt xs =
1 / (fromIntegral $ toInt (undefined :: n))Обратите внимение на появление forall в определении типа. Попробуйте скомпилировать пример без
него или переместите его в другое место. Во многих случаях применения этого рсширения можно избежать
с помощью стандартной функции asTypeOf, посмотрим на определение из Prelude
:asTypeOf ::
a -> a -> aasTypeOf x y =
xФактически это функция const, оба типа которой одинаковы. Она часто используется в инфиксной форме
для фиксации типа первого аргумента:
q =
f $ x ‘asTypeOf‘ varПолучается очень наглядно, словно это предложение обычного языка.
И другие удобства и украшения
Стоит упомянуть несколько расширений. Они лёгкие для понимания, в основном служат украшению
записи или для сокращения рутинного кода.
Директива deriving
может использоваться только с несколькими стандартными классами, но если мыопределили тип-обёртку через newtype
или просто синоним, то мы можем очень просто определить новыйтип экземпляром любого класса, который доступен завёрнутому типу. Как раз для этого существует расши-
рение GeneralizedNewtypeDeriving
:newtype MyDouble = MyDouble Double
deriving
(Show, Eq, Enum, Ord, Num, Fractional, Floating)Мы говорили о том, что обычные числа в Haskell перегружены, иногда возникает необходимость в пе-