-
PatternSynonyms -
- Добавлено в:
-
7.8.1
Разрешает определять синонимы шаблонов.
Синонимы шаблонов включаются языковым расширением PatternSynonyms, которое требуется для их определения, но не для их использования. Дополнительную информацию и примеры синонимов шаблонов можно найти на странице Wiki.
Синонимы шаблонов позволяют давать имена параметризованным схемам шаблонов. Их также можно рассматривать как абстрактные конструкторы, не влияющие на представление данных. Например, при реализации языка программирования типы языка можно представить следующим образом:
data Type = App String [Type]
Вот несколько примеров использования этого представления. Рассмотрим несколько типов из вселенной Type, закодированных таким образом:
App "->" [t1, t2] -- t1 -> t2 App "Int" [] -- Int App "Maybe" [App "Int" []] -- Maybe Int
Это представление очень универсально, поскольку ни одному типу не отводится особое место. Однако некоторым функциям может потребоваться специальная обработка отдельных известных типов. Например, следующие две функции собирают все типы аргументов (вложенных) типов стрелок и распознают тип Int соответственно:
collectArgs :: Type -> [Type] collectArgs (App "->" [t1, t2]) = t1 : collectArgs t2 collectArgs _ = [] isInt :: Type -> Bool isInt (App "Int" []) = True isInt _ = False
Сопоставление непосредственно с App трудно читать, а писать его чревато ошибками. При вложенном сопоставлении ситуация ещё хуже:
isIntEndo :: Type -> Bool isIntEndo (App "->" [App "Int" [], App "Int" []]) = True isIntEndo _ = False
Синонимы шаблонов позволяют абстрагироваться от представления и предоставлять сопоставители, которые при сопоставлении с образцом ведут себя как конструкторы. Мы можем создать синонимы шаблонов для интересующих нас известных типов, не привязывая к ним представление (обратите внимание: их не обязательно определять в том же модуле, что и тип Type):
pattern Arrow t1 t2 = App "->" [t1, t2] pattern Int = App "Int" [] pattern Maybe t = App "Maybe" [t]
Это позволяет переписать наши функции в гораздо более чистом стиле:
collectArgs :: Type -> [Type] collectArgs (Arrow t1 t2) = t1 : collectArgs t2 collectArgs _ = [] isInt :: Type -> Bool isInt Int = True isInt _ = False isIntEndo :: Type -> Bool isIntEndo (Arrow Int Int) = True isIntEndo _ = False
В целом существует три вида синонимов шаблонов: однонаправленные, двунаправленные и явно двунаправленные. Приведённые выше примеры демонстрируют двунаправленные синонимы шаблонов. Двунаправленный синоним ведёт себя так же, как обычный конструктор данных. Его можно использовать в контексте шаблона для разборки значений и в контексте выражения для создания значений. Например, мы можем создать значение intEndo с помощью синонимов шаблонов Arrow и Int, определённых ранее.
intEndo :: Type intEndo = Arrow Int Int
Этот пример эквивалентен гораздо более сложному конструированию, которое потребовалось бы при непосредственном использовании конструкторов Type.
intEndo :: Type intEndo = App "->" [App "Int" [], App "Int" []]
Однонаправленные синонимы можно использовать только в контексте шаблона; они определяются следующим образом:
pattern Head x <- x:xs
В этом случае Head ⟨x⟩ нельзя использовать в выражениях — только в шаблонах, поскольку он не задаёт значение для ⟨xs⟩ в правой части. Однако можно определить явно двунаправленный синоним шаблона, отдельно указав, как создавать и разбирать тип. Синтаксис для этого выглядит так:
pattern HeadC x <- x:xs where HeadC x = [x]
Теперь HeadC можно использовать как в контексте выражения, так и в контексте шаблона. В контексте шаблона он сопоставится с головой любого списка длиной не менее одного элемента. В контексте выражения он создаст список из одного элемента.
Явно двунаправленные синонимы шаблонов обеспечивают большую гибкость, чем неявно двунаправленные, с точки зрения допустимого синтаксиса. Например, следующий неявно двунаправленный синоним шаблона недопустим:
pattern StrictJust a = Just !a
Он недопустим, поскольку использование BangPatterns в правой части не позволяет считать её корректным выражением. Однако создать строгий синоним шаблона вполне возможно, используя явно двунаправленный синоним:
pattern StrictJust a <- Just !a where StrictJust !a = Just a
Явно двунаправленный синоним шаблона также:
- может создавать конструкторы данных, отличные от конструктора базового типа данных, и не только тот, который появляется в сопоставлении с образцом;
- может вызывать любые функции и использовать условную логику, особенно проверку корректности, если в результате создаётся значение нужного типа;
- может использовать охранные условия в левой части
=; - может иметь несколько уравнений.
Например:
data PosNeg = Pos Int | Neg Int
pattern Smarter{ nonneg } <- Pos nonneg where
Smarter x = if x >= 0 then (Pos x) else (Neg x)
Или с использованием охранных условий:
pattern Smarter{ nonneg } <- Pos nonneg where
Smarter x | x >= 0 = (Pos x)
| otherwise = (Neg x)
В сообществе Haskell широко распространено искусство создания умных конструкторов — по сути, функций, которые оборачивают конструктор проверкой корректности и не раскрывают его представление. Недостаток такого подхода в том, что базовый конструктор нельзя использовать для сопоставления. Синонимы шаблонов можно применять как настоящие умные конструкторы — как для проверки корректности, так и для сопоставления.
В таблице ниже показано, где можно использовать каждый вид синонимов шаблонов.
Контекст | Однонаправленный | Двунаправленный | Явно двунаправленный |
|---|---|---|---|
Шаблон | Да | Да | Да |
Выражение | Нет | Да (выводится автоматически) | Да (указывается явно) |
6.7.4.1. Синонимы шаблонов-записей
Также можно определять синонимы шаблонов, которые ведут себя так же, как конструкторы записей. Синтаксис для этого выглядит так:
pattern Point :: Int -> Int -> (Int, Int)
pattern Point{x, y} = (x, y)
Идея состоит в том, что затем можно использовать Point так, как если бы мы определили новый тип данных MyPoint с двумя полями x и y.
data MyPoint = Point { x :: Int, y :: Int }
Обычный синоним шаблона можно использовать двумя способами, а Point — семью. Именно теми же способами, что и обычный конструктор записи.
Использование | Пример |
|---|---|
Как конструктор |
|
Как конструктор с синтаксисом записи |
|
В контексте шаблона |
|
В контексте шаблона с синтаксисом записи |
|
В контексте шаблона с сокращённой записью полей |
|
При обновлении записи |
|
С помощью селекторов записей |
|
Для однонаправленного синонима шаблона-записи определяются селекторы записей, но обновление и создание записей не допускаются.
Синтаксис и семантика синонимов шаблонов подробно рассматриваются в следующих подразделах. Дополнительные сведения приведены в статье.
Дополнительные сведения см. на странице Wiki.
6.7.4.2. Синтаксис и область видимости синонимов шаблонов
Объявление синонима шаблона может быть однонаправленным, двунаправленным или явно двунаправленным. Синтаксис однонаправленных синонимов шаблонов:
pattern pat_lhs <- pat
синтаксис двунаправленных синонимов шаблонов:
pattern pat_lhs = pat
и синтаксис явно двунаправленных синонимов шаблонов:
pattern pat_lhs <- pat where pat_lhs = expr -- lhs restricted, see below
Изменяя форму pat_lhs, можно определять синонимы шаблонов с префиксной, инфиксной или записной формой. Их синтаксис представлен ниже:
Префиксная форма |
|
Инфиксная форма |
|
Форма записи |
|
В pat_lhs для явно двунаправленного создания нельзя использовать синтаксис записи. (Поскольку expr в правой части может создавать другие конструкторы данных.) Можно использовать охранные условия и несколько уравнений.
Объявления синонимов шаблонов могут находиться только на верхнем уровне модуля. В частности, они не допускаются в качестве локальных определений.
Переменные в левой части определения связываются шаблоном в правой части. Для двунаправленных синонимов шаблонов все переменные правой части должны также встречаться в левой части; кроме того, шаблоны с подстановочными знаками и шаблоны-представления не допускаются. Для однонаправленных и явно двунаправленных синонимов шаблонов ограничений на шаблон в правой части нет.
Синонимы шаблонов нельзя определять рекурсивно.
Можно указывать прагмы COMPLETE, чтобы сообщить проверке полноты сопоставления с образцом, что набор синонимов шаблонов является полным.
6.7.4.3. Импорт и экспорт синонимов шаблонов
Имя синонима шаблона находится в том же пространстве имён, что и обычные конструкторы данных. Как и обычные конструкторы данных, синонимы шаблонов можно импортировать и экспортировать вместе с конструктором типа или независимо от него.
Чтобы экспортировать их отдельно, в спецификации экспорта или импорта необходимо предварить имена шаблонов ключевым словом data или pattern, например:
module Example (data Zero) where -- alternatively: pattern Zero data MyNum = MkNum Int pattern Zero :: MyNum pattern Zero = MkNum 0
Без указания пространства имён data имя Zero в списке экспорта будет интерпретировано как конструктор типа.
Использование ключевого слова data в списках импорта и экспорта требует расширения ExplicitNamespaces и GHC ≥9.14. Этот вариант предпочтительнее pattern, если только не требуется поддерживать более старые версии GHC.
В спецификации импорта или экспорта также можно использовать ключевое слово data (или pattern), чтобы импортировать или экспортировать обычный конструктор данных. Например:
import Data.Maybe( data Just )
поместит в область видимости конструктор данных Just типа Maybe, не помещая туда конструктор типа Maybe.
Начиная с GHC 8.0.1, синонимы шаблонов также можно «объединять» с экспортируемым конструктором типа, чтобы такой шаблон считался конструктором данных этого типа. Чтобы объединить синоним шаблона, нужно указать его в списке экспорта модуля, экспортирующего конструктор типа. Например, чтобы объединить Zero с MyNum, можно написать следующее:
module Example ( MyNum(Zero) ) where
Если затем модуль импортирует MyNum из Example, он также импортирует синоним шаблона Zero.
В списке экспорта также можно использовать специальный токен .., означающий все текущие объединённые конструкторы. Например, можно написать:
module Example ( MyNum(.., Zero) ) where
в этом случае Example экспортирует конструктор типа MyNum вместе с конструктором данных MkNum и синонимом шаблона Zero.
Типы объединённых синонимов шаблонов проверяются, чтобы убедиться, что они совпадают с типом конструктора, с которым их объединяют. Синоним шаблона P нельзя объединить с конструктором типа T, если тип P явно несовместим с T.
Модуль, который импортирует MyNum(..) из Example, а затем повторно экспортирует MyNum(..), также экспортирует все синонимы шаблонов, объединённые с MyNum в Example. Более полное описание можно найти в Wiki.
6.7.4.4. Типизация синонимов шаблонов
Пусть задано определение синонима шаблона вида
pattern P var1 var2 ... varN <- pat
ему назначается тип шаблона вида
pattern P :: CReq => CProv => t1 -> t2 -> ... -> tN -> t
где ⟨CReq⟩ и ⟨CProv⟩ — контексты типов, а ⟨t1⟩, ⟨t2⟩, …, ⟨tN⟩ и ⟨t⟩ — типы. Обратите внимание на необычную форму типа с двумя контекстами ⟨CReq⟩ и ⟨CProv⟩:
- ⟨CReq⟩ — ограничения, необходимые для сопоставления с шаблоном.
- ⟨CProv⟩ — ограничения, предоставляемые при успешном сопоставлении с шаблоном.
Например, рассмотрим
data T a where MkT :: (Show b) => a -> b -> T a f1 :: (Num a, Eq a) => T a -> String f1 (MkT 42 x) = show x pattern ExNumPat :: (Num a, Eq a) => (Show b) => b -> T a pattern ExNumPat x = MkT 42 x f2 :: (Eq a, Num a) => T a -> String f2 (ExNumPat x) = show x
Здесь f1 не использует синонимы шаблонов. Для сопоставления с числовым шаблоном 42 вызывающий код должен удовлетворять ограничениям (Num a, Eq a), поэтому они появляются в типе f1. Вызов show порождает ограничение (Show b), где b — экзистенциальная переменная типа, связанная сопоставлением с образцом MkT. Однако это же сопоставление с образцом также предоставляет ограничение (Show b) (см. тип MkT), поэтому всё работает.
Точно такие же рассуждения применимы к ExNumPat: сопоставление с ExNumPat требует ограничений (Num a, Eq a) и предоставляет ограничение (Show b).
Обратите также внимание на следующие моменты:
- В обычном случае, когда
CProvпуст, (то есть()), его можно вообще опустить в приведённой выше сигнатуре типа шаблона дляP. -
Однако если
CProvне пуст, аCReq— пуст, приведённую выше сигнатуру типа шаблона дляPнеобходимо указать в видеP :: () => CProv => t1 -> t2 -> .. -> tN -> t
- Команда GHCi
:infoпоказывает типы шаблонов в этом формате. -
Можно указать явную сигнатуру шаблона, как мы сделали выше для
ExNumPat, чтобы задать тип шаблона так же, как это делается для функции. Как обычно, сигнатура типа может быть менее полиморфной, чем выведенный тип. Например:-- Inferred type would be 'a -> [a]' pattern SinglePair :: (a, a) -> [(a, a)] pattern SinglePair x = [x]
Как и сигнатуры привязок на уровне значений, сигнатуры синонимов шаблонов могут относиться к нескольким шаблонам. Например:
pattern Left', Right' :: a -> Either a a pattern Left' x = Left x pattern Right' x = Right x
-
Правила лексически ограниченных переменных типа (см. раздел Лексически ограниченные переменные типа) применяются к сигнатурам синонимов шаблонов. Согласно этим правилам, над определением синонима шаблона распространяется область видимости только переменных типа из явного, синтаксически видимого внешнего
forall(универсальных переменных); экзистенциальные переменные, связанные внутренним forall, в неё не входят. Например:data T a where MkT :: Bool -> b -> (b->Int) -> a -> T a pattern P :: forall a. forall b. b -> (b->Int) -> a -> T a pattern P x y v <- MkT True x y (v::a)
Здесь универсальная переменная типа
aраспространяет область видимости на определениеP, а экзистенциальная переменнаяb— нет. (См. обсуждение в задаче #14998.) -
Для двунаправленного синонима шаблона его использование в качестве выражения имеет тип
(CReq, CProv) => t1 -> t2 -> ... -> tN -> t
Поэтому в предыдущем примере
ExNumPatпри использовании в выражении имеет типExNumPat :: (Num a, Eq a, Show b) => b -> T t
Обратите внимание: это немного более ограничительно, чем выражение
MkT 42 x, которому не потребовалось бы(Eq a). -
Рассмотрим эти два синонима шаблонов:
data S a where S1 :: Bool -> S Bool pattern P1 :: Bool -> Maybe Bool pattern P1 b = Just b pattern P2 :: () => (b ~ Bool) => Bool -> S b pattern P2 b = S1 b f :: Maybe a -> String f (P1 x) = "no no no" -- Type-incorrect g :: S a -> String g (P2 b) = "yes yes yes" -- Fine
Шаблон
P1может сопоставляться только со значением типаMaybe Bool, поэтому функцияfотклоняется, поскольку сигнатура типа имеет видMaybe a. (Чтобы убедиться в этом, представьте, что синоним шаблона раскрывается.)С другой стороны, функция
gработает, поскольку сопоставление сP2(обёрткой над GADTS) предоставляет локальное равенство(a~Bool). Если указать явную сигнатуру шаблонаP2 :: Bool -> S Bool, типP2станет менее полиморфным и будет вести себя точно так же, какP1, поэтомуgбудет отклонена.Иными словами, если для синонимов шаблонов требуется поведение, аналогичное GADT, то, в отличие от конкретных конструкторов данных, например
S1, тип необходимо записывать с явно указанными предоставляемыми равенствами. Для конкретного конструктора данных, напримерS1, сигнатуру типа можно записать какS1 :: Bool -> S BoolилиS1 :: (b~Bool) => Bool -> S b— эти формы эквивалентны. Для синонимов шаблонов это не так: формы различаются, чтобы различать два описанных выше случая. (Обсуждение этого решения см. в задаче #9953.)
6.7.4.5. Сопоставление с синонимами шаблонов
При использовании синонима шаблона в шаблоне сначала выполняется сопоставление с самим синонимом шаблона, а затем — с шаблонами его аргументов.
Точнее, семантика сопоставления с образцом описана в разделе 3.17 отчёта Haskell 2010. К неформальной семантике из раздела 3.17.2 добавляется следующее правило:
-
Если шаблон является шаблоном-конструктором
(P p1 ... pn), гдеP— синоним шаблона, определённый с помощьюP x1 ... xn = pилиP x1 ... xn <- p, то:- Сопоставить значение
vсp. Если это сопоставление завершается неудачей или расходится, то и всё сопоставление (с синонимом шаблона) завершается неудачей или расходится. В противном случае сопоставление сpдолжно связать переменныеx1 ... xn; пусть они будут связаны со значениямиv1 ... vn. - Сопоставить
v1сp1,v2сp2и так далее. Если какое-либо из этих сопоставлений завершается неудачей или расходится, то и всё сопоставление завершается неудачей или расходится. - Если все сопоставления с
piуспешны, сопоставление успешно и связывает переменные, связанныеpi. (xiне связываются; они остаются локальными для объявления синонима шаблона.)
- Сопоставить значение
Например, в следующей программе f и f' эквивалентны:
pattern Pair x y <- [x, y] f (Pair True True) = True f _ = False f' [x, y] | True <- x, True <- y = True f' _ = False
Обратите внимание, что строгость f отличается от строгости g, определённого ниже:
g [True, True] = True g _ = False *Main> f (False:undefined) *** Exception: Prelude.undefined *Main> g (False:undefined) False
6.7.4.6. Прагмы для синонимов шаблонов
Начиная с GHC 9.2, для синонимов шаблонов поддерживаются прагмы INLINABLE, INLINE и NOINLINE. Например:
pattern InlinablePattern x = [x]
{-# INLINABLE InlinablePattern #-}
pattern InlinedPattern x = [x]
{-# INLINE InlinedPattern #-}
pattern NonInlinedPattern x = [x]
{-# NOINLINE NonInlinedPattern #-}
Как и для других прагм INLINABLE, INLINE и NOINLINE, можно указать фазу, к которой применяется прагма:
pattern Q x = [x]
{-# NOINLINE[1] Q #-}
Прагмы применяются как при использовании шаблона в качестве сопоставителя, так и при его использовании в качестве конструктора данных. Для явно двунаправленных синонимов шаблонов прагму нужно указывать на верхнем уровне, а не внутри предложения where. Например, следующий код не скомпилируется:
pattern HeadC x <- x:xs where
HeadC x = [x]
{-# INLINE HeadC #-}
а этот скомпилируется:
pattern HeadC x <- x:xs where
HeadC x = [x]
{-# INLINE HeadC #-}
Если для шаблона не указана прагма, решение о встраивании принимается на основе собственных эвристик GHC.