Spec-Zone.ru › Haskell 8

10.1. Параметры языка

Как и во всех известных системах Haskell, GHC реализует некоторые расширения стандартного языка Haskell. Их можно включить или отключить с помощью флагов командной строки или прагм языка. По умолчанию GHC понимает самую последнюю поддерживаемую им версию Haskell, а также несколько расширений.

Некоторые расширения Glasgow предназначены для доступа к базовым возможностям, с помощью которых мы реализуем Haskell. Таким образом, вы можете получить доступ к Raw Iron, если готовы написать непереносимый код на более примитивном уровне. Вам не нужно «застревать» на производительности из-за затрат на реализацию «высокоуровневых» функций Haskell — вы всегда можете кодировать «под» ними. В крайнем случае, вы можете написать весь свой критичный для производительности код на C, а затем просто склеить его с Haskell!

Прежде чем вы слишком увлечётесь работой на самом низком уровне (например, перемещением MutableByteArray# в вашем программном коде), вы можете проверить, существуют ли библиотеки, которые предоставляют «обёртку Haskell» над необходимыми вам функциями. Отдельная документация по библиотекам описывает все библиотеки, поставляемые с GHC.

Расширения языка контролируют, какие варианты языка разрешены.

Параметры языка можно контролировать двумя способами:

  • Каждый параметр языка может быть включён с помощью флага командной строки «-X...» (например, -XTemplateHaskell), и выключен с помощью флага «-XNo...»; (например, -XNoTemplateHaskell).
  • Параметры языка, распознаваемые Cabal, также можно включить с помощью прагмы LANGUAGE , тем самым {-# LANGUAGE TemplateHaskell #-} (см. прагму LANGUAGE).

GHC поддерживает эти параметры языка:

Расширение Описание

AllowAmbiguousTypes

Разрешить пользователю писать неоднозначные типы, а механизму вывода типов — выводить их.

ApplicativeDo

Включить расконвертирование обозначения Applicative do.

Arrows

Включить расширение с использованием стрелочного обозначения.

BangPatterns

Включить шаблоны bang.

BinaryLiterals

Включить поддержку двоичных литералов.

BlockArguments

Разрешить использование блоков do и других конструкций в качестве аргументов функций.

CApiFFI

Включить соглашение о вызовах CAPI.

ConstrainedClassMethods

Включить ограниченные методы классов.

ConstraintKinds

Включить вид ограничений.

CPP

Включить препроцессор C.

CUSKs

Включить обнаружение полных пользовательских сигнатур видов.

DataKinds

Включить повышение типов данных.

DatatypeContexts

Разрешить контексты для типов data.

DefaultSignatures

Включить сигнатуры по умолчанию.

DeriveAnyClass

Включить вывод для любого класса.

DeriveDataTypeable

Включить вывод для класса Data. Подразумевается (устаревшее) AutoDeriveTypeable.

DeriveFoldable

Включить вывод для класса Foldable. Подразумевается DeriveTraversable.

DeriveFunctor

Включить вывод для класса Functor. Подразумевается DeriveTraversable.

DeriveGeneric

Включить вывод для класса Generic.

DeriveLift

Включить вывод для класса Lift

DeriveTraversable

Включить вывод для класса Traversable. Подразумевает DeriveFunctor и DeriveFoldable.

DerivingStrategies

Включает стратегии вывода.

DerivingVia

Включить вывод экземпляров для типов via с одинаковым представлением во время выполнения. Подразумевает DerivingStrategies.

DisambiguateRecordFields

Включить устранение неоднозначностей полей записей. Подразумевается RecordWildCards.

DuplicateRecordFields

Разрешить определение типов записей с полями, имеющими одинаковые имена.

EmptyCase

Разрешить пустые альтернативы кейсов.

EmptyDataDecls

Разрешить определение пустых типов data.

EmptyDataDeriving

Разрешить вывод экземпляров стандартных классов типов для пустых типов данных.

ExistentialQuantification

Включить ослабленные синонимы типов.

ExplicitForAll

Включить явное универсальное квантификацию. Подразумевается ScopedTypeVariables, LiberalTypeSynonyms, RankNTypes и ExistentialQuantification.

ExplicitNamespaces

Включить использование ключевого слова type для указания пространства имен записей в импортах и экспортах (Явные пространства имен в импорте/экспорте). Подразумевается TypeOperators и TypeFamilies.

ExtendedDefaultRules

Использовать расширенные правила по умолчанию GHCi в обычном модуле.

FlexibleContexts

Включить гибкие контексты.

FlexibleInstances

Включить гибкие экземпляры. Подразумевает TypeSynonymInstances.

ForeignFunctionInterface

Включить интерфейс внешних функций.

FunctionalDependencies

Включить функциональные зависимости. Подразумевает MultiParamTypeClasses.

GADTs

Включить обобщённые алгебраические типы данных. Подразумевает GADTSyntax и MonoLocalBinds.

GADTSyntax

Включить синтаксис обобщённых алгебраических типов данных.

GeneralisedNewtypeDeriving

Включить вывод для newtype.

Haskell2010

Использовать вариант языка Haskell 2010.

Haskell98

Использовать вариант языка Haskell 2010.

HexFloatLiterals

Включить поддержку шестнадцатеричных литералов с плавающей запятой.

ImplicitParams

Включить неявные параметры.

ImplicitPrelude

Не неявно import Prelude. Подразумевается RebindableSyntax.

ImportQualifiedPost

ImportQualifiedPost разрешает синтаксис import M qualified

ImpredicativeTypes

Включить импредикативные типы. Подразумевает RankNTypes.

IncoherentInstances

Включить некогерентные экземпляры. Подразумевает OverlappingInstances.

InstanceSigs

Включить сигнатуры экземпляров.

InterruptibleFFI

Включить прерывимый FFI.

KindSignatures

Включить сигнатуры видов. Подразумевается TypeFamilies и PolyKinds.

LambdaCase

Включить выражения lambda-case.

LiberalTypeSynonyms

Включить ослабленные синонимы типов.

MagicHash

Разрешить # в качестве постфиксного модификатора идентификаторов.

MonadComprehensions

Включить монадные понимания.

MonadFailDesugaring

Включить отмену сахара monadfail.

MonoLocalBinds

Включить, чтобы не обобщать локальные связи. Подразумевается TypeFamilies и GADTs.

MonomorphismRestriction

Отключить ограничение мономорфизма.

MultiParamTypeClasses

Включить многопараметрические типы классов. Подразумевается FunctionalDependencies.

MultiWayIf

Включить многосторонние выражения if.

NamedFieldPuns

Включить псевдонимы записей.

NamedWildCards

Включить именованные дикие карты.

ndecreasingIndentation

Разрешить вложенным контекстам иметь тот же уровень отступа, что и их окружающему контексту.

NegativeLiterals

Включить поддержку отрицательных литералов.

NPlusKPatterns

Включить поддержку n+k шаблонов. Подразумевается Haskell98.

NullaryTypeClasses

Устаревшее, ничего не делает. Нульарные (без параметров) типы классов теперь включены с помощью MultiParamTypeClasses.

NumDecimals

Включить поддержку «дробных» целочисленных литералов.

NumericUnderscores

Включить поддержку числовых подчеркиваний.

OverlappingInstances

Включить перекрывающиеся инстансы.

OverloadedLabels

Включить перегруженные метки.

OverloadedLists

Включить перегруженные списки.

OverloadedStrings

Включить перегруженные строковые литералы.

PackageImports

Включить импорты с квалификацией пакетов.

ParallelListComp

Включить параллельные списки понимания.

PartialTypeSignatures

Включить частичные сигнатуры типов.

PatternGuards

Отключить шаблоны-защиты. Подразумевается Haskell98.

PatternSynonyms

Включить синонимы шаблонов.

PolyKinds

Включить полиморфизм видов. Подразумевает KindSignatures.

PostfixOperators

Включить постфиксные операторы.

QuantifiedConstraints

Разрешить forall квантификаторы в ограничениях.

QuasiQuotes

Включить квазицитирование.

Rank2Types

Включить типы ранга 2. Синоним для RankNTypes.

RankNTypes

Включить типы ранга N. Подразумевается ImpredicativeTypes.

RebindableSyntax

Использовать переопределяемую синтаксис. Подразумевает NoImplicitPrelude.

RecordWildCards

Включить дикие карты записей. Подразумевает DisambiguateRecordFields.

RecursiveDo

Включить рекурсивное do (mdo) обозначение.

RoleAnnotations

Включить аннотации ролей.

Safe

Включить режим Safe Haskell «Безопасный».

ScopedTypeVariables

Включить лексически-определённые переменные типов.

StandaloneDeriving

Включить самостоятельное получение.

StandaloneKindSignatures

Разрешить использование самостоятельных сигнатур видов.

StarIsType

Рассматривать * как Data.Kind.Type.

StaticPointers

Включить статические указатели.

Strict

Сделать связи в текущем модуле строгими по умолчанию.

StrictData

Включить строгие поля по умолчанию для типов данных.

TemplateHaskell

Включить Template Haskell.

TemplateHaskellQuotes

Включить подмножество цитирования Template Haskell.

TraditionalRecordSyntax

Отключить поддержку традиционного синтаксиса записей (как поддерживается Haskell 98) C {f = x}

TransformListComp

Включить обобщённые списки понимания.

Trustworthy

Включить режим Safe Haskell «Надёжный».

TupleSections

Включить секции кортежей.

TypeApplications

Включить синтаксис применения типов в терминах и типах.

TypeFamilies

Включить типы семейств. Подразумевает ExplicitNamespaces, KindSignatures и MonoLocalBinds.

TypeFamilyDependencies

Включить инъективные типы семейств. Подразумевает TypeFamilies.

TypeInType

Устаревшее. Включить полиморфизм видов и продвижение типов данных.

TypeOperators

Включить операторы типов. Подразумевает ExplicitNamespaces.

TypeSynonymInstances

Включить синонимы типов в заголовках экземпляров. Подразумевается FlexibleInstances.

UnboxedSums

Включить не упакованные суммы.

UnboxedTuples

Включить использование синтаксиса необолоченных кортежей.

UndecidableInstances

Включить неразрешимые экземпляры.

UndecidableSuperClasses

Разрешить все ограничения суперклассов, включая те, которые могут привести к не завершению работы типового проверяющего.

UnicodeSyntax

Включить синтаксис юникода.

UnliftedFFITypes

Включить неподнятые типы FFI

UnliftedNewtypes

Включить неподнятые newtypes.

Unsafe

Включить режим Unsafe для Безопасного Haskell.

ViewPatterns

Включить шаблоны представлений.

Несмотря на то, что это не рекомендуется, устаревший флаг -fglasgow-exts включает сразу большой набор расширений, поддерживаемых GHC.

-fglasgow-exts

Флаг -fglasgow-exts эквивалентен включению следующих расширений:

  • ConstrainedClassMethods
  • DeriveDataTypeable
  • DeriveFoldable
  • DeriveFunctor
  • DeriveGeneric
  • DeriveTraversable
  • EmptyDataDecls
  • ExistentialQuantification
  • ExplicitNamespaces
  • FlexibleContexts
  • FlexibleInstances
  • ForeignFunctionInterface
  • FunctionalDependencies
  • GeneralizedNewtypeDeriving
  • ImplicitParams
  • InterruptibleFFI
  • KindSignatures
  • LiberalTypeSynonyms
  • MagicHash
  • MultiParamTypeClasses
  • ParallelListComp
  • PatternGuards
  • PostfixOperators
  • RankNTypes
  • RecursiveDo
  • ScopedTypeVariables
  • StandaloneDeriving
  • TypeOperators
  • TypeSynonymInstances
  • UnboxedTuples
  • UnicodeSyntax
  • UnliftedFFITypes

Включение этих параметров — единственный эффект -fglasgow-exts. Мы пытаемся отказаться от этого сочетательного флага и перейти к включению функций по отдельности.

Haskell2010

Компиляция варианта языка Haskell 2010. Включает следующие расширения языка:

  • ImplicitPrelude
  • StarIsType
  • CUSKs
  • MonomorphismRestriction
  • DatatypeContexts
  • TraditionalRecordSyntax
  • EmptyDataDecls
  • ForeignFunctionInterface
  • PatternGuards
  • DoAndIfThenElse
  • RelaxedPolyRec
Haskell98

Компиляция с использованием варианта языка Haskell 98. Включает следующие расширения языка:

  • ImplicitPrelude
  • StarIsType
  • CUSKs
  • MonomorphismRestriction
  • NPlusKPatterns
  • DatatypeContexts
  • TraditionalRecordSyntax
  • NondecreasingIndentation

10.2. Необолоченные типы и примитивные операции

GHC построен на базе множества примитивных типов данных и операций; «примитивные» в том смысле, что они не могут быть определены в самом Haskell. Хотя вы действительно можете использовать это для написания быстрого кода, мы обычно находим это намного менее болезненным и более удовлетворительным в долгосрочной перспективе, использовать возможности и библиотеки более высокого уровня языка. При хорошем стечении обстоятельств, код, который вы пишете, будет оптимизирован до эффективного необолоченного варианта в любом случае. А если нет, нам хотелось бы об этом узнать.

Все эти примитивные типы данных и операции экспортируются библиотекой GHC.Prim, для которой существует подробная онлайн-документация <GHC.Prim.>. (Эта документация сгенерирована из файла compiler/prelude/primops.txt.pp.)

Если вы хотите упомянуть любые примитивные типы данных или операции в своей программе, вы должны сначала импортировать GHC.Prim для их включения в область видимости. Многие из них имеют имена, оканчивающиеся на #, и чтобы упомянуть такие имена, вам нужно расширение MagicHash.

Примитивы широко используют необолоченные типы и необолоченные кортежи, которые мы кратко изложим здесь.

10.2.1. Необолоченные типы

Большинство типов в GHC — оболоченные, что означает, что значения этого типа представляются указателем на объект в куче. Представление Haskell Int, например, — это двухсловный объект в куче. Необолоченный тип, однако, представлен самим значением, никаких указателей или выделения памяти в куче нет.

Необолоченные типы соответствуют «сырым машинных» типам, которые вы бы использовали в C: Int# (long int), Double# (double), Addr# (void *), и т. д. Примитивные операции (PrimOps) над этими типами — те, что вы могли бы ожидать; например, (+#) — это сложение для Int#, и это машинный метод сложения, который нам всем знаком и любим — обычно одна инструкция.

Примитивные (необолоченные) типы не могут быть определены в Haskell и поэтому встроены в язык и компилятор. Примитивные типы всегда неподнятые; то есть значение примитивного типа не может быть ошибкой. (Примечание: «оболоченный» тип означает, что значение представлено указателем на объект в куче; «поднятый» тип означает, что термины этого типа могут быть ошибкой. Пример см. в следующем абзаце.) Мы используем соглашение (но оно только соглашение), что примитивные типы, значения и операции имеют суффикс # (см. Магический хэш). Для некоторых примитивных типов у нас есть специальный синтаксис для литералов, также описанный в той же секции.

Примитивные значения часто представляются простым битовым шаблоном, например, Int#, Float#, Double#. Но это необязательно: примитивное значение может быть представлено указателем на объект, выделенный в куче. Примеры включают Array#, тип примитивных массивов. Таким образом, Array# является неподнятым, упакованным типом. Примитивный массив выделяется в куче, потому что он слишком большой, чтобы поместиться в регистр, и слишком дорого копировать; в некотором смысле, случайно, что он представлен указателем. Если указатель представляет примитивное значение, то он действительно указывает на это значение: нет неуточненных трюков, нет косвенных ссылок. Ничто не может быть по ту сторону указателя, кроме примитивного значения. Численно-ёмкая программа, использующая неупакованные типы, может работать намного быстрее, чем её «стандартный» аналог — мы наблюдали тройное увеличение скорости на одном примере.

10.2.2. Виды неупакованных типов

Поскольку неупакованные типы представлены без использования указателей, мы не можем хранить их в полиморфном типе данных в неупакованном типе. Например, узел Just типа Just 42# должен отличаться от узла Just типа Just 42; первый хранит целое число непосредственно, а второй — указатель. GHC в настоящее время не поддерживает этот вид узлов Just (ни для какого другого типа данных). Соответственно, *вид* неупакованного типа отличается от вида упакованного типа.

Отчёт по Haskell описывает, что * (написано Type и импортировано из Data.Kind в диалекте Haskell GHC) — это вид обычных типов данных, таких как Int. Кроме того, конструкторы типов могут иметь виды со стрелками; например, Maybe имеет вид Type -> Type. У неупакованных типов есть вид, который определяет их представление во время выполнения. Например, тип Int# имеет вид TYPE 'IntRep, а Double# имеет вид TYPE 'DoubleRep. Эти виды указывают, что представление во время выполнения Int# — это машинный целочисленный тип, а представление во время выполнения Double# — это машинный тип двойной точности с плавающей запятой. В отличие от этого, вид Type на самом деле является просто синонимом для TYPE 'LiftedRep. Более подробные сведения о механизмах TYPE приведены в разделе о полиморфизме представлений во время выполнения.

Учитывая, что вид Int# не является Type, следует, что Maybe Int# запрещено. Аналогично, поскольку переменные типов, как правило, имеют вид Type (например, в (.) :: (b -> c) -> (a -> b) -> a -> c, все переменные типа имеют вид Type), полиморфизм, как правило, не работает с примитивными типами. Шаг назад, это имеет смысл, потому что полиморфная функция должна манипулировать указателями на свои данные, а большинство примитивных типов являются неупакованными.

Существуют некоторые ограничения на использование примитивных типов:

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

    newtype A = MkA Int#
    

    Однако, это ограничение можно ослабить, включив UnliftedNewtypes. В разделе об неупакованных новых типах подробно описано поведение таких типов.

  • Вы не можете привязать переменную с неупакованным типом в *глобальной* привязке.
  • Вы не можете привязать переменную с неупакованным типом в *рекурсивной* привязке.
  • Вы можете привязать неупакованные переменные в (нерекурсивной, неглобальной) привязке шаблона, но вы должны сделать любой такой шаблонный сопоставление строгим. (Если этого не сделать, выдаётся предупреждение -Wunbanged-strict-patterns.) Например, вместо:

    data Foo = Foo Int Int#
    
    f x = let (Foo a b, w) = ..rhs.. in ..body..
    

    вы должны написать:

    data Foo = Foo Int Int#
    
    f x = let !(Foo a b, w) = ..rhs.. in ..body..
    

    поскольку b имеет тип Int#.

10.2.3. Неупакованные кортежи

UnboxedTuples
С момента

6.8.1

Неупакованные кортежи на самом деле не экспортируются GHC.Exts; они являются синтаксическим расширением (UnboxedTuples). Неупакованный кортеж выглядит так:

(# e_1, ..., e_n #)

где e_1..e_n — это выражения любого типа (примитивного или не примитивного).

Тип неупакованного кортежа выглядит так же.

Обратите внимание, что при включённых неупакованных кортежах, (# — это одно лексема, поэтому, например, при использовании операторов, таких как # и #-, необходимо писать ( # ) и ( #- ), а не (#) и (#-).

Неупакованные кортежи используются для функций, которым необходимо вернуть несколько значений, но они избегают выделения памяти в куче, обычно связанного с использованием полноценных кортежей. При возвращении неупакованного кортежа компоненты помещаются непосредственно в регистры или на стек; сам неупакованный кортеж не имеет составного представления. Многие примитивные операции, перечисленные в primops.txt.pp, возвращают неупакованные кортежи. В частности, монады IO и ST используют неупакованные кортежи, чтобы избежать ненужного выделения памяти во время последовательностей операций.

Существуют некоторые ограничения на использование неупакованных кортежей:

  • Типичное использование неупакованных кортежей — просто вернуть несколько значений, связав эти несколько результатов выражением case, таким образом:

    f x y = (# x+1, y-1 #)
    g x = case f x x of { (# a, b #) -> a + b }
    

    Вы можете иметь неупакованный кортеж в привязке шаблона, таким образом

    f x = let (# p,q #) = h x in ..body..
    

    Если типы p и q не являются неупакованными, то полученная привязка ленива, как и любая другая ленивая привязка шаблона Haskell. Приведённый выше пример трансформируется так:

    f x = let t = case h x of { (# p,q #) -> (p,q) }
              p = fst t
              q = snd t
          in ..body..
    

    Действительно, привязки могут быть даже рекурсивными.

10.2.4. Неупакованные суммы

UnboxedSums
С момента

8.2.1

Включить использование синтаксиса неупакованных сумм.

-XUnboxedSums включает новый синтаксис для анонимных неупакованных типов сумм. Синтаксис для неупакованного типа суммы с N альтернативами:

(# t_1 | t_2 | ... | t_N #)

где t_1 … t_N — это типы (которые могут быть неподнятыми, включая неупакованные кортежи и суммы).

Неупакованные кортежи могут использоваться для многоаргументных альтернатив. Например:

(# (# Int, String #) | Bool #)

Синтаксис на уровне терминов аналогичен. Ведущие и предшествующие черты (|) указывают, какая альтернатива. Вот два термина типа, показанного выше:

(# (# 1, "foo" #) | #) -- first alternative

(# | True #) -- second alternative

Синтаксис шаблонов отражает синтаксис терминов:

case x of
  (# (# i, str #) | #) -> ...
  (# | bool #) -> ...

Неупакованные суммы «неупакованы» в том смысле, что вместо выделения сумм в куче и представления значений как указателей, неупакованные суммы представляются как их компоненты, точно так же, как неупакованные кортежи. Эти «компоненты» зависят от альтернатив типа суммы. Как и неупакованные кортежи, неупакованные суммы ленивы в своих поднятых компонентах.

Генератор кода пытается сгенерировать максимально компактную компоновку для каждой неупакованной суммы. В лучшем случае, размер неупакованной суммы — это размер её наибольшей альтернативы плюс один слот (для тега). Алгоритм генерации компоновки памяти для типа суммы работает так:

  • Все типы классифицируются как один из этих классов: 32-битное слово, 64-битное слово, 32-битное число с плавающей запятой, 64-битное число с плавающей запятой, указатель.
  • Для каждой альтернативы типа суммы генерируется компоновка, состоящая из этих полей. Например, если альтернатива имеет поля Int, Float# и String, компоновка будет иметь поля 32-битного слова, 32-битного числа с плавающей запятой и указателя.
  • Поля компоновки затем перекрываются, чтобы конечная компоновка была максимально компактной. Например, предположим, что у нас есть неупакованная сумма:

    (# (# Word32#, String, Float# #)
    |  (# Float#, Float#, Maybe Int #) #)
    

    Конечная компоновка будет чем-то вроде

    Int32, Float32, Float32, Word32, Pointer
    

    Первый Int32 предназначен для тега. Есть два поля Float32 потому, что типы чисел с плавающей запятой не могут перекрываться с другими типами из-за ограничений генератора кода, которые мы надеемся преодолеть в будущем. Второй альтернативе нужны два поля Float32: поле Word32 предназначено для Word32# в первой альтернативе. Поле Pointer разделяется между значениями String и Maybe Int альтернатив.

    Ещё один пример — компоновка для неупакованной версии типа Maybe a, (# (# #) | a #):

    Int32, Pointer
    

    Поле Pointer не используется, когда тег говорит, что это Nothing. В противном случае Pointer указывает на значение в Just. Как упоминалось выше, этот тип ленив в своём поднятом поле. Следовательно, тип

    data Maybe' a = Maybe' (# (# #) | a #)
    

    тождественно изоморфен типу Maybe a, хотя его представление в памяти отличается.

    В вырожденном случае, когда все альтернативы имеют нулевую ширину, например, подобный Bool тип (# (# #) | (# #) #), компоновка неупакованной суммы содержит только поле тега Int32 (т. е. всё представляется целым числом).

10.2.5. Неподнятые новые типы

UnliftedNewtypes
С момента

8.10.1

Включить использование новых типов над типами с неподнятыми представлениями во время выполнения.

GHC реализует расширение UnliftedNewtypes, как указано в этом предложении GHC. UnliftedNewtypes ослабляет ограничения на типы, которые могут появляться внутри newtype. Например, тип

newtype A = MkA Int#

принимается, когда этот расширение включено. Это создаёт тип A :: TYPE 'IntRep и конструктор данных MkA :: Int# -> A. Хотя вид A выводится GHC, в этом типе нет ничего визуально отличительного, что указывало бы на то, что он не является вида Type , как это обычно бывает с newtype. GADTSyntax может использоваться для предоставления сигнатуры вида для дополнительной ясности.

newtype A :: TYPE 'IntRep where
  MkA :: Int# -> A

Механизм Coercible работает с неупакованными newtype так же, как и с упакованными типами. В любом из эквивалентных представлений A , приведенных выше, пользователи также получат доступ к преобразованию между A и Int#.

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

newtype Identity# :: forall (r :: RuntimeRep). TYPE r -> TYPE r where
  MkIdentity# :: forall (r :: RuntimeRep) (a :: TYPE r). a -> Identity# a

И с включенным UnboxedSums

newtype Maybe# :: forall (r :: RuntimeRep). TYPE r -> TYPE (SumRep '[r, TupleRep '[]]) where
  MkMaybe# :: forall (r :: RuntimeRep) (a :: TYPE r). (# a | (# #) #) -> Maybe# a

Это расширение также смягчает некоторые ограничения, связанные с экземплярами семейств данных. В частности, UnliftedNewtypes разрешает newtype instance иметь возвращаемый вид TYPE r, а не только Type. Например, следующие объявления newtype instance будут разрешены:

class Foo a where
  data FooKey a :: TYPE 'IntRep
class Bar (r :: RuntimeRep) where
  data BarType r :: TYPE r

instance Foo Bool where
  newtype FooKey Bool = FooKeyBoolC Int#
instance Bar 'WordRep where
  newtype BarType 'WordRep = BarTypeWordRepC Word#

Стоит отметить, что UnliftedNewtypes не требуется для придания семействам данных самим возвращаемых видов, включающих TYPE, таких как примеры FooKey и BarType выше. Расширение требуется только для объявлений newtype instance, таких как FooKeyBoolC и BarTypeWorkRepC выше.

Это расширение влияет на определение того, имеет ли newtype Полную сигнатуру вида, заданную пользователем (CUSK). Точное влияние указано в разделе о CUSK.

10.3. Синтаксические расширения

10.3.1. Синтаксис Unicode

UnicodeSyntax
С тех пор

6.8.1

Разрешить использование символов Unicode вместо эквивалентных ASCII последовательностей.

Языковое расширение UnicodeSyntax позволяет использовать символы Unicode для обозначения определённых последовательностей ASCII символов. Предоставляются следующие альтернативы:

ASCII

Альтернатива Unicode

Код символа

Имя

::

∷

0x2237

ПРОПОРЦИЯ

=>

⇒

0x21D2

ПРАВЫЙ ДВОЙНОЙ СТРЕЛКА

->

→

0x2192

ПРАВЫЙ СТРЕЛКА

<-

←

0x2190

ЛЕВЫЙ СТРЕЛКА

>-

⤚

0x291a

ПРАВЫЙ СТРЕЛКА-ХВОСТ

-<

⤙

0x2919

ЛЕВЫЙ СТРЕЛКА-ХВОСТ

>>-

⤜

0x291C

ПРАВЫЙ ДВОЙНОЙ СТРЕЛКА-ХВОСТ

-<<

⤛

0x291B

ЛЕВЫЙ ДВОЙНОЙ СТРЕЛКА-ХВОСТ

*

★

0x2605

ЧЁРНЫЙ ЗВЕЗДА

forall

∀

0x2200

ДЛЯ ВСЕХ

(|

⦇

0x2987

ЛЕВЫЙ ОБРАЗОВАТЕЛЬНЫЙ КВАДРАТНЫЙ КРОНЕК

|)

⦈

0x2988

ПРАВЫЙ ОБРАЗОВАТЕЛЬНЫЙ КВАДРАТНЫЙ КРОНЕК

[|

⟦

0x27E6

МАТЕМАТИЧЕСКИЙ ЛЕВЫЙ БЕЛЫЙ КВАДРАТНЫЙ КРОНЕК

|]

⟧

0x27E7

МАТЕМАТИЧЕСКИЙ ПРАВЫЙ БЕЛЫЙ КВАДРАТНЫЙ КРОНЕК

10.3.2. Магическая хеш-сумма

MagicHash
С тех пор

6.8.1

Разрешает использование символа хеш (#) в качестве суффикса идентификатора.

Языковое расширение MagicHash позволяет использовать # в качестве постфиксного модификатора идентификаторов. Таким образом, x# — это допустимая переменная, а T# — допустимый конструктор типа или данных.

Знак хеш не изменяет семантику. Мы склонны использовать имена переменных, заканчивающиеся «#», для неупакованных значений или типов (например, Int#), но это не обязательно; они просто обычные переменные. Расширение MagicHash также не вносит ничего в область видимости. Например, чтобы добавить Int# в область видимости, нужно импортировать GHC.Prim (см. Неупакованные типы и примитивные операции); расширение MagicHash затем позволяет ссылаться на Int# , которое теперь находится в области видимости. Обратите внимание, что с этим вариантом значение x#y = 0 меняется: оно определяет функцию x# , принимающую один аргумент y; чтобы определить оператор #, поставьте пробел: x # y = 0.

Расширение MagicHash также включает некоторые новые формы литералов (см. Неупакованные типы):

  • 'x'# имеет тип Char#
  • "foo"# имеет тип Addr#
  • 3# имеет тип Int#. В общем случае, любой лексический целочисленный литерал Haskell, за которым следует #, является Int# литералом, например, -0x3A# так же, как и 32#.
  • 3## имеет тип Word#. В общем случае, любой неотрицательный лексический целочисленный литерал Haskell, за которым следует ##, является Word#.
  • 3.2# имеет тип Float#.
  • 3.2## имеет тип Double#

10.3.3. Отрицательные литералы

NegativeLiterals
С тех пор

7.8.1

Разрешить использование не-скобочных отрицательных числовых литералов.

Литерал -123 согласно Haskell98 и Haskell 2010, обрабатывается как negate (fromInteger 123). Языковое расширение NegativeLiterals означает, что он вместо этого обрабатывается как fromInteger (-123).

Это может повлиять, когда диапазон положительных и отрицательных значений числового типа не совпадает. Например, в 8-битной арифметике -128 представлен, но +128 нет. Поэтому negate (fromInteger 128) вызовет неожиданное сообщение об ошибке переполнения целочисленного литерала.

10.3.4. Дробные целочисленные литералы

NumDecimals
С тех пор

7.8.1

Разрешить использование синтаксиса с плавающей точкой для целочисленных типов.

Haskell 2010 и Haskell 98 определяют литералы с плавающей точкой с синтаксисом 1.2e6. Эти литералы имеют тип Fractional a => a.

Языковое расширение NumDecimals позволяет также использовать синтаксис с плавающей точкой для экземпляров Integral, и иметь значения, подобные (1.2e6 :: Num a => a)

10.3.5. Двоичные целочисленные литералы

BinaryLiterals
С тех пор

7.10.1

Разрешить использование двоичного обозначения в целочисленных литералах.

Haskell 2010 и Haskell 98 допускают целочисленные литералы в десятичном, восьмеричном (с префиксом 0o или 0O), или шестнадцатеричном формате (с префиксом 0x или 0X).

Языковое расширение BinaryLiterals добавляет поддержку выражения целочисленных литералов в двоичном формате с префиксом 0b или 0B. Например, двоичный целочисленный литерал 0b11001001 будет преобразован в fromInteger 201 при включённом BinaryLiterals.

10.3.6. Шестнадцатеричные литералы с плавающей точкой

HexFloatLiterals
С тех пор

8.4.1

Разрешить запись литералов с плавающей точкой с использованием шестнадцатеричного обозначения.

Шестнадцатеричная запись для литералов с плавающей точкой полезна, когда требуется точно указать константы с плавающей точкой, так как запись литерала тесно связана с кодированием числа в двоичном формате.

В этой записи числа с плавающей точкой записываются с использованием шестнадцатеричных цифр, и поэтому цифры интерпретируются по основанию 16, а не по обычному основанию 10. Это означает, что цифры слева от десятичной точки соответствуют положительным степеням 16, а цифры справа — отрицательным.

Вы также можете записать явный показатель степени, который похож на показатель степени в десятичной записи с следующими отличиями: - показатель степени начинается с p вместо e - показатель степени записывается по основанию 10 (не 16) - основание показателя степени равно 2 (не 16).

С точки зрения кодирования в двоичном формате, каждая шестнадцатеричная цифра соответствует 4 битам, и вы можете представить, что показатель степени «перемещает» число с плавающей точкой на одну позицию влево (отрицательное) или вправо (положительное). Вот несколько примеров:

  • 0x0.1 эквивалентно 1/16
  • 0x0.01 эквивалентно 1/256
  • 0xF.FF эквивалентно 15 + 15/16 + 15/256
  • 0x0.1p4 эквивалентно 1
  • 0x0.1p-4 эквивалентно 1/256
  • 0x0.1p12 эквивалентно 256

10.3.7. Числовые подчеркивания

NumericUnderscores
С

8.6.1

Разрешает использование подчеркиваний в числовых литералах.

GHC допускает числовые литералы в десятичной, восьмеричной, шестнадцатеричной, двоичной или формате с плавающей точкой.

Языковой расширение NumericUnderscores добавляет поддержку выражения подчеркиваний в числовых литералах. Например, числовой литерал 1_000_000 будет распарсен в 1000000 при включенном расширении NumericUnderscores. То есть, подчеркивания в числовых литералах игнорируются при включенном NumericUnderscores. См. также #14473.

Например:

-- decimal
million    = 1_000_000
billion    = 1_000_000_000
lightspeed = 299_792_458
version    = 8_04_1
date       = 2017_12_31

-- hexadecimal
red_mask = 0xff_00_00
size1G   = 0x3fff_ffff

-- binary
bit8th   = 0b01_0000_0000
packbits = 0b1_11_01_0000_0_111
bigbits  = 0b1100_1011__1110_1111__0101_0011

-- float
pi       = 3.141_592_653_589_793
faraday  = 96_485.332_89
avogadro = 6.022_140_857e+23

-- function
isUnderMillion = (< 1_000_000)

clip64M x
    | x > 0x3ff_ffff = 0x3ff_ffff
    | otherwise = x

test8bit x = (0b01_0000_0000 .&. x) /= 0

О допустимости:

x0 = 1_000_000   -- valid
x1 = 1__000000   -- valid
x2 = 1000000_    -- invalid
x3 = _1000000    -- invalid

e0 = 0.0001      -- valid
e1 = 0.000_1     -- valid
e2 = 0_.0001     -- invalid
e3 = _0.0001     -- invalid
e4 = 0._0001     -- invalid
e5 = 0.0001_     -- invalid

f0 = 1e+23       -- valid
f1 = 1_e+23      -- valid
f2 = 1__e+23     -- valid
f3 = 1e_+23      -- invalid

g0 = 1e+23       -- valid
g1 = 1e+_23      -- invalid
g2 = 1e+23_      -- invalid

h0 = 0xffff      -- valid
h1 = 0xff_ff     -- valid
h2 = 0x_ffff     -- valid
h3 = 0x__ffff    -- valid
h4 = _0xffff     -- invalid

10.3.8. Защитные шаблоны

NoPatternGuards
Подразумевается

Haskell98

С

6.8.1

Отключить защитные шаблоны.

10.3.9. Шаблоны представлений

ViewPatterns
С

6.10.1

Разрешить использование синтаксиса шаблонов представлений.

Шаблоны представлений включены языковым расширением ViewPatterns. Более подробную информацию и примеры шаблонов представлений можно найти на странице Wiki.

Шаблоны представлений чем-то похожи на защитные шаблоны, которые могут быть вложены внутри других шаблонов. Они являются удобным способом сопоставления с образцом для значений абстрактных типов. Например, в реализации языка программирования мы можем представить синтаксис типов языка следующим образом:

type Typ

data TypView = Unit
             | Arrow Typ Typ

view :: Typ -> TypView

-- additional operations for constructing Typ's ...

Представление Typ сохраняется как абстрактное, что позволяет реализациям использовать сложную структуру (например, хэширование для управления обменом).

size :: Typ -> Integer
size t = case view t of
  Unit -> 1
  Arrow t1 t2 -> size t1 + size t2

Необходимо перебирать случаи, а не использовать определение функции равенства.

t

Шаблоны представлений позволяют вызывать функцию представления внутри шаблона и сопоставлять результат с образцом:

size (view -> Unit) = 1
size (view -> Arrow t1 t2) = size t1 + size t2

То есть, мы добавляем новый вид шаблона, записанный ⟨выражение⟩ -> ⟨шаблон⟩, что означает «применить выражение к тому, с чем мы пытаемся сопоставить, а затем сопоставить результат этого применения с шаблоном». Выражение может быть любым выражением Haskell типа функции, и шаблоны представлений могут использоваться везде, где используются шаблоны.

Семантика шаблона ( ⟨выражение⟩ -> ⟨шаблон⟩ ) таковы:

  • Область видимости: Переменные, связанные шаблоном представления, — это переменные, связанные шаблоном ⟨шаблон⟩.

    Любые переменные в ⟨выражение⟩ являются связанными, но переменные, связанные «слева» в шаблоне, находятся в области видимости. Эта функция позволяет, например, использовать один аргумент функции в представлении другого аргумента. Например, функция clunky из Защитных шаблонов можно записать с использованием шаблонов представлений следующим образом:

    clunky env (lookup env -> Just val1) (lookup env -> Just val2) = val1 + val2
    ...other equations for clunky...
    

    Более точно, правила области видимости:

    • В одном шаблоне переменные, связанные шаблонами слева от выражения шаблона представления, находятся в области видимости. Например:

      example :: Maybe ((String -> Integer,Integer), String) -> Bool
      example (Just ((f,_), f -> 4)) = True
      

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

      example :: (String -> Integer) -> String -> Bool
      example f (f -> 4) = True
      

      То есть, область видимости такая же, как если бы аргументы с последовательной обработкой были объединены в кортеж.

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

      let {(x -> y) = e1 ;
           (y -> x) = e2 } in x
      

    (Для некоторых дополнений к этому проектному решению см. #4061.

  • Типизация: Если ⟨выражение⟩ имеет тип ⟨T1⟩ -> ⟨T2⟩, а ⟨шаблон⟩ сопоставляет ⟨T2⟩, то весь шаблон представления сопоставляет ⟨T1⟩.
  • Сопоставление: К уравнениям в разделе 3.17.3 документации Haskell 98 добавьте следующее:

    case v of { (e -> p) -> e1 ; _ -> e2 }
     =
    case (e v) of { p -> e1 ; _ -> e2 }
    

    То есть, для сопоставления переменной ⟨v⟩ с шаблоном ( ⟨выражение⟩ -> ⟨шаблон⟩ ), вычислите ( ⟨выражение⟩ ⟨v⟩ ) и сопоставьте результат с шаблоном ⟨шаблон⟩.

  • Эффективность: Когда одна и та же функция представления применяется в нескольких ветвях определения функции или выражения case (например, в size выше), GHC пытается собрать эти применения в одно вложенное выражение case, чтобы функция представления применялась только один раз. Компиляция шаблонов в GHC следует алгоритму матрицы, описанному в главе 4 The Implementation of Functional Programming Languages. Если верхние строки первого столбца матрицы являются всеми шаблонами представлений с «одним и тем же» выражением, эти шаблоны преобразуются в одно вложенное выражение case. Это включает, например, смежные шаблоны представлений, которые выровнены в кортеже, как в

    f ((view -> A, p1), p2) = e1
    f ((view -> B, p3), p4) = e2
    

    Текущее представление того, когда два выражения шаблонов представлений являются «одинаковыми», очень ограничено: оно не является даже полным синтаксическим равенством. Однако оно включает переменные, литералы, применения и кортежи; например, два экземпляра view ("hi", "there") будут собраны. Однако текущая реализация не сравнивает до альфа-эквивалентности, поэтому два экземпляра (x, view x -> y) не будут объединены.

10.3.10. Шаблоны n+k

NPlusKPatterns
Подразумевается

Haskell98

С

6.12.1

Включить использование n+k шаблонов.

10.3.11. Рекурсивное выражение do

RecursiveDo
С

6.8.1

Разрешить использование рекурсивного do выражения.

Выражение do в Haskell 98 не допускает рекурсивных связей, то есть переменные, связанные в выражении do, видны только в текстовом блоке, следующему за ним. Сравните с выражением let, где связанные переменные видны во всем блоке связей.

Оказывается, такие рекурсивные связи действительно имеют смысл для различных монад, но не для всех. В частности, рекурсия в этом смысле требует оператора неподвижной точки для базовой монады, заданного методом mfix класса MonadFix, определенного в Control.Monad.Fix следующим образом:

class Monad m => MonadFix m where
   mfix :: (a -> m a) -> m a

Монады Haskell Maybe, [] (список), ST (оба строгого и ленивого типа), IO, и многие другие монады имеют экземпляры MonadFix. С другой стороны, монада продолжения со схемой (a -> r) -> r — нет.

Для монад, которые принадлежат классу MonadFix, GHC предоставляет расширенную версию выражения do, которая позволяет рекурсивные связи. RecursiveDo (языковой псевдоним: RecursiveDo) обеспечивает необходимую синтаксическую поддержку, вводя ключевые слова mdo и rec для верхнего и нижнего уровней обозначения соответственно. В отличие от связей в выражении do, связи, вводимые mdo и rec, определены рекурсивно, как и в обычном выражении let. Из-за нового ключевого слова mdo, это обозначение также называется mdo-нотацией.

Вот простой (хотя и придуманный) пример:

{-# LANGUAGE RecursiveDo #-}
justOnes = mdo { xs <- Just (1:xs)
               ; return (map negate xs) }

или эквивалентно

{-# LANGUAGE RecursiveDo #-}
justOnes = do { rec { xs <- Just (1:xs) }
              ; return (map negate xs) }

Как вы можете догадаться, justOnes будет вычисляться в Just [-1,-1,-1,....

Реализация GHC для обозначения mdo тесно следует исходному переводу, как описано в статье Рекурсивный do для Haskell, которая, в свою очередь, основана на работе Рекурсия значений в монадических вычислениях. Кроме того, GHC расширяет синтаксис, описанный в первой статье, с помощью синтаксиса более низкого уровня, помеченного ключевым словом rec , как описано ниже.

10.3.11.1. Группы рекурсивных связываний

Расширение RecursiveDo также вводит новое ключевое слово rec, которое оборачивает группу взаиморекурсивных монадических утверждений внутри выражения do, формируя единственное утверждение. Подобно утверждению let внутри do, переменные, привязанные в rec, видны во всей группе rec и ниже неё. Например, сравните

do { a <- getChar            do { a <- getChar
   ; let { r1 = f a r2          ; rec { r1 <- f a r2
   ;     ; r2 = g r1 }          ;     ; r2 <- g r1 }
   ; return (r1 ++ r2) }        ; return (r1 ++ r2) }

В обоих случаях r1 и r2 доступны как в блоке let или rec, так и в последующих утверждениях. Разница заключается в том, что let не является монадическим, а rec — монадическим. (В Haskell let на самом деле letrec, конечно.)

Семантика rec довольно проста. Всякий раз, когда GHC находит группу rec, он вычисляет множество связанных переменных и вводит соответствующий вызов базового оператора монадической рекурсии значений mfix, принадлежащего классу MonadFix . Вот пример:

rec { b <- f a c     ===>    (b,c) <- mfix (\ ~(b,c) -> do { b <- f a c
    ; c <- f b a }                                         ; c <- f b a
                                                           ; return (b,c) })

Как обычно, метапеременные b, c и т.д. могут быть произвольными шаблонами. В общем случае утверждение rec ss преобразуется в утверждение

vs <- mfix (\ ~vs -> do { ss; return vs })

где vs — кортеж переменных, связанных с ss.

Обратите особое внимание на то, что перевод блока rec включает только оборачивание вызова mfix; он не выполняет никакого другого анализа привязок. Последнее — задача обозначения mdo, которое описано далее.

10.3.11.2. Обозначение mdo

Блок rec указывает компилятору, где именно следует завязать рекурсивный узел. Оказывается, расположение рекурсивных узлов может быть довольно деликатным: в частности, мы хотели бы, чтобы узлы были обернуты вокруг наименьших возможных групп. Этот процесс известен как сегментация и подробно описан в разделе 3.2 статьи Рекурсивный do для Haskell. Сегментация улучшает полиморфизм и уменьшает размер рекурсивного узла. Самое главное, она предотвращает ненужное вмешательство, вызванное фундаментальной проблемой так называемого аксиомы сжатия справа для монадической рекурсии. Короче говоря, большинство интересующих нас монад (IO, строгое состояние и т.д.) не имеют операторов рекурсии, которые удовлетворяют этому аксиому, и поэтому отсутствие сегментации может вызвать ненужное вмешательство, изменяя поведение завершения результирующего перевода. (Подробности см. в разделах 3.1 и 7.2.2 статьи Рекурсия значений в монадических вычислениях.)

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

Определение является синтаксическим:

  • Генератор ⟨g⟩ зависит от последующего генератора ⟨g’⟩, если

    • ⟨g’⟩ определяет переменную, используемую ⟨g⟩, или
    • ⟨g’⟩ расположено текстово между ⟨g⟩ и ⟨g’’⟩, где ⟨g⟩ зависит от ⟨g’’⟩.
  • Сегмент заданного выражения mdo — это минимальная последовательность генераторов, такая, что ни один генератор последовательности не зависит от внешнего генератора. В качестве частного случая, хотя это не генератор, последнее выражение в выражении mdo считается формирующим сегмент само по себе.

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

Вот пример выражения mdo и его перевод в блоки rec:

mdo { a <- getChar      ===> do { a <- getChar
    ; b <- f a c                ; rec { b <- f a c
    ; c <- f b a                ;     ; c <- f b a }
    ; z <- h a b                ; z <- h a b
    ; d <- g d e                ; rec { d <- g d e
    ; e <- g a z                ;     ; e <- g a z }
    ; putChar c }               ; putChar c }

Обратите внимание, что данное выражение mdo может привести к созданию нескольких блоков rec . Если нет рекурсивных зависимостей, mdo не введёт никаких блоков rec . В последнем случае выражение mdo точно такое же, как выражение do , как ожидалось.

В заключение, для заданного выражения mdo GHC сначала выполняет сегментацию, вводя блоки rec для обертывания минимальных рекурсивных групп. Затем каждый полученный rec преобразуется, используя вызов Control.Monad.Fix.mfix , как описано в предыдущем разделе. Исходное выражение mdo проходит проверку типов тогда и только тогда, когда преобразованная версия сделала бы это.

Вот некоторые другие важные моменты при использовании синтаксиса рекурсивного do:

  • Он активируется с помощью расширения RecursiveDo или директивы LANGUAGE RecursiveDo . (Это же расширение активирует как обозначение mdo, так и использование блоков rec внутри выражений do.)
  • Блоки rec также могут использоваться внутри выражений mdo, которые будут обрабатываться как одно утверждение. Однако рекомендуется использовать блоки mdo или rec в одном выражении.
  • Если для монады требуются рекурсивные привязки, то эта монада должна быть объявлена экземпляром класса MonadFix .
  • Следующие экземпляры MonadFix предоставляются автоматически: Список, Может быть, IO. Кроме того, модули Control.Monad.ST и Control.Monad.ST.Lazy предоставляют экземпляры класса MonadFix для внутренней монады состояния Haskell (строгого и ленивого соответственно).
  • Как для привязок let и where, не допускается перекрытие имён внутри выражения mdo или блока rec; то есть, все имена, привязанные в одном rec , должны быть разными. (GHC выдаст сообщение об ошибке, если это не так.)

10.3.12. Применяемая do-нотация

ApplicativeDo
С

8.0.1

Разрешить использование обозначения Applicative do .

Параметр языка ApplicativeDo позволяет использовать альтернативный перевод do-нотации, который использует операторы <$>, <*>, а также join насколько это возможно. Есть две основные причины для этого:

  • Мы можем использовать do-нотацию с типами, которые являются экземплярами Applicative и Functor, но не Monad
  • В некоторых монадах использование применяемых операторов более эффективно, чем монадическая привязка. Например, это может позволить больше параллелизма.

Применяемая do-нотация сохраняет исходную семантику при условии, что экземпляр Applicative удовлетворяет <*> = ap и pure = return (это верно для всех распространенных монадических типов). Таким образом, вы обычно можете включить ApplicativeDo без опасения нарушить свою программу. Есть одна ловушка, о которой нужно помнить; см. Что следует иметь в виду.

С ApplicativeDo нет синтаксических изменений. Единственный способ, которым это проявляется на уровне исходного кода, заключается в том, что у вас может быть выражение do , которое не требует ограничения Monad . Например, в GHCi:

Prelude> :set -XApplicativeDo
Prelude> :t \m -> do { x <- m; return (not x) }
\m -> do { x <- m; return (not x) }
  :: Functor f => f Bool -> f Bool

Этот пример требует только Functor, потому что он переводится в (\x -> not x) <$> m. Более сложный пример требует Applicative,

Prelude> :t \m -> do { x <- m 'a'; y <- m 'b'; return (x || y) }
\m -> do { x <- m 'a'; y <- m 'b'; return (x || y) }
  :: Applicative f => (Char -> f Bool) -> f Bool

Здесь GHC перевёл выражение в

(\x y -> x || y) <$> m 'a' <*> m 'b'

Можно увидеть фактический перевод, используя -ddump-ds, но будьте предупреждены, вывод довольно объёмный.

Обратите внимание, что если выражение нельзя перевести в использование <$>, <*> только, то оно понесёт ограничение Monad как обычно. Это происходит, когда есть зависимость от значения, произведённого предыдущим утверждением в блоке do:

Prelude> :t \m -> do { x <- m True; y <- m x; return (x || y) }
\m -> do { x <- m True; y <- m x; return (x || y) }
  :: Monad m => (Bool -> m Bool) -> m Bool

Здесь m x зависит от значения x , произведённого первым утверждением, поэтому выражение нельзя перевести, используя <*>.

В общем случае правило, при котором утверждение do несёт ограничение Monad , следующее. Если выражение do имеет следующий вид:

do p1 <- E1; ...; pn <- En; return E

где ни одна из переменных, определённых p1...pn , не упоминается в E1...En, а p1...pn все переменные или ленивые шаблоны, то выражение будет требовать только Applicative. В противном случае выражение будет требовать Monad. Блок может возвращать чистое выражение E в зависимости от результатов p1...pn с return или pure.

Примечание: последнее утверждение должно точно соответствовать одному из этих шаблонов:

  • return E
  • return $ E
  • pure E
  • pure $ E

в противном случае GHC не сможет распознать его как оператор return, и преобразование на использование <$>, которое мы видели выше, не применяется. В частности, небольшие вариации, такие как return . Just $ x или let x = e in return x, не будут распознаны.

Если конечный оператор не имеет одного из этих форм, GHC возвращается к стандартному do разбору, и выражение потребует ограничения Monad.

Когда операторы выражения do имеют зависимости между ними, и ApplicativeDo не может вывести тип Applicative, он использует эвристический алгоритм, чтобы попытаться использовать <*> по возможности. Этот алгоритм обычно находит наилучшее решение, но в редких сложных случаях он может пропустить возможность. Существует алгоритм, который находит оптимальное решение, предоставляемый как опция:

-foptimal-applicative-do
Since

8.0.1

Включает альтернативный алгоритм для выбора места использования <*> в сочетании с расширением языка ApplicativeDo. Этот алгоритм всегда находит оптимальное решение, но он дорогостоящий: O(n^3), поэтому этот параметр может привести к длительным временам компиляции при наличии очень больших выражений do (более 100 операторов). По умолчанию алгоритм ApplicativeDo является O(n^2).

10.3.12.1. Строгие шаблоны

Строгий шаблон соответствия в операторе связывания предотвращает ApplicativeDo от преобразования этого оператора на использование Applicative. Это происходит потому, что преобразование изменит семантику, сделав выражение более ленивым.

Например, этот код потребует ограничения Monad:

> :t \m -> do { (x:xs) <- m; return x }
\m -> do { (x:xs) <- m; return x } :: Monad m => m [b] -> m b

но приведение шаблона соответствия к ленивому виду позволяет иметь ограничение Functor:

> :t \m -> do { ~(x:xs) <- m; return x }
\m -> do { ~(x:xs) <- m; return x } :: Functor f => f [b] -> f b

«Строгое соответствие шаблона» — это любое соответствие шаблона, которое может потерпеть неудачу. Например, (), (x:xs), !z, и C x являются строгими шаблонами, но x и ~(1,2) не являются. Для целей ApplicativeDo, соответствие шаблона конструктору newtype считается строгим.

Когда в последовательности операторов есть строгое соответствие шаблона, ApplicativeDo помещает >>= между этим оператором и следующим. Последовательность может быть преобразована на использование <*> в другом месте, но строгое соответствие шаблона и следующий оператор всегда будут соединены с >>=, чтобы сохранить ту же семантику строгости, что и стандартная запись do-notation. Если вы этого не хотите, просто поместите ~ на соответствие шаблона, чтобы сделать его ленивым.

10.3.12.2. Опасные моменты

Ваш код должен работать так же, как и раньше, при включенном ApplicativeDo, при условии использования обычных экземпляров Applicative. Однако, если вы определите экземпляр Functor или Applicative с использованием do-notation, то, скорее всего, он превратится в бесконечный цикл в GHC. Например, если вы сделаете это:

instance Functor MyType where
    fmap f m = do x <- m; return (f x)

Тогда применение разбора преобразует его в

instance Functor MyType where
    fmap f m = fmap (\x -> f x) m

И программа будет циклиться во время выполнения. Аналогично, экземпляр Applicative вида

instance Applicative MyType where
    pure = return
    x <*> y = do f <- x; a <- y; return (f a)

приведет к бесконечному циклу при вызове <*>.

Так же, как вы не определяете экземпляр Monad с помощью do-notation, вы не должны определять экземпляр Functor или Applicative с помощью do-notation (при использовании ApplicativeDo). Правильный способ определения этих экземпляров в терминах Monad — использование операций Monad напрямую, например

instance Functor MyType where
    fmap f m = m >>= return . f

instance Applicative MyType where
    pure = return
    (<*>) = ap

10.3.13. Параллельные списки-понимания

ParallelListComp
Since

6.8.1

Разрешить синтаксис параллельных списков-понимания.

Параллельные списки-понимания являются естественным расширением списков-понимания. Спискам-понимания можно рассматривать как удобный синтаксис для записи отображений и фильтров. Параллельные понимания расширяют это, включая семейство zipWith.

Параллельное понимание списка содержит несколько независимых ветвей списков квалификаторов, каждая из которых отделена символом |. Например, следующее объединяет два списка:

[ (x, y) | x <- xs | y <- ys ]

Поведение параллельных списков-понимания соответствует поведению zip, при котором результирующий список будет иметь такую же длину, как у самой короткой ветви.

Мы можем определить параллельные списки-понимания, переводя их в обычные списки-понимания. Вот основная идея:

Предположим параллельное понимание вида:

[ e | p1 <- e11, p2 <- e12, ...
    | q1 <- e21, q2 <- e22, ...
    ...
]

Это будет переведено в:

[ e | ((p1,p2), (q1,q2), ...) <- zipN [(p1,p2) | p1 <- e11, p2 <- e12, ...]
                                      [(q1,q2) | q1 <- e21, q2 <- e22, ...]
                                      ...
]

где zipN — соответствующий zip для заданного количества ветвей.

10.3.14. Обобщенные (по типу SQL) списки-понимания

TransformListComp
Since

6.10.1

Разрешить использование обобщенного синтаксиса списков-понимания (по типу SQL). Это вводит ключевые слова group, by, и using.

Обобщенные списки-понимания представляют собой дальнейшее расширение синтаксического сахара списков-понимания, позволяющее выполнять операции, такие как сортировка и группировка, знакомые по SQL. Они подробно описаны в статье Comprehensive comprehensions: comprehensions with “order by” and “group by”, за исключением того, что используемый нами синтаксис немного отличается от синтаксиса в статье.

Расширение включено с помощью расширения TransformListComp.

Вот пример:

employees = [ ("Simon", "MS", 80)
            , ("Erik", "MS", 100)
            , ("Phil", "Ed", 40)
            , ("Gordon", "Ed", 45)
            , ("Paul", "Yale", 60) ]

output = [ (the dept, sum salary)
         | (name, dept, salary) <- employees
         , then group by dept using groupWith
         , then sortWith by (sum salary)
         , then take 5 ]

В этом примере список output примет значение:

[("Yale", 60), ("Ed", 85), ("MS", 180)]

Существует три новых ключевых слова: group, by, и using. (Функции sortWith и groupWith не являются ключевыми словами; они являются обычными функциями, экспортируемыми GHC.Exts.)

Есть пять новых форм квалификатора списков-понимания, все введенные существующим ключевым словом then:

  • then f
    

    Этот оператор требует, чтобы f имел тип для всех a. [a] -> [a]. Вы можете увидеть пример его использования в примере, так как эта форма используется для применения take 5.

  • then f by e
    

    Эта форма похожа на предыдущую, но позволяет создавать функцию, которая будет передаваться в качестве первого аргумента в f. Вследствие этого f должен иметь тип forall a. (a -> t) -> [a] -> [a]. Как видно из типа, эта функция позволяет f «выводить» некоторую информацию из элементов списка, который он преобразует.

    Пример показан в начальном примере, где sortWith снабжается функцией, которая позволяет ему находить sum salary для любого элемента в списке-понимании, который он преобразует.

  • then group by e using f
    

    Это наиболее общая из операторов группировки. В этой форме f должен иметь тип forall a. (a -> t) -> [a] -> [[a]]. Как и в случае с then f by e выше, первый аргумент — это функция, предоставляемая компилятором в f, которая позволяет вычислить e для каждого элемента преобразуемого списка. Однако в отличие от случая без группировки, f дополнительно разбивает список на несколько подсписков: это означает, что в любой момент после этого оператора связывающие переменные, появляющиеся перед ним в списке-понимании, относятся к списку возможных значений, а не к отдельным значениям. Чтобы помочь в понимании этого, давайте рассмотрим пример:

    -- This works similarly to groupWith in GHC.Exts, but doesn't sort its input first
    groupRuns :: Eq b => (a -> b) -> [a] -> [[a]]
    groupRuns f = groupBy (\x y -> f x == f y)
    
    output = [ (the x, y)
    | x <- ([1..3] ++ [1..2])
    , y <- [4..6]
    , then group by x using groupRuns ]
    

    Это приводит к тому, что переменная output принимает значение ниже:

    [(1, [4, 5, 6]), (2, [4, 5, 6]), (3, [4, 5, 6]), (1, [4, 5, 6]), (2, [4, 5, 6])]
    

    Обратите внимание, что мы использовали функцию the для изменения типа x со списка на его исходный числовой тип. Переменная y, в отличие от этого, остается неизменной в форме списка, введенной группировкой.

  • then group using f
    

    В этой форме оператора группировки f должен иметь просто тип forall a. [a] -> [[a]], который будет использоваться для непосредственной группировки списков-пониманий. Пример такой формы выглядит следующим образом:

    output = [ x
    | y <- [1..5]
    , x <- "hello"
    , then group using inits]
    

    Это приведет к списку, содержащему все префиксы слова «hello», написанные 5 раз:

    ["","h","he","hel","hell","hello","helloh","hellohe","hellohel","hellohell","hellohello","hellohelloh",...]
    

10.3.15. Понимания монады

MonadComprehensions
Since

7.2.1

Включить синтаксис списков-понимания для произвольных монад.

Понимания монады обобщают запись списков-понимания, включая параллельные понимания (Параллельные списки-понимания) и преобразующие понимания (Обобщенные (по типу SQL) списки-понимания), чтобы они работали для любой монады.

Понимания монады поддерживают:

  • Связывания:

    [ x + y | x <- Just 1, y <- Just 2 ]
    

    Связывания переводятся с помощью функций (>>=) и return в обычную запись с использованием конструкции `do`:

    do x <- Just 1
       y <- Just 2
       return (x+y)
    
  • Ограничения:

    [ x | x <- [1..10], x <= 5 ]
    

    Ограничения переводятся с помощью функции guard, которая требует экземпляр MonadPlus:

    do x <- [1..10]
       guard (x <= 5)
       return x
    
  • Выражения преобразования (как в TransformListComp):

    [ x+y | x <- [1..10], y <- [1..x], then take 2 ]
    

    Это переводится как:

    do (x,y) <- take 2 (do x <- [1..10]
                           y <- [1..x]
                           return (x,y))
       return (x+y)
    
  • Выражения группировки (как в TransformListComp):

    [ x | x <- [1,1,2,2,3], then group by x using GHC.Exts.groupWith ]
    [ x | x <- [1,1,2,2,3], then group using myGroup ]
    
  • Параллельные выражения (как в ParallelListComp):

    [ (x+y) | x <- [1..10]
            | y <- [11..20]
            ]
    

    Параллельные выражения переводятся с использованием функции mzip, которая требует экземпляр MonadZip, определённый в Control.Monad.Zip:

    do (x,y) <- mzip (do x <- [1..10]
                         return x)
                     (do y <- [11..20]
                         return y)
       return (x+y)
    

Все эти возможности включены по умолчанию, если включен MonadComprehensions расширение. Типы и более подробные примеры использования выражений описаны в предыдущих главах Обобщённые (похожие на SQL) выражения для списков и Параллельные выражения для списков. В общем случае нужно только заменить тип [a] на тип Monad m => m a для выражений для монады.

Примечание

Хотя большинство этих примеров используют монаду списка, выражения для монады работают для любой монады. Пакет base предлагает все необходимые экземпляры для списков, что делает MonadComprehensions обратной совместимой с встроенными, трансформирующими и параллельными выражениями для списков.

Более формально, переписывание происходит следующим образом. Мы пишем D[ e | Q] для обозначения переписывания выражения для монады [ e | Q]:

Expressions: e
Declarations: d
Lists of qualifiers: Q,R,S

-- Basic forms
D[ e | ]               = return e
D[ e | p <- e, Q ]  = e >>= \p -> D[ e | Q ]
D[ e | e, Q ]          = guard e >> \p -> D[ e | Q ]
D[ e | let d, Q ]      = let d in D[ e | Q ]

-- Parallel comprehensions (iterate for multiple parallel branches)
D[ e | (Q | R), S ]    = mzip D[ Qv | Q ] D[ Rv | R ] >>= \(Qv,Rv) -> D[ e | S ]

-- Transform comprehensions
D[ e | Q then f, R ]                  = f D[ Qv | Q ] >>= \Qv -> D[ e | R ]

D[ e | Q then f by b, R ]             = f (\Qv -> b) D[ Qv | Q ] >>= \Qv -> D[ e | R ]

D[ e | Q then group using f, R ]      = f D[ Qv | Q ] >>= \ys ->
                                        case (fmap selQv1 ys, ..., fmap selQvn ys) of
                                         Qv -> D[ e | R ]

D[ e | Q then group by b using f, R ] = f (\Qv -> b) D[ Qv | Q ] >>= \ys ->
                                        case (fmap selQv1 ys, ..., fmap selQvn ys) of
                                           Qv -> D[ e | R ]

where  Qv is the tuple of variables bound by Q (and used subsequently)
       selQvi is a selector mapping Qv to the ith component of Qv

Operator     Standard binding       Expected type
--------------------------------------------------------------------
return       GHC.Base               t1 -> m t2
(>>=)        GHC.Base               m1 t1 -> (t2 -> m2 t3) -> m3 t3
(>>)         GHC.Base               m1 t1 -> m2 t2         -> m3 t3
guard        Control.Monad          t1 -> m t2
fmap         GHC.Base               forall a b. (a->b) -> n a -> n b
mzip         Control.Monad.Zip      forall a b. m a -> m b -> m (a,b)

Выражение должно быть корректно типизировано, когда его переписывание было бы корректно типизировано, за исключением того (как обсуждалось в Обобщённые (похожие на SQL) выражения для списков), в пунктах «тогда f» и «тогда сгруппировать с использованием f», когда квалификатор «по b» опущен, аргумент f должен иметь полиморфный тип. В частности, «тогда Data.List.sort» и «тогда сгруппировать с использованием Data.List.group» недостаточно полиморфны.

Выражения для монады поддерживают переопределяемую синтаксическую конструкцию (Переопределяемая синтаксическая конструкция и неявный импорт Prelude). Без переопределяемой синтаксической конструкции используются операторы из модуля «стандартное связывание»; с переопределяемой синтаксической конструкцией операторы ищутся в текущем лексическом пространстве. Например, параллельные выражения будут проверяться на типизацию и переписываться с использованием того, что представляет собой «mzip» в области видимости.

Переопределяемые операторы должны иметь указанные в таблице выше «Ожидаемый тип». Эти типы удивительно общие. Например, вы можете использовать оператор связывания с типом

(>>=) :: T x y a -> (a -> T y z b) -> T x z b

В случае с трансформирующими выражениями обратите внимание, что группы параметризованы по некоторому произвольному типу n (при условии, что он имеет fmap, а также то, что выражение является произвольной монадой.

10.3.16. Новый механизм переписывания с моноидным неудачным результатом

MonadFailDesugaring
С

8.0.1

Используйте MonadFail.fail вместо устаревшей функции Monad.fail при переписывании опровергаемых шаблонов в блоках do.

Расширение -XMonadFailDesugaring переключает переписывание блоков do на использование MonadFail.fail вместо Monad.fail.

Это расширение включено по умолчанию начиная с GHC 8.6.1 в рамках Предложения по MonadFail (MFP).

Это расширение временное и будет устаревшим в будущей версии.

10.3.17. Переопределяемая синтаксическая конструкция и неявный импорт Prelude

NoImplicitPrelude
С

6.8.1

Не импортировать Prelude по умолчанию.

GHC обычно импортирует файлы Prelude.hi для вас. Если вы предпочитаете, чтобы этого не происходило, укажите опцию -XNoImplicitPrelude. Идея в том, что вы можете импортировать свой собственный Prelude. (Но не называйте его Prelude; пространство имён Haskell-модулей плоское, и вы не должны конфликтовать с каким-либо модулем Prelude.)

RebindableSyntax
Подразумевает

NoImplicitPrelude

С

7.0.1

Включить переопределение различных обычно встроенных операций.

Предположим, вы импортируете свой собственный Prelude, чтобы определить свою собственную иерархию числовых классов. Это полностью разрушает цель, если буквальный «1» означает «Prelude.fromInteger 1», что и определено в документе Haskell. Таким образом, расширение RebindableSyntax заставляет следующие части встроенного синтаксиса ссылаться на то, что находится в области видимости, а не на версии из Prelude:

  • Целочисленная литерал 368 означает «fromInteger (368::Integer)», а не «Prelude.fromInteger (368::Integer)».
  • Дробные литералы обрабатываются аналогичным образом, за исключением того, что переписывание — fromRational (3.68::Rational).
  • Строковые литералы также обрабатываются аналогично, за исключением того, что переписывание — fromString ("368"::String).
  • Тест на равенство в перегруженном числовом шаблоне использует то, что находится в области видимости (==).
  • Операция вычитания и тест «больше или равно» в шаблонах n+k используют то, что находится в области видимости (-) и (>=).
  • Отрицание (например, «- (f x)») означает «negate (f x)» как в числовых шаблонах, так и в выражениях.
  • Условные операторы (например, «if e1 then e2 else e3») означают «ifThenElse e1 e2 e3». Однако выражения case не затрагиваются.
  • Конструкция `do` переписывается с использованием функций (>>=), (>>), и fail, которые находятся в области видимости (а не версии из Prelude). Выражения для списков, mdo (Рекурсивное обозначение `do`), и параллельные выражения для массивов не затрагиваются.
  • Стрелочная запись (см. Стрелочная запись) использует функции arr, (>>>), first, app, (|||) и loop которые находятся в области видимости. Но, в отличие от других конструкций, типы этих функций должны очень точно соответствовать типам из Prelude. Подробности изменчивы; если вы хотите их использовать, спросите!
  • Запись списков, таких как [x,y] или [m..n] также может обрабатываться с помощью переопределяемого синтаксиса, если вы используете -XOverloadedLists; см. Перегруженные списки.
  • Перегруженный метка «#foo» означает «fromLabel @"foo"», а не «GHC.OverloadedLabels.fromLabel @"foo"» (см. Перегруженные метки).

RebindableSyntax подразумевает NoImplicitPrelude.

Во всех случаях (кроме стрелочной записи), статическая семантика должна быть такой же, как у переписанной формы, даже если это немного неожиданно. Например, статическая семантика литерала 368 точно такая же, как у fromInteger (368::Integer) ; для fromInteger допустимо наличие любого из типов:

fromInteger :: Integer -> Integer
fromInteger :: forall a. Foo a => Integer -> a
fromInteger :: Num a => a -> Integer
fromInteger :: Integer -> Bool -> Bool

Будьте осторожны: это экспериментальная функция с меньшим количеством проверок, чем обычно. Используйте -dcore-lint для проверки переписанной программы на типизацию. Если Core Lint удовлетворён, всё должно быть в порядке.

10.3.17.1. Элементы, не подверженные влиянию RebindableSyntax

RebindableSyntax не применяется к коду, сгенерированному из пункта `where` или объявления. Чтобы понять почему, рассмотрим следующий код:

{-# LANGUAGE RebindableSyntax, OverloadedStrings #-}
newtype Text = Text String

fromString :: String -> Text
fromString = Text

data Foo = Foo deriving Show

Это сгенерирует код примерно такого эффекта:

instance Show Foo where
  showsPrec _ Foo = showString "Foo"

Но поскольку RebindableSyntax и OverloadedStrings включены, строковая литерал "Foo" теперь будет иметь тип Text, а не String, который showString не принимает! Это приводит к ошибке типизации созданного экземпляра Show. Трудно представить себе любой сценарий, в котором желательно было бы иметь поведение RebindableSyntax внутри производного кода, поэтому GHC просто игнорирует RebindableSyntax при проверке производного кода.

10.3.18. Постфиксные операторы

PostfixOperators
Since

7.10.1

Разрешить использование постфиксных операторов

Расширение PostfixOperators позволяет расширить синтаксис левых секций оператора, что позволяет определить постфиксные операторы. Расширение заключается в том, что левая секция

(e !)

эквивалентна (с точки зрения проверки типов и выполнения) выражению

((!) e)

(для любого выражения e и оператора (!). Строгое толкование Haskell 98 состоит в том, что секция эквивалентна

(\y -> (!) e y)

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

Расширение не распространяется на левую часть определений функций; вы должны определять такие функции в префиксной форме.

10.3.19. Секции кортежей

TupleSections
Since

6.12

Разрешить использование синтаксиса секций кортежей

Расширение TupleSections позволяет использовать частично применённые конструкторы кортежей. Например, следующая программа

(, True)

рассматривается как альтернативная запись более громоздкой альтернативы

\x -> (x, True)

Вы можете опустить любую комбинацию аргументов кортежа, как в следующем примере

(, "I", , , "Love", , 1337)

что переводится в

\a b c d -> (a, "I", b, c, "Love", d, 1337)

Если у вас включены неупакованные кортежи, секции кортежей также будут доступны для них, например так

(# , True #)

Поскольку неупакованного кортежа единицы не существует, следующее выражение

(# #)

по-прежнему обозначает неупакованный конструктор кортежа-одиночки.

10.3.20. Lambda-case

LambdaCase
Since

7.6.1

Разрешить использование синтаксиса lambda-case.

Расширение LambdaCase позволяет использовать выражения вида

\case { p1 -> e1; ...; pN -> eN }

что эквивалентно

\freshName -> case freshName of { p1 -> e1; ...; pN -> eN }

Обратите внимание, что \case запускает верстку, поэтому вы можете написать

\case
  p1 -> e1
  ...
  pN -> eN

10.3.21. Пустые варианты case

EmptyCase
Since

7.8.1

Разрешить пустые выражения case.

Расширение EmptyCase позволяет использовать выражения case, или lambda-case, не имеющие альтернатив, например:

case e of { }   -- No alternatives

или

\case { }       -- -XLambdaCase is also required

Это может быть полезно, когда известно, что анализируемое выражение не имеет значений, отличных от bottom. Например:

data Void
f :: Void -> Int
f x = case x of { }

С зависимо-типизированными особенностями это более полезно (см. #2431). Например, рассмотрим два кандидатных определения absurd:

data a :~: b where
  Refl :: a :~: a

absurd :: True :~: False -> a
absurd x = error "absurd"    -- (A)
absurd x = case x of {}      -- (B)

Мы предпочитаем (B). Почему? Потому что GHC может выяснить, что (True :~: False) — это пустой тип. Так (B) не имеет частичности, и GHC может скомпилировать с -Wincomplete-patterns и -Werror. С другой стороны, (A) выглядит опасно, и GHC не проверяет, чтобы убедиться, что функция на самом деле никогда не может быть вызвана.

10.3.22. Многосторонние выражения if

MultiWayIf
Since

7.6.1

Разрешить использование синтаксиса multi-way-if.

С расширением MultiWayIf GHC принимает условные выражения с несколькими ветвями:

if | guard1 -> expr1
   | ...
   | guardN -> exprN

что примерно эквивалентно

case () of
  _ | guard1 -> expr1
  ...
  _ | guardN -> exprN

Многосторонние выражения if вводят новый контекст верстки. Таким образом, приведенный выше пример эквивалентен:

if { | guard1 -> expr1
   ; | ...
   ; | guardN -> exprN
   }

Следующее поведение предсказуемо:

if | guard1 -> if | guard2 -> expr2
                  | guard3 -> expr3
   | guard4 -> expr4

потому что верстка интерпретирует его как

if { | guard1 -> if { | guard2 -> expr2
                    ; | guard3 -> expr3
                    }
   ; | guard4 -> expr4
   }

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

10.3.23. Локальные объявления фиксностей

Внимательное прочтение отчета Haskell 98 показывает, что объявления фиксностей (infix, infixl, и infixr) разрешено размещать внутри локальных связей, таких как те, которые вводятся let и where. Однако, отчет Haskell не определяет семантику таких связей очень точно.

В GHC объявление фиксностей может сопровождать локальную связь:

let f = ...
    infixr 3 `f`
in
    ...

и объявление фиксностей применяется везде, где связь находится в области видимости. Например, в let, оно применяется в правых частях других let-связей и в теле letC. Или в рекурсивных do выражениях (Рекурсивное обозначение do), локальные объявления фиксностей оператора let охватывают другие операторы в группе, точно так же, как и связанное имя.

Кроме того, локальное объявление фиксностей должно сопровождать локальную связь этого имени: нельзя пересматривать фиксность имени, связанного в другом месте, как в

let infixr 9 $ in ...

Поскольку локальные объявления фиксностей технически являются частью Haskell 98, для их включения не требуется никакого расширения.

10.3.24. Расширения импорта и экспорта

10.3.24.1. Скрытие того, что импортированный модуль не экспортирует

Технически в Haskell 2010 это незаконно:

module A( f ) where
  f = True

module B where
  import A hiding( g )  -- A does not export g
  g = f

import A hiding( g ) в модуле B технически является ошибкой (Отчет Haskell, 5.3.1), потому что A не экспортирует g. Однако GHC позволяет это сделать, чтобы поддерживать обратную совместимость; например, более новая версия A может экспортировать g, и вы хотите, чтобы B работало в любом случае.

Предупреждение -Wdodgy-imports, которое по умолчанию выключено, но включается с -W, предупреждает, если вы скрываете то, что импортированный модуль не экспортирует.

10.3.24.2. Импорты с квалификацией пакета

PackageImports
Since

6.10.1

Разрешить использование синтаксиса импорта с квалификацией пакета import.

С расширением PackageImports GHC позволяет объявлениям импорта квалифицироваться именем пакета, из которого должен быть импортирован модуль. Например:

import "network" Network.Socket

импортирует модуль Network.Socket из пакета network (любой версии). Это может использоваться для устранения неоднозначности при импорте, когда один и тот же модуль доступен из нескольких пакетов или присутствует как в текущем пакете, который строится, так и в внешнем пакете.

Специальное имя пакета this может использоваться для ссылки на текущий пакет, который строится.

Примечание

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

10.3.24.3. Безопасные импорты

Safe
Since

7.2.1

Объявить состояние Safe Haskell текущего модуля.

Trustworthy
Since

7.2.1

Объявить состояние Safe Haskell текущего модуля.

Unsafe
Since

7.4.1

Объявить состояние Safe Haskell текущего модуля.

С флагами языка Safe, Trustworthy и Unsafe GHC расширяет синтаксис объявления импорта, чтобы принять необязательное ключевое слово safe после ключевого слова import. Эта функция является частью расширения Safe Haskell GHC. Например:

import safe qualified Network.Socket as NS

было бы импортировать модуль Network.Socket с успешной компиляцией только в том случае, если Network.Socket можно безопасно импортировать. Описание того, когда импорт считается безопасным, см. в Safe Haskell.

10.3.24.4. Явные пространства имён в import/export

ExplicitNamespaces
С тех пор

7.6.1

Включить использование явных пространств имён в списках экспорта модулей.

В списке импорта или экспорта, например:

module M( f, (++) ) where ...
  import N( f, (++) )
  ...

сущности f и (++) являются значениями. Однако с операторами типов (Операторы типов) появляется возможность объявить (++) как конструктор типа. В этом случае, как его экспортировать или импортировать?

Расширение ExplicitNamespaces позволяет вам префиксровать имя конструктора типа в списке импорта или экспорта с «type», чтобы избежать неоднозначности в этом случае, таким образом:

module M( f, type (++) ) where ...
  import N( f, type (++) )
  ...
module N( f, type (++) ) where
  data family a ++ b = L a | R b

Расширение ExplicitNamespaces подразумевается TypeOperators и (по какой-то причине) TypeFamilies.

Кроме того, с PatternSynonyms вы можете префиксровать имя конструктора данных в списке импорта или экспорта ключевым словом pattern, чтобы разрешить импорт или экспорт конструктора данных без его родительского конструктора типа (см. Импорт и экспорт синонимов шаблонов).

10.3.24.5. Запись qualified в постпозитивной позиции

ImportQualifiedPost
С тех пор

8.10.1

ImportQualifiedPost позволяет синтаксис import M qualified, то есть, аннотировать модуль как qualified, написав qualified после имени модуля.

Для импорта квалифицированного модуля обычно необходимо указать qualified в препозитивной позиции : import qualified M. Это часто приводит к «висящему отступу» (который автоматически вставляется некоторыми автоформатерами и является распространённым в многих кодовых базах. Например:

import qualified A
import           B
import           C

Расширение ImportQualifiedPost позволяет qualified появиться в постпозитивной позиции : import M qualified. С включённым этим расширением, можно написать:

import A qualified
import B
import C

Это ошибка, если qualified появляется в обеих пре- и постпозитивных позициях.

Предупреждение -Wprepositive-qualified-syntax (выключено по умолчанию) сообщает обо всех случаях импортов, аннотированных qualified с использованием препозитивного синтаксиса.

10.3.25. Более либеральный синтаксис для аргументов функций

BlockArguments
С тех пор

8.6.1

Разрешить do выражения, лямбда-выражения и т. д. для непосредственного использования в качестве аргумента функции.

В Haskell 2010 определённые виды выражений могут использоваться без скобок в качестве аргумента оператора, но не в качестве аргумента функции. К ним относятся do, лямбда, if, case, и let выражения. Некоторые расширения GHC также определяют конструкции языка такого типа: mdo (Рекурсивное обозначение do), \case (Лямбда-случай) и proc (Стрелочное обозначение).

Расширение BlockArguments позволяет использовать эти конструкции непосредственно в качестве аргумента функции. Например:

when (x > 0) do
  print x
  exitFailure

будет разбор как:

when (x > 0) (do
  print x
  exitFailure)

и

withForeignPtr fptr \ptr -> c_memcpy buf ptr size

будет разбор как:

withForeignPtr fptr (\ptr -> c_memcpy buf ptr size)

10.3.25.1. Изменения в грамматике

Отчёт Haskell определяет нетерминал lexp следующим образом (* указывает интересующее правило)

lexp  →  \ apat1 … apatn -> exp            (lambda abstraction, n ≥ 1)  *
      |  let decls in exp                  (let expression)             *
      |  if exp [;] then exp [;] else exp  (conditional)                *
      |  case exp of { alts }              (case expression)            *
      |  do { stmts }                      (do expression)              *
      |  fexp

fexp  →  [fexp] aexp                       (function application)

aexp  →  qvar                              (variable)
      |  gcon                              (general constructor)
      |  literal
      |  ( exp )                           (parenthesized expression)
      |  qcon { fbind1 … fbindn }          (labeled construction)
      |  aexp { fbind1 … fbindn }          (labelled update)
      |  …

Расширение BlockArguments перемещает эти правила вывода под aexp

lexp  →  fexp

fexp  →  [fexp] aexp                       (function application)

aexp  →  qvar                              (variable)
      |  gcon                              (general constructor)
      |  literal
      |  ( exp )                           (parenthesized expression)
      |  qcon { fbind1 … fbindn }          (labeled construction)
      |  aexp { fbind1 … fbindn }          (labelled update)
      |  \ apat1 … apatn -> exp            (lambda abstraction, n ≥ 1)  *
      |  let decls in exp                  (let expression)             *
      |  if exp [;] then exp [;] else exp  (conditional)                *
      |  case exp of { alts }              (case expression)            *
      |  do { stmts }                      (do expression)              *
      |  …

Теперь нетерминал lexp избыточен и может быть исключён из грамматики.

Обратите внимание, что это изменение опирается на существующее метаправило для разрешения неоднозначностей:

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

Например, f \a -> a b будет разбор как f (\a -> a b), а не как f (\a -> a) b.

10.3.26. Сводка по украденному синтаксису

Включение опции, которая включает специальный синтаксис, может привести к тому, что работающий код Haskell 98 не будет компилироваться, возможно, потому что он использует имя переменной, ставшее зарезервированным словом. В этом разделе перечислены синтаксические конструкции, которые «украдены» расширениями языка. Мы используем обозначения и имена нетерминалов из лексического синтаксиса Haskell 98 (см. Отчёт Haskell 98). Мы перечисляем только изменения синтаксиса, которые могут повлиять на существующие работающие программы (т. е. «украденный» синтаксис). Многие из этих расширений также будут включать новый контекстно-свободный синтаксис, но во всех случаях программы, написанные для использования нового синтаксиса, не будут компилироваться без включённой опции.

Существует два класса специального синтаксиса:

  • Новые зарезервированные слова и символы: последовательности символов, которые больше недоступны для использования в качестве идентификаторов в программе.
  • Другой специальный синтаксис: последовательности символов, которые имеют другое значение при включённой этой конкретной опции.

Следующий синтаксис украден:

forall

Украден (в типах) по умолчанию (см. Лексический синтаксис). forall является зарезервированным ключевым словом и никогда не является переменной типа, в соответствии с GHC Proposal #43.

mdo

Украден расширением: RecursiveDo

foreign

Украден расширением: ForeignFunctionInterface

rec, proc, -<, >-, -<<, >>-, (|, |)

Украден расширением: Arrows

?varid

Украден расширением: ImplicitParams

[|, [e|, [p|, [d|, [t|, [||, [e||

Украден расширением: QuasiQuotes. Кроме того, это вводит неоднозначность с синтаксисом списочного понимания. См. обсуждение квазицитаций для получения подробностей.

$(, $$(, $varid, $$varid

Украден расширением: TemplateHaskell

[varid|

Украден расширением: QuasiQuotes

⟨varid⟩, #⟨char⟩, #, ⟨string⟩, #, ⟨integer⟩, #, ⟨float⟩, #, ⟨float⟩, ##

Украден расширением: MagicHash

(#, #)

Украден расширением: UnboxedTuples

⟨varid⟩, !, ⟨varid⟩

Украден расширением: BangPatterns

pattern

Украден расширением: PatternSynonyms

static

Украден расширением: StaticPointers

10.4. Расширения типов данных и синонимов типов

10.4.1. Типы данных без конструкторов

EmptyDataDecls
С тех пор

6.8.1

Разрешить определение пустых data типов.

С расширением EmptyDataDecls, GHC позволяет объявлять тип данных без конструкторов.

Вам нужно включить это расширение только если используемый язык — Haskell 98, в котором тип данных должен иметь по крайней мере один конструктор. Haskell 2010 ослабил это правило, разрешив типы данных без конструкторов, и поэтому EmptyDataDecls включено по умолчанию, когда язык Haskell 2010.

Например:

data S      -- S :: Type
data T a    -- T :: Type -> Type

Синтаксически объявление не содержит части «= constrs». Тип может быть параметризован по типам любого рода, но если род не Type , то должен использоваться явный аннотация рода (см. Явно-родовые квантификаторы).

Такие типы данных имеют только одно значение, а именно bottom. Тем не менее, они могут быть полезны при определении «фантомных типов».

В сочетании с расширением EmptyDataDeriving объявления пустых данных также могут выводить экземпляры стандартных типов классов (см. Выведение экземпляров для типов пустых данных).

10.4.2. Контексты типов данных

DatatypeContexts
С тех пор

7.0.1

Разрешить контексты для data типов.

Haskell позволяет задавать контексты для типов данных, например:

data Eq a => Set a = NilSet | ConsSet a (Set a)

предоставляют конструкторам типы:

NilSet :: Set a
ConsSet :: Eq a => a -> Set a -> Set a

Это широко считается ошибкой и будет удалено из языка. В GHC это контролируется устаревшим расширением DatatypeContexts.

10.4.3. Инфиксные конструкторы типов, классы и переменные типов

GHC позволяет конструкторам типов, классам и переменным типов быть операторами и записываться инфиксно, очень похоже на выражения. Более конкретно:

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

    data a :*: b = Foo a b
    type a :+: b = Either a b
    class a :=: b where ...
    
    data (a :**: b) x = Baz a b x
    type (a :++: b) y = Either (a,b) y
    
  • Типы и ограничения классов могут быть записаны инфиксно. Например:

    x :: Int :*: Bool
    f :: (a :=: b) => a -> b
    
  • Обратные кавычки работают так же, как и для выражений, как для конструкторов типов, так и для переменных типов; например, Int `Either` Bool, или Int `a` Bool. Аналогично, скобки работают одинаково; например, (:*:) Int Bool.
  • Фиксирующие правила могут быть объявлены для конструкторов типов или классов так же, как и для конструкторов данных. Однако нельзя различить их в объявлении фиксированности; объявление фиксированности устанавливает фиксированность для конструктора данных и соответствующего конструктора типа. Например:

    infixl 7 T, :*:
    

    устанавливает фиксированность как для конструктора типа T , так и для конструктора данных T, и аналогично для :*:. Int `a` Bool.

  • Стрелка функции -> является infixr с фиксированностью -1.

10.4.4. Операторы типов

TypeOperators
Подразумевает

ExplicitNamespaces

С тех пор

6.8.1

Разрешить использование и определение типов с именами операторов.

В типах символ оператора, например, (+) обычно обрабатывается как переменная типа, так же как и a. Таким образом, в Haskell 98 можно сказать

type T (+) = ((+), (+))
-- Just like: type T a = (a,a)

f :: T Int -> Int
f (x,y)= x

Как вы можете видеть, использование операторов таким образом не очень полезно, и Haskell 98 даже не позволяет вам записывать их инфиксным способом.

Язык TypeOperators изменяет это поведение:

  • Символы операторов становятся конструкторами типов, а не переменными типов.
  • Символы операторов в типах могут быть написаны инфиксным способом, как в определениях, так и в использованиях. Например:

    data a + b = Plus a b
    type Foo = Int + Bool
    
  • Теперь существует некоторая потенциальная неоднозначность в списках импорта и экспорта; например, если вы напишете import M( (+) ) , имеете ли вы в виду функцию (+) или конструктор типа (+)? По умолчанию это первое, но с ExplicitNamespaces (что подразумевается TypeOperators) GHC позволяет вам указать последнее, добавив ключевое слово type, таким образом:

    import M( type (+) )
    

    См. Явные пространства имен в импорте/экспорте.

  • Фиксированность оператора типа может быть задана с помощью обычных объявлений фиксированности, но, как и в Инфиксные конструкторы типов, классы и переменные типов, функция и конструктор типа делят одну фиксированность.

10.4.5. Либерализованные синонимы типов

LiberalTypeSynonyms
Подразумевает

ExplicitForAll

С тех пор

6.8.1

Смягчение многих правил Haskell 98 для определений синонимов типов.

Синонимы типов похожи на макросы на уровне типов, но Haskell 98 накладывает много правил на отдельные объявления синонимов. С расширением LiberalTypeSynonyms GHC выполняет проверку типов только после расширения синонимов типов. Это означает, что GHC может быть гораздо более либеральным в отношении синонимов типов, чем Haskell 98.

  • Вы можете написать forall (включая перегрузку) в синониме типа, таким образом:

    type Discard a = forall b. Show b => a -> b -> (a, String)
    
    f :: Discard a
    f x y = (x, show y)
    
    g :: Discard Int -> (Int,String)    -- A rank-2 type
    g f = f 3 True
    
  • Если вы также используете UnboxedTuples, вы можете записать неразложенную кортеж в синониме типа:

    type Pr = (# Int, Int #)
    
    h :: Int -> Pr
    h x = (# x, x #)
    
  • Вы можете применить синоним типа к типу с квантором forall:

    type Foo a = a -> a -> Bool
    
    f :: Foo (forall b. b->b)
    

    После расширения синонима f имеет легальный (в GHC) тип:

    f :: (forall b. b->b) -> (forall b. b->b) -> Bool
    
  • Вы можете применить синоним типа к частично применённому синониму типа:

    type Generic i o = forall x. i x -> o x
    type Id x = x
    
    foo :: Generic Id []
    

    После расширения синонима foo имеет легальный (в GHC) тип:

    foo :: forall x. x -> [x]
    

GHC в настоящее время выполняет проверку типа перед расширением синонимов (хотя даже это может быть изменено).

После расширения синонимов типов GHC проверяет типы на соответствие следующим дефектам, которые не обнаруживаются только проверкой типа:

  • Конструктор типа применяется к типу, включающему for-all (если ImpredicativeTypes выключен)
  • Частично применённый синоним типа.

Например, это будет отклонено:

type Pr = forall a. a

h :: [Pr]
h = ...

потому что GHC не допускает применения конструкторов типов к типам с квантором for-all.

10.4.6. Существовательно квантованные конструкторы данных

ExistentialQuantification
Подразумевает

ExplicitForAll

С тех пор

6.8.1

Разрешить существовательно квантованные переменные типов в типах.

Идея использования существовательной квантификации в объявлениях типов данных была предложена Перри и реализована в Hope+ (Найджел Перри, *Реализация практических функциональных языков программирования*, диссертация на соискание степени доктора философии, Лондонский университет, 1991). Позже она была формализована Лофером и Одерски ( *Полиморфный вывод типов и абстрактные типы данных*, TOPLAS, 16(5), с. 1411-1430, 1994). Она уже несколько лет присутствует в компиляторе Haskell Леннарта Аугуссона hbc и оказалась очень полезной. Вот идея. Рассмотрим объявление:

data Foo = forall a. MkFoo a (a -> Bool)
         | Nil

Тип данных Foo имеет два конструктора с типами:

MkFoo :: forall a. a -> (a -> Bool) -> Foo
Nil   :: Foo

Обратите внимание, что переменная типа a в типе MkFoo не появляется в самом типе данных, который является простым Foo. Например, следующее выражение является корректным:

[MkFoo 3 even, MkFoo 'c' isUpper] :: [Foo]

Здесь (MkFoo 3 even) упаковывает целое число с функцией even, отображающей целое число в Bool; а MkFoo 'c' isUpper упаковывает символ с совместимой функцией. Эти две вещи каждый имеют тип Foo и могут быть помещены в список.

Что мы можем сделать со значением типа Foo? В частности, что происходит при сопоставлении с образцом на MkFoo?

f (MkFoo val fn) = ???

Поскольку всё, что мы знаем о val и fn , это то, что они совместимы, единственное (полезное) действие, которое мы можем с ними совершить, — это применить fn к val для получения булевого значения. Например:

f :: Foo -> Bool
f (MkFoo val fn) = fn val

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

10.4.6.1. Почему существовательная?

Что это имеет общего с существовательной квантификацией? Просто то, что MkFoo имеет (почти) изоморфный тип

MkFoo :: (exists a . (a, a -> Bool)) -> Foo

Но программисты Haskell могут безопасно мыслить в терминах обычного универсального квантифицированного типа, который представлен выше, тем самым избегая добавления нового конструктора существовательной квантификации.

10.4.6.2. Существование и типы классов

Простое расширение заключается в разрешении произвольных контекстов перед конструктором. Например:

data Baz = forall a. Eq a => Baz1 a a
         | forall b. Show b => Baz2 b (b -> b)

У двух конструкторов есть ожидаемые типы:

Baz1 :: forall a. Eq a => a -> a -> Baz
Baz2 :: forall b. Show b => b -> (b -> b) -> Baz

Но при сопоставлении с образцом на Baz1 сопоставленные значения могут быть сравнены на равенство, а при сопоставлении с образцом на Baz2 первое сопоставленное значение может быть преобразовано в строку (а также применена функция к нему). Таким образом, эта программа является корректной:

f :: Baz -> String
f (Baz1 p q) | p == q    = "Yes"
             | otherwise = "No"
f (Baz2 v fn)            = show (fn v)

В реализации с передачей словаря конструкторы Baz1 и Baz2 должны хранить словари для Eq и Show соответственно и извлекать их при сопоставлении с образцом.

10.4.6.3. Конструкторы записей

GHC позволяет использовать экзистенциальные типы с синтаксисом записей. Например:

data Counter a = forall self. NewCounter
    { _this    :: self
    , _inc     :: self -> self
    , _display :: self -> IO ()
    , tag      :: a
    }

Здесь tag — это общедоступное поле со хорошо типизированной функцией-селектором tag :: Counter a -> a. Тип self скрыт снаружи; любая попытка применить _this, _inc или _display как функции вызовет ошибку во время компиляции. Другими словами, GHC определяет функцию-селектор записи только для полей, тип которых не упоминает экзистенциально квантифицированные переменные. (В этом примере в полях, для которых не будут определены селекторы записей, использовалась нижняя черта, но это всего лишь стиль программирования; GHC их игнорирует.)

Чтобы использовать эти скрытые поля, нам нужно создать некоторые вспомогательные функции:

inc :: Counter a -> Counter a
inc (NewCounter x i d t) = NewCounter
    { _this = i x, _inc = i, _display = d, tag = t }

display :: Counter a -> IO ()
display NewCounter{ _this = x, _display = d } = d x

Теперь мы можем определять счётчики с различными реализациями:

counterA :: Counter String
counterA = NewCounter
    { _this = 0, _inc = (1+), _display = print, tag = "A" }

counterB :: Counter String
counterB = NewCounter
    { _this = "", _inc = ('#':), _display = putStrLn, tag = "B" }

main = do
    display (inc counterA)         -- prints "1"
    display (inc (inc counterB))   -- prints "##"

Синтаксис обновления записей поддерживается для экзистенциальных типов (и GADTs):

setTag :: Counter a -> a -> Counter a
setTag obj t = obj{ tag = t }

Правило для обновления записей таково:

типы обновляемых полей могут упоминать только универсально квантифицированные переменные типа конструктора данных. Для GADTs поле может упоминать только типы, которые появляются как простые аргументы переменных типа в типе результата конструктора.

Например:

data T a b where { T1 { f1::a, f2::b, f3::(b,c) } :: T a b } -- c is existential
upd1 t x = t { f1=x }   -- OK:   upd1 :: T a b -> a' -> T a' b
upd2 t x = t { f3=x }   -- BAD   (f3's type mentions c, which is
                        --        existentially quantified)

data G a b where { G1 { g1::a, g2::c } :: G a [c] }
upd3 g x = g { g1=x }   -- OK:   upd3 :: G a b -> c -> G c b
upd4 g x = g { g2=x }   -- BAD (f2's type mentions c, which is not a simple
                        --      type-variable argument in G1's result type)

10.4.6.4. Ограничения

Существуют определенные ограничения на способы использования конструкторов с экзистенциально квантифицированными типами.

  • При сопоставлении с образцом каждый сопоставительный образец вводит новый, отдельный тип для каждой экзистенциальной переменной типа. Эти типы не могут быть унифицированы ни с каким другим типом, и они не могут выйти за пределы области действия сопоставления с образцом. Например, эти фрагменты некорректны:

    f1 (MkFoo a f) = a
    

    Здесь тип, ограниченный MkFoo «уходит», потому что a — результат f1. Один из способов понять, почему это неправильно, — спросить, какой тип имеет f1:

    f1 :: Foo -> a             -- Weird!
    

    Что представляет собой «a» в типе результата? Очевидно, мы не имеем в виду это:

    f1 :: forall a. Foo -> a   -- Wrong!
    

    Исходная программа просто неправильна. Вот ещё один вид ошибки:

    f2 (Baz1 a b) (Baz1 p q) = a==q
    

    В порядке указать a==b или p==q, но a==q — неправильно, потому что оно уравнивает два разных типа, возникающих из двух Baz1 конструкторов.

  • Вы не можете сопоставить с образцом экзистенциально квантифицированный конструктор в группе связываний let или where. Поэтому это незаконно:

    f3 x = a==b where { Baz1 a b = x }
    

    Вместо этого используйте выражение case:

    f3 x = case x of Baz1 a b -> a==b
    

    В общем случае вы можете сопоставить с образцом экзистенциально квантифицированный конструктор только в выражении case или в шаблонах определения функции. Причина этого ограничения — чисто имплементационная. Проверка типов групп связываний и без экзистенциальных типов уже представляет собой проблему, а экзистенциальные типы ещё больше усложняют задачу. Также экзистенциальная привязка шаблонов на верхнем уровне модуля не имеет смысла, так как не ясно, как предотвратить «утечку» экзистенциально квантифицированного типа. Поэтому на данный момент существует простое ограничение. Посмотрим, насколько это будет неудобно.

  • Вы не можете использовать экзистенциальную квантификацию для объявлений newtype. Следовательно, это незаконно:

    newtype T = forall a. Ord a => MkT a
    

    Причина: значение типа T должно быть представлено как пара словаря для Ord t и значения типа t Это противоречит идее о том, что у newtype нет конкретного представления. Можно получить такую же эффективность и эффект, используя data вместо newtype Если нет перегрузки, тогда есть больше оснований для разрешения экзистенциально квантифицированного newtype, поскольку версия data несёт затраты на реализацию, но экзистенциально квантифицированные конструкторы с одним полем не очень полезны. Поэтому простое ограничение (без экзистенциальных типов для newtype) остаётся, если нет убедительных причин его изменить.

  • Вы не можете использовать deriving для определения экземпляров типа данных с экзистенциально квантифицированными конструкторами данных. Причина: в большинстве случаев это не имеет смысла. Например:

    data T = forall a. MkT [a] deriving( Eq )
    

    Чтобы вывести Eq стандартным способом, нам нужно иметь равенство между единственным компонентом двух MkT конструкторов:

    instance Eq T where
      (MkT a) == (MkT b) = ???
    

    Но a и b имеют разные типы и, следовательно, не могут быть сравнены. Можно представить примеры, в которых полученный экземпляр имеет смысл, но кажется проще просто запретить такие объявления. Определяйте свои собственные экземпляры!

10.4.7. Объявление типов данных с явными подписями конструкторов

GADTSyntax
С тех пор

7.2.1

Разрешить использование синтаксиса GADTs в определениях типов данных (но не самих GADTs; об этом см. GADTs)

Когда включено расширение GADTSyntax, GHC позволяет объявлять алгебраический тип данных, явно указывая типы подписей конструкторов. Например:

data Maybe a where
    Nothing :: Maybe a
    Just    :: a -> Maybe a

Форма называется «объявлением в стиле GADTs», потому что обобщённые алгебраические типы данных, описанные в Обобщённые алгебраические типы данных (GADTs), могут быть объявлены только с использованием этой формы.

Обратите внимание, что синтаксис в стиле GADTs обобщает экзистенциальные типы (Экзистенциально квантифицированные конструкторы данных). Например, эти два объявления эквивалентны:

data Foo = forall a. MkFoo a (a -> Bool)
data Foo' where { MKFoo :: a -> (a->Bool) -> Foo' }

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

data Set a where
  MkSet :: Eq a => [a] -> Set a

makeSet :: Eq a => [a] -> Set a
makeSet xs = MkSet (nub xs)

insert :: a -> Set a -> Set a
insert a (MkSet as) | a `elem` as = MkSet as
                    | otherwise   = MkSet (a:as)

Использование MkSet в качестве конструктора (например, в определении makeSet) порождает ограничение (Eq a), как ожидалось. Новая возможность заключается в том, что сопоставление с образцом по MkSet (как в определении insert) делает доступным контекст (Eq a). С точки зрения реализации, конструктор MkSet имеет скрытое поле, которое хранит словарь (Eq a) , который передаётся в MkSet; поэтому при сопоставлении с образцом этот словарь становится доступным для правой части сопоставления. В примере словарь равенства используется для удовлетворения ограничения равенства, сгенерированного вызовом elem, так что сам тип insert не имеет ограничения Eq.

Например, одним из возможных применений является реификация словарей:

data NumInst a where
  MkNumInst :: Num a => NumInst a

intInst :: NumInst Int
intInst = MkNumInst

plus :: NumInst a -> a -> a -> a
plus MkNumInst p q = p + q

Здесь значение типа NumInst a эквивалентно явному словарю (Num a).

Всё это применимо к конструкторам, объявленным с использованием синтаксиса Экзистенциальные типы и классы типов. Например, тип данных NumInst выше можно эквивалентно объявить так:

data NumInst a
   = Num a => MkNumInst (NumInst a)

Обратите внимание, что, в отличие от ситуации при объявлении экзистенциального типа, здесь нет forall, потому что Num ограничивает универсальную переменную типа данных a Конструктор может иметь как универсальные, так и экзистенциальные переменные типа: например, следующие два объявления эквивалентны:

data T1 a
 = forall b. (Num a, Eq b) => MkT1 a b
data T2 a where
 MkT2 :: (Num a, Eq b) => a -> b -> T2 a

Всё это поведение контрастирует со специфической обработкой контекстов в объявлении типа данных в Haskell 98 (раздел 4.2.1 отчёта Haskell 98). В Haskell 98 определение

data Eq a => Set' a = MkSet' [a]

присваивает MkSet' тот же тип, что и MkSet выше. Но вместо предоставления ограничения (Eq a), сопоставление с образцом по MkSet' требует ограничения (Eq a)! GHC точно реализует это поведение, каким бы странным оно ни было. Однако для объявлений в стиле GADTs поведение GHC гораздо более полезно и интуитивно понятно.

Остальная часть этого раздела предоставляет дополнительные подробности об объявлениях типов данных в стиле GADTs.

  • Тип результата каждого конструктора данных должен начинаться с типа конструктора, который определяется. Если тип результата всех конструкторов имеет вид T a1 ... an, где a1 ... an — это различные переменные типа, то тип данных является обычным; в противном случае это обобщённый тип данных (Обобщённые алгебраические типы данных (GADTs)).
  • Как и с другими сигнатурами типов, вы можете указать одну сигнатуру для нескольких конструкторов данных. В этом примере мы даём одну сигнатуру для T1 и T2:

    data T a where
      T1,T2 :: a -> T a
      T3 :: T a
    
  • Сигнатура типа каждого конструктора независима и неявно универсально квантифицируется, как обычно. В частности, переменные типа в заголовке «data T a where» не имеют области действия, и разные конструкторы могут иметь разные универсально квантифицированные переменные типа:

    data T a where        -- The 'a' has no scope
      T1,T2 :: b -> T b   -- Means forall b. b -> T b
      T3 :: T a           -- Means forall a. T a
    
  • Сигнатура конструктора может упоминать ограничения класса типов, которые могут различаться для разных конструкторов. Например, это допустимо:

    data T a where
      T1 :: Eq b => b -> b -> T b
      T2 :: (Show c, Ix c) => c -> [c] -> T c
    

    При сопоставлении с образцом эти ограничения становятся доступными для разрешения ограничений в теле сопоставления. Например:

    f :: T a -> String
    f (T1 x y) | x==y      = "yes"
               | otherwise = "no"
    f (T2 a b)             = show a
    

    Обратите внимание, что f не перегружен; ограничение Eq, возникающее из использования ==, разрешается при сопоставлении с образцом T1, и аналогично ограничение Show от использования show.

  • В отличие от объявления типа в стиле Haskell-98, переменные типа в заголовке «data Set a where» не имеют области действия. Действительно, можно написать сигнатуру вида:

    data Set :: Type -> Type where ...
    

    или даже смесь двух вариантов:

    data Bar a :: (Type -> Type) -> Type where ...
    

    Переменные типа (если заданы) могут быть явно охарактеризованы, поэтому мы также можем записать заголовок для Foo так:

    data Bar a (b :: Type -> Type) where ...
    
  • Вы можете использовать аннотации строгости в очевидных местах в типе конструктора:

    data Term a where
        Lit    :: !Int -> Term Int
        If     :: Term Bool -> !(Term a) -> !(Term a) -> Term a
        Pair   :: Term a -> Term b -> Term (a,b)
    
  • Вы можете использовать условное deriving в объявлении типа данных в стиле GADT. Например, эти два объявления эквивалентны:

    data Maybe1 a where {
        Nothing1 :: Maybe1 a ;
        Just1    :: a -> Maybe1 a
      } deriving( Eq, Ord )
    
    data Maybe2 a = Nothing2 | Just2 a
         deriving( Eq, Ord )
    
  • Сигнатура типа может иметь квантифицированные переменные типа, которые не появляются в типе результата:

    data Foo where
       MkFoo :: a -> (a->Bool) -> Foo
       Nil   :: Foo
    

    Здесь переменная типа a не появляется в типе результата ни одного конструктора. Хотя она универсально квантифицируется в типе конструктора, такая переменная типа часто называется «экзистенциальной». Действительно, вышеприведённое объявление описывает ровно тот же тип, что и data Foo в Экзистенциально квантифицированные конструкторы данных.

    Тип, конечно, также может содержать контекст класса:

    data Showable where
      MkShowable :: Show a => a -> Showable
    
  • Вы можете использовать синтаксис записи в объявлении типа данных в стиле GADT:

    data Person where
        Adult :: { name :: String, children :: [Person] } -> Person
        Child :: Show a => { name :: !String, funny :: a } -> Person
    

    Как обычно, для каждого конструктора, имеющего поле f, тип поля f должен быть одинаковым (с учётом альфа-преобразования). Конструктор Child выше показывает, что сигнатура может иметь контекст, экзистенциально квантифицированные переменные и аннотации строгости, как и в случае без записи. (Примечание: «тип», который следует за двоеточием, на самом деле не является типом из-за синтаксиса записи и аннотаций строгости. «Тип» такого вида может появляться только в сигнатуре конструктора.)

  • Обновления записей разрешены с объявлениями в стиле GADT, только для полей, которые обладают следующим свойством: тип поля не упоминает никаких экзистенциальных переменных типа.
  • Как и в случае с экзистенциалами, объявленными с использованием синтаксиса записи в стиле Haskell-98 (Конструкторы записи), функции-селекторы записи генерируются только для тех полей, которые имеют правильно типизированные селекторы. Вот пример из того раздела, в синтаксисе GADT:

    data Counter a where
        NewCounter :: { _this    :: self
                      , _inc     :: self -> self
                      , _display :: self -> IO ()
                      , tag      :: a
                      } -> Counter a
    

    Как и прежде, здесь генерируется только одна функция-селектор — для tag. Тем не менее, вы по-прежнему можете использовать все имена полей в сопоставлении с образцами и построении записей.

  • В объявлении типа данных в стиле GADT нет очевидного способа указать, что конструктор данных должен быть инфиксным, что влияет, если вы выводите Show для типа. (Конструкторы данных, объявленные инфиксными, отображаются инфиксным способом выведенной show. Так GHC реализует следующий дизайн: конструктор данных, объявленный в объявлении типа данных в стиле GADT, отображается инфиксным способом в Show тогда и только тогда, когда (а) он является символом оператора, (б) он имеет два аргумента, (в) он имеет объявление фиктивности, заданное программистом. Например

    infix 6 (:--:)
    data T a where
      (:--:) :: Int -> Bool -> T Int
    

10.4.8. Обобщённые алгебраические типы данных (GADTs)

GADTs
Подразумевает

MonoLocalBinds, GADTSyntax

С

6.8.1

Разрешает использование обобщённых алгебраических типов данных (GADTs).

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

data Term a where
    Lit    :: Int -> Term Int
    Succ   :: Term Int -> Term Int
    IsZero :: Term Int -> Term Bool
    If     :: Term Bool -> Term a -> Term a -> Term a
    Pair   :: Term a -> Term b -> Term (a,b)

Заметьте, что тип возврата конструкторов не всегда Term a, как в случае с обычными типами данных. Эта общность позволяет нам написать правильно типизированную eval функцию для этих Terms:

eval :: Term a -> a
eval (Lit i)      = i
eval (Succ t)     = 1 + eval t
eval (IsZero t)   = eval t == 0
eval (If b e1 e2) = if eval b then eval e1 else eval e2
eval (Pair e1 e2) = (eval e1, eval e2)

Ключевой момент в отношении GADTs заключается в том, что сопоставление с образцом приводит к уточнению типа. Например, в правой части уравнения

eval :: Term a -> a
eval (Lit i) =  ...

тип a уточняется до Int. В этом и суть! Точное описание правил типов выходит за рамки того, на что претендует это руководство пользователя, но дизайн тесно следует тому, что описано в статье Simple unification-based type inference for GADTs (ICFP 2006). Общий принцип таков: уточнение типа выполняется только на основе предоставленных пользователем аннотаций типов. Таким образом, если для eval не указана сигнатура типа, уточнение типа не происходит, и появятся много неясных сообщений об ошибках. Однако уточнение довольно общее. Например, если бы у нас было:

eval :: Term a -> a -> a
eval (Lit i) j =  i+j

сопоставление с образцом приводит к уточнению типа a до Int (из-за типа конструктора Lit), и это уточнение также применяется к типу j, и типу результата выражения case. Следовательно, дополнение i+j допустимо.

Эти и многие другие примеры приведены в статьях Хонгвей Си и Тимом Ширдом. Более подробное введение доступно на вики, а в статье Ральфа Хинца Fun with phantom types также есть несколько примеров. Обратите внимание, что статьи могут использовать различную нотацию по сравнению с той, что реализована в GHC.

В остальной части этого раздела описаны расширения GHC, поддерживающие GADTs. Расширение включено с помощью GADTs. Расширение GADTs также устанавливает GADTSyntax и MonoLocalBinds.

  • GADT может быть объявлен только с использованием синтаксиса GADT (Объявление типов данных со явными сигнатурами конструкторов); старый синтаксис Haskell 98 для объявлений типов всегда объявляет обычный тип данных. Тип результата каждого конструктора должен начинаться с определяемого конструктора типа, но для GADT аргументы конструктора типа могут быть произвольными монотипами. Например, в типе данных Term выше тип каждого конструктора должен заканчиваться Term ty, но ty не обязательно должна быть переменной типа (например, конструктор Lit).
  • Разрешено объявлять обычный алгебраический тип данных с использованием синтаксиса GADT. То, что делает GADT GADT, — это не синтаксис, а наличие конструкторов данных, чьи типы результата не только T a b.
  • Вы не можете использовать условное deriving для GADT; только для обычного типа данных.
  • Как упоминалось в Объявление типов данных со явными сигнатурами конструкторов, поддерживается синтаксис записи. Например:

    data Term a where
        Lit    :: { val  :: Int }      -> Term Int
        Succ   :: { num  :: Term Int } -> Term Int
        Pred   :: { num  :: Term Int } -> Term Int
        IsZero :: { arg  :: Term Int } -> Term Bool
        Pair   :: { arg1 :: Term a
                  , arg2 :: Term b
                  }                    -> Term (a,b)
        If     :: { cnd  :: Term Bool
                  , tru  :: Term a
                  , fls  :: Term a
                  }                    -> Term a
    

    Однако для GADTs существует дополнительное ограничение: каждый конструктор, имеющий поле f , должен иметь одинаковый тип результата (с учётом альфа-преобразования). Следовательно, в приведённом выше примере мы не можем объединить поля num и arg под одним именем. Хотя их типы полей оба Term Int, их функции-селекторы на самом деле имеют разные типы:

    num :: Term Int -> Term Int
    arg :: Term Bool -> Term Int
    
  • При сопоставлении с образцом конструкторов данных, взятых из GADT, например, в выражении case , применяются следующие правила:

    • Тип операнда должен быть жёстким.
    • Тип всего выражения case должен быть жёстким.
    • Тип любой свободной переменной, упомянутой в любом из case альтернатив, должен быть жёстким.

    Тип является «жёстким», если он полностью известен компилятору в месте его объявления. Самый простой способ гарантировать, что переменная имеет жёсткий тип, — это предоставить ей сигнатуру типа. Более подробные сведения см. в Simple unification-based type inference for GADTs. Критерии, реализованные в GHC, приведены в Приложении.

10.5. Расширения системы записей

10.5.1. Традиционный синтаксис записей

NoTraditionalRecordSyntax
С

7.4.1

Запрещает использование синтаксиса записи.

Традиционный синтаксис записей, такой как C {f = x}, включён по умолчанию. Чтобы отключить его, можно использовать расширение NoTraditionalRecordSyntax.

10.5.2. Разрешение неоднозначности полей записей

DisambiguateRecordFields
С

6.8.1

Разрешить компилятору автоматически выбирать между именами селекторов записей, идентичными по названию, но различающимися по типу (если выбор однозначен).

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

module M where
  data S = MkS { x :: Int, y :: Bool }

module Foo where
  import M

  data T = MkT { x :: Int }

  ok1 (MkS { x = n }) = n+1   -- Unambiguous
  ok2 n = MkT { x = n+1 }     -- Unambiguous

  bad1 k = k { x = 3 }        -- Ambiguous
  bad2 k = x k                -- Ambiguous

Несмотря на то, что в области видимости находятся две x’ы, очевидно, что x в шаблоне в определении ok1 может означать только поле x типа S. Аналогично для функции ok2. Однако в обновлении записи в bad1 и выборе записи в bad2 неясно, какой из двух типов подразумевается.

Haskell 98 считает все четыре случая неоднозначными, но с расширением DisambiguateRecordFields GHC будет принимать первые два. Правила полностью совпадают с правилами для объявлений экземпляров в Haskell 98, где имена методов в левой части связываний методов в объявлении экземпляра однозначно ссылаются на метод этого класса (при условии, что они вообще находятся в области видимости), даже если существуют другие переменные с тем же именем. Это уменьшает количество квалифицированных имён, когда вы импортируете две записи из разных модулей, использующих одно и то же имя поля.

Некоторые детали:

  • Разрешение неоднозначности полей можно сочетать с омонимией (см. Омонимия записей). Например:

    module Foo where
      import M
      x=True
      ok3 (MkS { x }) = x+1   -- Uses both disambiguation and punning
    
  • С расширением DisambiguateRecordFields можно использовать неквалифицированные имена полей, даже если соответствующий селектор находится в области видимости только в квалифицированной форме. Например, предположим, что используется тот же модуль M , что и в нашем предыдущем примере; это допустимо:

    module Foo where
      import qualified M    -- Note qualified
    
      ok4 (M.MkS { x = n }) = n+1   -- Unambiguous
    

    Поскольку конструктор MkS находится в области видимости только в квалифицированной форме, его нужно назвать M.MkS, но поле x не нужно квалифицировать, даже если M.x находится в области видимости, а x нет (фактически, оно квалифицируется конструктором).

10.5.3. Повторяющиеся поля записей

DuplicateRecordFields
Подразумевает

DisambiguateRecordFields

С

8.0.1

Разрешить определение типов записей с полями, имеющими одинаковые имена.

Выходя за рамки DisambiguateRecordFields (см. Разрешение неоднозначности полей записей), расширение DuplicateRecordFields позволяет объявлять несколько типов данных, используя одинаковые имена полей в одном модуле. Например, оно допускает это:

module M where
  data S = MkS { x :: Int }
  data T = MkT { x :: Bool }

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

s = MkS { x = 3 }

f (MkT { x = b }) = b

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

10.5.3.1. Селекторные функции

Поля могут быть использованы как селекторные функции только в том случае, если они однозначны, поэтому это всё ещё недопустимо, если в области видимости находятся и S(x) , и T(x):

bad r = x r

Неоднозначный селектор можно разрешить, «отодвинув» тип к месту использования селектора (см. Выведение типов для получения дополнительной информации о том, что означает «отодвинуть»). Например, следующие допустимы:

ok1 = x :: S -> Int

ok2 :: S -> Int
ok2 = x

ok3 = k x -- assuming we already have k :: (S -> Int) -> _

Кроме того, тип подразумеваемого типа данных можно указать как тип аргумента селектора:

ok4 s = x (s :: S)

Однако мы не выводим тип аргумента для определения типа данных или не имеем возможности отложить выбор в решатель ограничений. Таким образом, следующее неоднозначно:

bad :: S -> Int
bad s = x s

Даже если метка поля дублируется в своём модуле определения, возможно, что селектор можно использовать однозначно в другом месте. Например, другой модуль может импортировать S(x) , но не T(x), и затем использовать x однозначно.

10.5.3.2. Обновления записей

В обновлении записи, например, e { x = 1 }, если в области видимости находится несколько полей x, тогда тип контекста должен определить, какой тип данных записи подразумевается, или нужно указать тип. Рассмотрим следующие определения:

data S = MkS { foo :: Int }
data T = MkT { foo :: Int, bar :: Int }
data U = MkU { bar :: Int, baz :: Int }

Без расширения DuplicateRecordFields обновление, упоминающее foo , всегда будет неоднозначным, если все эти определения находятся в области видимости. Когда расширение включено, существует несколько вариантов разрешения неоднозначности обновлений:

  • Проверьте типы, содержащие все обновляемые поля. Например:

    f x = x { foo = 3, bar = 2 }
    

    Здесь f должно обновлять T, так как ни S, ни U не имеют оба поля.

  • Используйте тип, «отодвинутый» в обновление записи, как в следующем:

    g1 :: T -> T
    g1 x = x { foo = 3 }
    
    g2 x = x { foo = 3 } :: T
    
    g3 = k (x { foo = 3 }) -- assuming we already have k :: T -> _
    
  • Используйте явное указание типа в выражении записи, как в следующем:

    h x = (x :: T) { foo = 3 }
    

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

let x :: T
    x = blah
in x { foo = 3 }

\x -> [x { foo = 3 },  blah :: T ]

\ (x :: T) -> x { foo = 3 }

10.5.3.3. Импорт и экспорт полей записей

Когда включено расширение DuplicateRecordFields, неоднозначное поле должно экспортироваться как часть его типа данных, а не на верхнем уровне. Например, следующее допустимо:

module M (S(x), T(..)) where
  data S = MkS { x :: Int }
  data T = MkT { x :: Bool }

Однако это не будет разрешено, потому что x неоднозначно:

module M (x) where ...

Аналогичные ограничения применяются к импорту.

10.5.4. Омонимия записей

NamedFieldPuns
С

6.10.1

Разрешить использование омонимии записей.

Омонимия записей включена расширением языка NamedFieldPuns.

При работе с записями часто записывается шаблон, связывающий переменную с тем же именем, что и поле записи, например:

data C = C {a :: Int}
f (C {a = a}) = a

Омонимия записей позволяет опустить имя переменной, поэтому можно просто написать

f (C {a}) = a

что означает тот же шаблон, что и выше. То есть в шаблоне записи шаблон a расширяется до шаблона a = a для того же имени a.

Обратите внимание:

  • Омонимия записей также может использоваться в выражении, например, записью

    let a = 1 in C {a}
    

    вместо

    let a = 1 in C {a = a}
    

    Расширение является чисто синтаксическим, поэтому расширенное выражение правой части ссылается на ближайшую вложенную переменную, которая имеет то же имя, что и имя поля.

  • Омонимия и другие шаблоны могут быть смешаны в одной записи:

    data C = C {a :: Int, b :: Int}
    f (C {a, b = 4}) = a
    
  • Омонимия может использоваться везде, где встречаются шаблоны записей (например, в связывании let или на верхнем уровне).
  • Омонимия квалифицированного имени поля расширяется путём удаления квалификатора модуля. Например:

    f (C {M.a}) = a
    

    означает

    f (M.C {M.a = a}) = a
    

    (Это полезно, если селектор поля a для конструктора M.C находится в области видимости только в квалифицированной форме.)

10.5.5. Подстановочные знаки записей

RecordWildCards
Подразумевает

DisambiguateRecordFields.

С

6.8.1

Разрешить использование подстановочных знаков при построении и сопоставлении с образцом записей.

Подстановочные знаки записей включены расширением языка RecordWildCards. Это расширение подразумевает DisambiguateRecordFields.

Для записей с большим количеством полей может быть утомительно прописывать каждое поле по отдельности в шаблоне записи, как в

data C = C {a :: Int, b :: Int, c :: Int, d :: Int}
f (C {a = 1, b = b, c = c, d = d}) = b + c + d

Синтаксис подстановочных знаков записей позволяет использовать «..» в шаблоне записи, где каждое опущенное поле f заменяется шаблоном f = f. Например, вышеприведённый шаблон можно записать как

f (C {a = 1, ..}) = b + c + d

Дополнительные подробности:

  • В шаблонах можно комбинировать шаблоны с другими шаблонами, включая конструкции с заменой (Замены в записях); например, в шаблоне (C {a = 1, b, ..}). Кроме того, шаблоны с подстановкой записей можно использовать везде, где встречаются шаблоны записей, включая в let привязках и на верхнем уровне. Например, привязка на верхнем уровне

    C {a = 1, ..} = e
    

    определяет b, c, и d.

  • Шаблоны с подстановкой записей также могут использоваться в выражениях при построении записи. Например,

    let {a = 1; b = 2; c = 3; d = 4} in C {..}
    

    вместо

    let {a = 1; b = 2; c = 3; d = 4} in C {a=a, b=b, c=c, d=d}
    

    Расширение чисто синтаксическое, поэтому выражение шаблона с подстановкой записи относится к ближайшим вложенным переменным, имеющим то же имя, что и пропущенные имена полей.

  • Для шаблонов с подстановкой и выражений с подстановкой «..» расширяется до отсутствующих доступных полей записи. В частности, расширение «C {..}» включает f тогда и только тогда, когда:

    • f является полем записи конструктора C.
    • Поле записи f каким-либо образом находится в области видимости (явное или неявное).

    Эти правила ограничивают шаблоны с подстановкой записей ситуациями, в которых пользователь мог бы написать расширенную версию. Например

    module M where
      data R = R { a,b,c :: Int }
    module X where
      import M( R(R,a,c) )
      f a b = R { .. }
    

    R{..} расширяется до R{a=a}, исключая b, так как поле записи не находится в области видимости, и исключая c, так как переменная c не находится в области видимости (кроме привязки селектора записи c, конечно).

  • Когда шаблоны с подстановкой записей используются при построении записи, поле f инициализируется только если f находится в области видимости и не импортируется и не привязывается на верхнем уровне. Например, f может быть привязано с помощью вложенного соответствия шаблону или привязки let/where. Например

    module M where
      import A( a )
    
      data R = R { a,b,c,d :: Int }
    
      c = 3 :: Int
    
      f b = R { .. }  -- Expands to R { b = b, d = d }
        where
          d = b+1
    

    Здесь a импортировано, а c привязано на верхнем уровне, поэтому ни то, ни другое не вносит вклад в расширение «..». Цель состоит в том, чтобы читателю было легко понять, во что «..» расширяется.

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

    f x = x { v=True, .. }   -- Illegal (a)
    
    data T = MkT Int Bool
    g = MkT { .. }           -- Illegal (b)
    h (MkT { .. }) = True    -- Illegal (b)
    

10.5.6. Полиморфизм селектора поля записи

Модуль GHC.Records определяет следующее:

class HasField (x :: k) r a | x r -> a where
  getField :: r -> a

Ограничение HasField x r a указывает на то, что x является полем типа a, принадлежащим типу записи r. Метод getField возвращает функцию селектора записи.

Это позволяет определять определения, которые являются полиморфными по типам записей со специфицированным полем. Например, следующее работает с любым типом записи, у которого есть поле name :: String:

foo :: HasField "name" r String => r -> String
foo r = reverse (getField @"name" r)

HasField — это встроенный магический тип класса (аналогичный Coercible, например). Он обрабатывается решателем ограничений особым образом (см. Решение ограничений HasField). Пользователи также могут определять свои экземпляры HasField (см. Виртуальные поля записи).

10.5.6.1. Решение ограничений HasField

Если решатель ограничений сталкивается с ограничением HasField x r a где r — это конкретный тип данных с полем x в области видимости, он автоматически решит ограничение, используя селектор поля в качестве словаря, при необходимости унифицируя a с типом поля. Это происходит независимо от включённых расширений.

Например, если в области видимости находится следующий тип данных

data Person = Person { name :: String }

конечный результат будет похож на экземпляр

instance HasField "name" Person String where
  getField = name

за исключением того, что этот экземпляр фактически не генерируется, а ограничение решается непосредственно решателем ограничений.

Поле должно быть в области видимости для решения соответствующего ограничения HasField. Это сохраняет существующий механизм сокрытия представлений, при котором модуль может выбрать не экспортировать поле, предотвращая доступ или обновление клиента напрямую.

Решение ограничений HasField зависит от функций селектора поля, которые генерируются для каждого определения типа данных:

  • Если у поля записи нет функции селектора, потому что его тип позволит переменной существования сбежать, соответствующее ограничение HasField не будет решено. Например,

    {-# LANGUAGE ExistentialQuantification #-}
    data Exists t = forall x . MkExists { unExists :: t x }
    

    не порождает селектор unExists :: Exists t -> t x и мы не будем решать HasField "unExists" (Exists t) a автоматически.

  • Если у поля записи есть полиморфный тип (и, следовательно, функция селектора имеет более высокий ранг), соответствующее ограничение HasField не будет решено, потому что это нарушит функциональную зависимость от HasField и/или потребует импредикативности. Например,

    {-# LANGUAGE RankNTypes #-}
    data Higher = MkHigher { unHigher :: forall t . t -> t }
    

    порождает селектор unHigher :: Higher -> (forall t . t -> t), но не приводит к решению ограничения HasField "unHigher" Higher a.

  • Запись GADT может иметь ограниченный тип для функции селектора, что может привести к дополнительной унификации при решении ограничений HasField. Например,

    {-# LANGUAGE GADTs #-}
    data Gadt t where
      MkGadt :: { unGadt :: Maybe v } -> Gadt [v]
    

    порождает селектор unGadt :: Gadt [v] -> Maybe v, поэтому решатель уменьшит ограничение HasField "unGadt" (Gadt t) b путём унификации t ~ [v] и b ~ Maybe v для некоторой свежей метапеременной v, как будто у нас был экземпляр

    instance (t ~ [v], b ~ Maybe v) => HasField "unGadt" (Gadt t) b
    
  • Если у типа записи есть контекст старого типа данных, ограничение HasField будет уменьшено до решения ограничений из контекста. Например,

    {-# LANGUAGE DatatypeContexts #-}
    data Eq a => Silly a = MkSilly { unSilly :: a }
    

    порождает селектор unSilly :: Eq a => Silly a -> a, поэтому решатель уменьшит ограничение HasField "unSilly" (Silly a) b до Eq a (и унифицирует a с b ), как будто у нас был экземпляр

    instance (Eq a, a ~ b) => HasField "unSilly" (Silly a) b
    

10.5.6.2. Виртуальные поля записи

Пользователи могут определять свои собственные экземпляры HasField, при условии, что они не конфликтуют с встроенным поведением решения ограничений. Это позволяет определять «виртуальные» поля записей для типов данных, которые их не имеют.

Например, этот экземпляр сделает поле name из Person доступным с помощью #fullname:

instance HasField "fullname" Person String where
  getField = name

Более существенно, библиотека анонимных записей могла бы предоставить экземпляры HasField для своих анонимных записей и, таким образом, быть совместимой с полиморфными селекторами записей, введёнными этим предложением. Например, что-то вроде этого позволяет использовать getField для доступа к значениям Record с соответствующей строкой в списке полей на уровне типов:

data Record (xs :: [(k, Type)]) where
  Nil  :: Record '[]
  Cons :: Proxy x -> a -> Record xs -> Record ('(x, a) ': xs)

instance HasField x (Record ('(x, a) ': xs)) a where
  getField (Cons _ v _) = v
instance HasField x (Record xs) a => HasField x (Record ('(y, b) ': xs)) a where
  getField (Cons _ _ r) = getField @x r

r :: Record '[ '("name", String) ]
r = Cons Proxy "R" Nil)

x = getField @"name" r

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

data PersonFields = Name

s :: Record '[ '(Name, String) ]
s = Cons Proxy "S" Nil

y = getField @Name s

Для предотвращения конфликтов с встроенным решением ограничений запрещены следующие пользовательские экземпляры HasField (в дополнение к обычным правилам, таким как запрет семейств типов, появляющихся в заголовках экземпляров):

  • HasField _ r _ где r — переменная;
  • HasField _ (T ...) _ если T — это семейство данных (поскольку оно может иметь поля, добавленные позже с помощью объявлений экземпляров данных);
  • HasField x (T ...) _ если x — это переменная, а T имеет какие-либо поля (но этот экземпляр разрешен, если у T нет полей);
  • HasField "foo" (T ...) _ если у T есть поле foo (но этот экземпляр разрешен, если его нет).

Если у поля есть тип более высокого ранга или тип существования, соответствующее ограничение HasField не будет решено автоматически (как описано выше), но в интересах простоты мы не разрешаем пользователям определять свои собственные экземпляры.

10.6. Расширения механизма «deriving»

Haskell 98 позволяет программисту добавить производящую строку к объявлению типа данных, чтобы сгенерировать стандартное объявление экземпляра для указанного класса. GHC расширяет этот механизм по нескольким направлениям:

  • Механизм вывода может использоваться отдельно от объявления типа данных, используя механизм самостоятельного вывода.
  • В Haskell 98 единственными выводимыми классами являются Eq, Ord, Enum, Ix, Bounded, Read, и Show. Различные расширения языка расширяют этот список.
  • Помимо стандартного подхода к выводу экземпляров путём генерации всех определений методов, GHC поддерживает две дополнительные стратегии вывода, которые могут выводить произвольные классы:

    • Обобщённый вывод для newtype для newtype и
    • вывод любого класса с использованием пустого объявления экземпляра.

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

10.6.1. Вывод экземпляров для пустых типов данных

END_OF_DOCUMENT_MARKER
EmptyDataDeriving
Since

8.4.1

Разрешает вывод экземпляров стандартных типов классов для пустых типов данных.

Можно написать типы данных без конструкторов, используя флаг EmptyDataDecls (см. Типы данных без конструкторов), который включён по умолчанию в Haskell 2010. По умолчанию не включена возможность вывода экземпляров типов классов для этих типов. Эта возможность активируется с помощью флага EmptyDataDeriving. Например, это позволяет написать:

data Empty deriving (Eq, Ord, Read, Show)

Это сгенерировало бы следующие экземпляры:

instance Eq Empty where
  _ == _ = True

instance Ord Empty where
  compare _ _ = EQ

instance Read Empty where
  readPrec = pfail

instance Show Empty where
  showsPrec _ x = case x of {}

Флаг EmptyDataDeriving требуется только для включения вывода этих четырёх «стандартных» типов классов (которые упоминаются в отчёте Haskell). Другие расширения механизма deriving , которые объяснены ниже более подробно, не требуют использования EmptyDataDeriving в сочетании с пустыми типами данных. К ним относятся:

  • StandaloneDeriving (см. Самостоятельные объявления вывода)
  • Типы классов, для вывода которых необходимо включить свои расширения, такие как DeriveFunctor (см. Вывод экземпляров дополнительных классов (Data и т.д.))
  • DeriveAnyClass (см. Вывод любого другого класса)

10.6.2. Выведенный контекст для положений вывода

Отчёт Haskell неясно описывает, когда предложение вывода является допустимым. Например:

data T0 f a = MkT0 a         deriving( Eq )
data T1 f a = MkT1 (f a)     deriving( Eq )
data T2 f a = MkT2 (f (f a)) deriving( Eq )

Естественный сгенерированный Eq код приведёт к этим объявлениям экземпляров:

instance Eq a         => Eq (T0 f a) where ...
instance Eq (f a)     => Eq (T1 f a) where ...
instance Eq (f (f a)) => Eq (T2 f a) where ...

Первый из них очевидно корректен. Второй также корректен, хотя и менее очевидно. Третий не является Haskell 98 и рискует потерей завершения экземпляров.

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

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

10.6.3. Самостоятельные объявления вывода

StandaloneDeriving
Since

6.8.1

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

GHC позволяет использовать самостоятельные deriving объявления, активированные с помощью StandaloneDeriving:

data Foo a = Bar a | Baz String

deriving instance Eq a => Eq (Foo a)

Синтаксис идентичен синтаксису обычного объявления экземпляра, за исключением (а) ключевого слова deriving, и (б) отсутствия части where.

Однако, самостоятельный вывод отличается от предложения deriving в ряде важных аспектов:

  • Самостоятельное объявление вывода не должно находиться в том же модуле, что и объявление типа данных. (Но будьте осторожны с опасностями «сиротских» экземпляров (Сиротские модули и объявления экземпляров).
  • В большинстве случаев вам нужно предоставить явный контекст (в примере контекстом является (Eq a)), точно так же, как и в обычном объявлении экземпляра. (В отличие от этого, в предложении deriving , прикреплённом к объявлению типа данных, контекст выводится.)

    Исключение из этого правила заключается в том, что контекст самостоятельного объявления вывода может выводить свой контекст, когда используется единственное ограничение с дополнительными подстановками в качестве контекста, например:

    deriving instance _ => Eq (Foo a)
    

    Это по существу то же самое, что если бы вы написали deriving Eq после объявления для data Foo a. Для использования этой функции требуется использование PartialTypeSignatures (Частичные типы сигнатур).

  • В отличие от объявления deriving , прикреплённого к объявлению data , экземпляр может быть более конкретным, чем тип данных (при условии, что вы также используете FlexibleInstances, Смягчённые правила для контекстов экземпляров). Рассмотрим, например:

    data Foo a = Bar a | Baz String
    
    deriving instance Eq a => Eq (Foo [a])
    deriving instance Eq a => Eq (Foo (Maybe a))
    

    Это сгенерирует выведенный экземпляр для (Foo [a]) и (Foo (Maybe a)), но другие типы, такие как (Foo (Int,Bool)) не будут экземплярами Eq.

  • В отличие от объявления deriving , прикреплённого к объявлению data , GHC не ограничивает форму типа данных. Вместо этого GHC просто генерирует соответствующий код шаблона для указанного класса и проверяет его типизацию. Если возникает ошибка типа, это ваша проблема. (GHC покажет вам ошибочный код, если он содержит ошибку типа.)

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

    data T a where
       T1 :: T Int
       T2 :: T Bool
    
    deriving instance Show (T a)
    

    В этом примере вы не можете сказать ... deriving( Show ) в объявлении типа данных для T, поскольку T является GADT, но вы можете сгенерировать объявление экземпляра, используя самостоятельный вывод.

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

  • В большинстве случаев вы не можете использовать самостоятельный вывод для создания экземпляра для типа данных, конструкторы которого не находятся в области видимости. Это связано с тем, что выведенный экземпляр генерирует код, который использует конструкторы за кулисами, что нарушит абстракцию.

    Исключение из этого правила — DeriveAnyClass, так как вывод экземпляра через DeriveAnyClass просто генерирует пустое объявление экземпляра, которое не требует использования конструкторов. См. раздел вывод любого класса для получения более подробной информации.

В других отношениях самостоятельный вывод подчиняется тем же правилам, что и обычный вывод:

  • Объявление deriving instance должно подчиняться тем же правилам относительно формы и завершения, что и обычные объявления экземпляров, управляемые теми же флагами; см. Объявления экземпляров.
  • Синтаксис самостоятельного вывода обобщён для новых типов точно так же, как обобщены обычные предложения deriving ( Обобщенные выведенные экземпляры для новых типов). Например:

    newtype Foo a = MkFoo (State Int a)
    
    deriving instance MonadState Int Foo
    

    GHC всегда обрабатывает последний параметр экземпляра (Foo в данном примере) как тип, экземпляр которого выводится.

10.6.4. Вывод экземпляров дополнительных классов (Data, и т. д.)

Haskell 98 позволяет программисту добавлять «deriving( Eq, Ord )» к объявлению типа данных, чтобы сгенерировать стандартное объявление экземпляра для классов, указанных в предложении deriving. В Haskell 98, единственные классы, которые могут появляться в предложении deriving — это стандартные классы Eq, Ord, Enum, Ix, Bounded, Read, и Show.

GHC расширяет этот список несколькими другими классами, которые могут быть автоматически выведены:

  • С помощью DeriveGeneric, вы можете вывести экземпляры классов Generic и Generic1, определённых в GHC.Generics. Вы можете использовать их для определения обобщённых функций, как описано в Обобщённое программирование.
  • С помощью DeriveFunctor, вы можете вывести экземпляры класса Functor, определённого в GHC.Base.
  • С помощью DeriveDataTypeable, вы можете вывести экземпляры класса Data, определённого в Data.Data.
  • С помощью DeriveFoldable, вы можете вывести экземпляры класса Foldable, определённого в Data.Foldable.
  • С помощью DeriveTraversable, вы можете вывести экземпляры класса Traversable, определённого в Data.Traversable. Поскольку экземпляр Traversable определяет экземпляры Functor и Foldable, вероятно, вам также захочется вывести их, поэтому DeriveTraversable подразумевает DeriveFunctor и DeriveFoldable.
  • С помощью DeriveLift, вы можете вывести экземпляры класса Lift, определённого в модуле Language.Haskell.TH.Syntax пакета template-haskell.

Вы также можете использовать самостоятельное объявление вывода (см. Самостоятельные объявления вывода).

В каждом случае соответствующий класс должен быть в области видимости, прежде чем он может быть упомянут в deriving пункте.

10.6.4.1. Выведение экземпляров Functor

DeriveFunctor
Since

7.10.1

Разрешить автоматическое выведение экземпляров для типового класса Functor.

С помощью DeriveFunctor можно вывести экземпляры Functor для типов данных вида Type -> Type. Например, это объявление:

data Example a = Ex a Char (Example a) (Example Char)
  deriving Functor

сгенерировало бы следующий экземпляр:

instance Functor Example where
  fmap f (Ex a1 a2 a3 a4) = Ex (f a1) a2 (fmap f a3) a4

Основной алгоритм для DeriveFunctor проходит по аргументам каждого конструктора типа данных, применяя функцию отображения в зависимости от типа каждого аргумента. Если обнаружена простая переменная типа, которая синтаксически эквивалентна последнему типу параметра типа данных (a в приведённом выше примере), то мы применяем функцию f непосредственно к ней. Если встречается тип, не синтаксически эквивалентный последнему типу параметру, но содержащий его где-то в себе, то делается рекурсивный вызов fmap. Если найден тип, который вообще не упоминает последний тип параметр, то он оставляется без изменений.

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

newtype Right a = Right (Either Int a) deriving Functor

Однако, незначительное изменение кода приводит к тому, что код не будет компилироваться:

newtype Wrong a = Wrong (Either a Int) deriving Functor

Различие заключается в размещении последнего типа параметра, a. В случае Right a встречается в типе Either Int a, и, более того, появляется как последний тип аргумент Either . В случае Wrong , однако, a не является последним типом аргументом для Either; а Int есть.

Это различие важно из-за того, как работает DeriveFunctor. Выведенный экземпляр Functor Right будет:

instance Functor Right where
  fmap f (Right a) = Right (fmap f a)

В данном случае, для значения типа Right a, GHC должен произвести значение типа Right b. Поскольку аргумент конструктора Right имеет тип Either Int a, код рекурсивно вызывает fmap для него, чтобы произвести значение типа Either Int b, которое в свою очередь используется для построения конечного значения типа Right b.

Сгенерированный код для экземпляра Functor Wrong будет выглядеть точно так же, за исключением того, что Wrong заменит все вхождения Right. Проблема теперь в том, что fmap рекурсивно применяется к значению типа Either a Int . Это не может произвести значение типа Either b Int, поскольку fmap может изменить только последний тип параметр! Это приводит к тому, что сгенерированный код будет плохо типизирован.

В общем случае, если у типа данных есть выведенный экземпляр Functor и его последний тип параметр появляется в правой части объявления типа, то либо он должен (1) появляться как есть (например, newtype Id a = Id a), либо (2) как последний аргумент конструктора типа (как в Right выше).

Есть два исключения из этого правила:

  1. Кортежи. Когда неединичный кортеж используется в правой части объявления типа, DeriveFunctor обрабатывает его как произведение различных типов. Другими словами, следующий код:

    newtype Triple a = Triple (a, Int, [a]) deriving Functor
    

    приведёт к выведенному экземпляру Functor в виде:

    instance Functor Triple where
      fmap f (Triple a) =
        Triple (case a of
                     (a1, a2, a3) -> (f a1, a2, fmap f a3))
    

    То есть, DeriveFunctor проталкивается внутрь кортежей и применяет отображение к каждому типу, образующему кортеж. Сгенерированный код напоминает то, что сгенерировано бы из data Triple a = Triple a Int [a], за исключением дополнительного механизма для работы с кортежами.

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

    newtype CovFun1 a = CovFun1 (Int -> a) deriving Functor
    newtype CovFun2 a = CovFun2 ((a -> Int) -> a) deriving Functor
    newtype CovFun3 a = CovFun3 (((Int -> a) -> Int) -> a) deriving Functor
    

    Все три этих примера будут компилироваться без ошибок. С другой стороны:

    newtype ContraFun1 a = ContraFun1 (a -> Int) deriving Functor
    newtype ContraFun2 a = ContraFun2 ((Int -> a) -> Int) deriving Functor
    newtype ContraFun3 a = ContraFun3 (((a -> Int) -> a) -> Int) deriving Functor
    

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

    Интуитивно ковариантный тип вырабатывается, а контравариантный тип потребляется. Большинство типов в Haskell являются ковариантными, но тип функции является особым, поскольку левая часть стрелки функции меняет вариативность. Если тип функции a -> b появляется в ковариантной позиции (например, CovFun1 выше), то a находится в контравариантной позиции, а b в ковариантной позиции. Аналогично, если a -> b появляется в контравариантной позиции (например, CovFun2 выше), то a находится в a ковариантной позиции, а b в контравариантной позиции.

    Чтобы увидеть, почему у типа данных с контравариантным вхождением последнего типа параметра не может быть экземпляра выведенного Functor , предположим, что существует экземпляр Functor ContraFun1. Реализация будет выглядеть примерно так:

    instance Functor ContraFun1 where
      fmap f (ContraFun g) = ContraFun (\x -> _)
    

    У нас есть f :: a -> b, g :: a -> Int, и x :: b . Используя их, мы должны как-то заполнить пробел (обозначенный нижним подчеркиванием) значением типа Int . Какие у нас варианты?

    Мы могли бы попробовать применить g к x . Однако это не сработает, так как g ожидает аргумент типа a, а x :: b . Ещё хуже, мы не можем превратить x во что-то типа a, так как f также требует аргумента типа a! Короче говоря, нет хорошего способа сделать это.

    С другой стороны, выведенные экземпляры Functor для CovFun находятся в области возможностей:

    instance Functor CovFun1 where
      fmap f (CovFun1 g) = CovFun1 (\x -> f (g x))
    
    instance Functor CovFun2 where
      fmap f (CovFun2 g) = CovFun2 (\h -> f (g (\x -> h (f x))))
    
    instance Functor CovFun3 where
      fmap f (CovFun3 g) = CovFun3 (\h -> f (g (\k -> h (\x -> f (k x)))))
    

Есть и другие сценарии, в которых выведенный экземпляр Functor не будет компилироваться:

  1. Тип данных не имеет параметров типа (например, data Nothing = Nothing).
  2. Последняя переменная типа типа данных используется в ограничении DatatypeContexts (например, data Ord a => O a = O a).
  3. Последняя переменная типа типа данных используется в ограничении ExistentialQuantification или уточняется в GADT. Например,

    data T a b where
        T4 :: Ord b => b -> T a b
        T5 :: b -> T b b
        T6 :: T a (b,b)
    
    deriving instance Functor (T a)
    

    не будет компилироваться успешно из-за того, как b ограничен.

Когда последний тип параметр играет роль фантома (см. Роли), выведенный экземпляр Functor не будет создан с помощью обычного алгоритма. Вместо этого всё значение будет принудительно преобразовано.

data Phantom a = Z | S (Phantom a) deriving Functor

произведёт следующий экземпляр:

instance Functor Phantom where
  fmap _ = coerce

Когда тип не имеет конструкторов, выведенный экземпляр Functor просто принудительно задаёт (нижнюю) значение аргумента с помощью EmptyCase.

data V a deriving Functor
type role V nominal

произведёт

instance Functor V where

fmap _ z = case z of

10.6.4.2. Выведение экземпляров Foldable

DeriveFoldable
Since

7.10.1

Разрешить автоматическое выведение экземпляров для типового класса Foldable.

С помощью DeriveFoldable можно вывести экземпляры Foldable для типов данных вида Type -> Type. Например, это объявление:

data Example a = Ex a Char (Example a) (Example Char)
  deriving Foldable

сгенерирует следующий экземпляр:

instance Foldable Example where
  foldr f z (Ex a1 a2 a3 a4) = f a1 (foldr f z a3)
  foldMap f (Ex a1 a2 a3 a4) = mappend (f a1) (foldMap f a3)

Алгоритм для DeriveFoldable адаптирован из алгоритма DeriveFunctor, но он генерирует определения для foldMap, foldr, и null вместо fmap. Кроме того, DeriveFoldable отфильтровывает все аргументы конструкторов в выражении справа, типы которых не упоминают последний параметр типа, поскольку эти аргументы не нужно складывать.

Когда параметр типа имеет фиктивную роль (см. Роли), DeriveFoldable выводит тривиальный экземпляр. Например, это объявление:

data Phantom a = Z | S (Phantom a)

сгенерирует следующий экземпляр.

instance Foldable Phantom where
  foldMap _ _ = mempty

Аналогично, когда тип не имеет конструкторов, DeriveFoldable выведет тривиальный экземпляр:

data V a deriving Foldable
type role V nominal

сгенерирует следующее.

instance Foldable V where
  foldMap _ _ = mempty

Вот различия между сгенерированным кодом для Functor и Foldable:

#. Когда встречается переменная типа a, DeriveFunctor сгенерирует f a для определения fmap. DeriveFoldable сгенерирует f a z для foldr, f a для foldMap и False для null.

  1. Когда встречается тип, который не является синтаксически эквивалентным a, но который содержит a, DeriveFunctor рекурсивно вызовет fmap на нём. Аналогично, DeriveFoldable рекурсивно вызовет foldr и foldMap. В зависимости от контекста, null может рекурсивно вызвать null или all null. Например, при

    data F a = F (P a)
    data G a = G (P (a, Int))
    data H a = H (P (Q a))
    

    Foldable вывод будет

    null (F x) = null x
    null (G x) = null x
    null (H x) = all null x
    
  2. DeriveFunctor собирает всё вместе в конце, вызвав конструктор. DeriveFoldable, однако, строит значение некоторого типа. Для foldr, это достигается путём цепочки применений f и рекурсивных вызовов foldr на значении состояния z. Для foldMap, это происходит путём объединения всех значений с mappend. Для null, значения обычно объединяются с &&. Однако, если какое-либо из значений известно как False, все остальные будут отброшены. Например,

    data SnocList a = Nil | Snoc (SnocList a) a
    

    не сгенерирует

    null (Snoc xs _) = null xs && False
    

    а скорее

    null (Snoc _ _) = False
    

Есть некоторые другие различия относительно типов данных, которые могут иметь сгенерированные Foldable экземпляры:

  1. Типы данных, содержащие типы функций справа, не могут иметь сгенерированных Foldable экземпляров.
  2. Экземпляры Foldable могут быть выведены для типов данных, в которых последний параметр типа существует или уточняется в GADT. Например, этот тип данных:

    data E a where
        E1 :: (a ~ Int) => a   -> E a
        E2 ::              Int -> E Int
        E3 :: (a ~ Int) => a   -> E Int
        E4 :: (a ~ Int) => Int -> E a
    
    deriving instance Foldable E
    

    имел бы следующий сгенерированный Foldable экземпляр:

    instance Foldable E where
        foldr f z (E1 e) = f e z
        foldr f z (E2 e) = z
        foldr f z (E3 e) = z
        foldr f z (E4 e) = z
    
        foldMap f (E1 e) = f e
        foldMap f (E2 e) = mempty
        foldMap f (E3 e) = mempty
        foldMap f (E4 e) = mempty
    

    Обратите внимание, как каждый конструктор E использует какой-то вид экзистенциального квантификатора, но только аргумент E1 фактически «складывается». Это происходит потому, что мы намеренно выбираем складывать только универсально полиморфные типы, которые синтаксически эквивалентны последнему параметру типа. В частности:

  • Мы не складываем аргументы E1 или E4, потому что, хотя (a ~ Int), Int не синтаксически эквивалентны a.
  • Мы не складываем аргумент E3, потому что a не универсально полиморфен. a в E3 (явным образом) экзистенциально квантифицировано, поэтому оно не такое же, как последний параметр типа E.

10.6.4.3. Выведение экземпляров Traversable

DeriveTraversable
Подразумевает

DeriveFoldable, DeriveFunctor

С

7.10.1

Разрешает автоматическое выведение экземпляров для типового класса Traversable.

С помощью DeriveTraversable можно вывести экземпляры Traversable для типов данных вида Type -> Type. Например, это объявление:

data Example a = Ex a Char (Example a) (Example Char)
  deriving (Functor, Foldable, Traversable)

сгенерирует следующий экземпляр Traversable:

instance Traversable Example where
  traverse f (Ex a1 a2 a3 a4)
    = fmap (\b1 b3 -> Ex b1 a2 b3 a4) (f a1) <*> traverse f a3

Алгоритм для DeriveTraversable адаптирован из алгоритма DeriveFunctor, но он генерирует определение для traverse вместо fmap. Кроме того, DeriveTraversable фильтрует все аргументы конструкторов в выражении справа, типы которых не упоминают последний параметр типа, поскольку эти аргументы не оказывают никакого влияния при обходе.

Когда параметр типа имеет фиктивную роль (см. Роли), DeriveTraversable принудительно преобразует свой аргумент. Например, это объявление:

data Phantom a = Z | S (Phantom a) deriving Traversable

сгенерирует следующий экземпляр:

instance Traversable Phantom where
  traverse _ z = pure (coerce z)

Когда тип не имеет конструкторов, DeriveTraversable выведет самый ленивый экземпляр, который может.

data V a deriving Traversable
type role V nominal

сгенерирует следующее, используя EmptyCase:

instance Traversable V where
  traverse _ z = pure (case z of)

Вот различия между сгенерированным кодом в каждом расширении:

  1. Когда встречается переменная типа a, как DeriveFunctor, так и DeriveTraversable сгенерируют f a для fmap и traverse определения соответственно.
  2. Когда встречается тип, который не является синтаксически эквивалентным a, но который содержит a, DeriveFunctor рекурсивно вызовет fmap на нём. Аналогично, DeriveTraversable рекурсивно вызовет traverse.
  3. DeriveFunctor собирает всё вместе в конце, вызвав конструктор. DeriveTraversable делает нечто подобное, но работает в контексте Applicative, объединяя всё вместе с помощью (<*>).

В отличие от DeriveFunctor, DeriveTraversable нельзя использовать с типами данных, содержащими тип функции справа.

Полное описание алгоритмов, используемых в DeriveFunctor, DeriveFoldable и DeriveTraversable, см. на этой странице вики .

10.6.4.4. Выведение экземпляров Data

DeriveDataTypeable
С

6.8.1

Разрешает автоматическое выведение экземпляров для типового класса Data

10.6.4.5. Выведение экземпляров Typeable

Класс Typeable очень специфичен:

  • Typeable является полиморфным по типу (см. Полиморфизм по типу).
  • GHC имеет собственный решатель для разрядки ограничений, которые включают класс Typeable, и ручные реализации запрещены. Это гарантирует, что программист не может обойти систему типов, написав фальшивые реализации.
  • Производные реализации Typeable могут быть объявлены, если включено расширение DeriveDataTypeable, но они игнорируются, и они могут быть сообщены как ошибка в более поздней версии компилятора.
  • Правила для решения ограничений Typeable таковы:

    • Конструктор конкретного типа, применённый к некоторым типам.

      instance (Typeable t1, .., Typeable t_n) =>
        Typeable (T t1 .. t_n)
      

      Это правило работает для любого конструктора конкретного типа, включая конструкторы типов с полиморфными типами. Единственное ограничение заключается в том, что если конструктор типа имеет полиморфный тип, то он должен применяться ко всем его параметрам типа, и эти типы должны быть конкретными (т. е. они не могут упоминать переменные типа).

    • Переменная типа, применённая к некоторым типам:

      instance (Typeable f, Typeable t1, .., Typeable t_n) =>
        Typeable (f t1 .. t_n)
      
    • Конкретная литеральная запись типа.:

      instance Typeable 0       -- Type natural literals
      instance Typeable "Hello" -- Type-level symbols
      

10.6.4.6. Выведение реализаций Lift

DeriveLift
С момента

8.0.1

Включить автоматическое выведение реализаций для типового класса Lift для Template Haskell.

Класс Lift, в отличие от других классов, допускающих вывод, находится в template-haskell вместо base. Наличие в качестве реализации типа данных класса Lift позволяет продвигать его значения до выражений Template Haskell (типа ExpQ и TExpQ a), которые затем могут быть вставлены в исходный код Haskell.

Вот пример того, как можно вывести Lift:

{-# LANGUAGE DeriveLift #-}
module Bar where

import Language.Haskell.TH.Syntax

data Foo a = Foo a | a :^: a deriving Lift

{-
instance (Lift a) => Lift (Foo a) where
    lift (Foo a) = [| Foo a |]
    lift ((:^:) u v) = [| (:^:) u v |]

    liftTyped (Foo a) = [|| Foo a ||]
    liftTyped ((:^:) u v) = [|| (:^:) u v ||]
-}

-----
{-# LANGUAGE TemplateHaskell #-}
module Baz where

import Bar
import Language.Haskell.TH.Lift

foo :: Foo String
foo = $(lift $ Foo "foo")

fooExp :: Lift a => Foo a -> Q Exp
fooExp f = [| f |]

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

{-# LANGUAGE DeriveLift, MagicHash #-}
module Unboxed where

import GHC.Exts
import Language.Haskell.TH.Syntax

data IntHash = IntHash Int# deriving Lift

{-
instance Lift IntHash where
    lift (IntHash i) = [| IntHash i |]
    liftTyped (IntHash i) = [|| IntHash i ||]
-}

10.6.5. Обобщенные производные реализации для newtype

GeneralisedNewtypeDeriving
GeneralizedNewtypeDeriving
С момента

6.8.1. Британская орфография с 8.6.1.

Включить хитрое обобщенное механизм вывода GHC для newtype.

Когда вы определяете абстрактный тип с помощью newtype, вы можете захотеть, чтобы новый тип унаследовал некоторые реализации от своего представления. В Haskell 98 можно унаследовать реализации Eq, Ord, Enum и Bounded путём вывода, но для любых других классов вам нужно написать явное объявление реализации. Например, если вы определите

newtype Dollars = Dollars Int

и вы хотите использовать арифметику над Dollars, вам нужно явно определить реализацию Num:

instance Num Dollars where
  Dollars a + Dollars b = Dollars (a+b)
  ...

Все, что делает реализация, это применяет и удаляет конструктор newtype. Особенно неприятно, что, поскольку конструктор не появляется во время выполнения, это объявление реализации определяет словарь, который полностью эквивалентен словарю Int, только медленнее!

DerivingVia (см. Выведение через) является обобщением этой идеи.

10.6.5.1. Обобщение фрагмента вывода

GHC теперь разрешает выводить такие реализации вместо этого, используя расширение GeneralizedNewtypeDeriving, так что можно написать

newtype Dollars = Dollars { getDollars :: Int } deriving (Eq,Show,Num)

и реализация использует тот же самый словарь Num для Dollars , что и для Int. Другими словами, GHC сгенерирует что-то, что напоминает следующий код

instance Num Int => Num Dollars

и затем попытается упростить контекст Num Int как можно больше. GHC знает, что существует реализация Num Int , поэтому он может разрядить ограничение Num Int , оставив код, который GHC фактически генерирует

instance Num Dollars

Можно представить себе эту реализацию, реализованную с использованием того же самого кода, что и реализация Num Int , но с Dollars и getDollars добавленными там, где это необходимо, чтобы сделать её типобезопасной. (На практике GHC использует несколько другой подход к генерации кода. См. раздел Более точное описание ниже для получения дополнительных сведений.)

Мы также можем вывести реализации классов конструкторов аналогичным образом. Например, предположим, что мы реализовали трансформаторы моноидов состояния и ошибок, такие что

instance Monad m => Monad (State s m)
instance Monad m => Monad (Failure m)

В Haskell 98 мы можем определить моноид парсинга как

type Parser tok m a = State [tok] (Failure m) a

который автоматически является моноидом благодаря объявлениям реализаций выше. С расширением мы можем сделать тип парсера абстрактным, без необходимости писать реализацию класса Monad с помощью

newtype Parser tok m a = Parser (State [tok] (Failure m) a)
                       deriving Monad

В этом случае объявление производной реализации имеет вид

instance Monad (State [tok] (Failure m)) => Monad (Parser tok m)

Обратите внимание, что, поскольку Monad является классом конструктора, реализация является частичным применением newtype, а не всей левой части. Мы можем представить себе, что объявление типа «эта-преобразуется», чтобы сгенерировать контекст объявления реализации.

Мы даже можем вывести реализации многопараметрических классов, при условии, что newtype является последним параметром класса. В этом случае в фрагменте deriving появляется «частичное применение» класса. Например, задан класс

class StateMonad s m | m -> s where ...
instance Monad m => StateMonad s (State s m) where ...

тогда мы можем вывести реализацию StateMonad для Parser с помощью

newtype Parser tok m a = Parser (State [tok] (Failure m) a)
                       deriving (Monad, StateMonad [tok])

Производная реализация получается путем завершения применения класса к типу newtype:

instance StateMonad [tok] (State [tok] (Failure m)) =>
         StateMonad [tok] (Parser tok m)

В результате этого расширения все производные реализации в объявлениях newtype обрабатываются одинаково (и реализуются просто за счет повторного использования словаря для типа представления), за исключением Show и Read, которые действительно ведут себя по-разному для newtype и его представления.

Примечание

Иногда необходимо включить дополнительные расширения языка при выводе реализаций с помощью GeneralizedNewtypeDeriving. Например, рассмотрим простой класс и реализацию, использующую синтаксис UnboxedTuples:

{-# LANGUAGE UnboxedTuples #-}

module Lib where

class AClass a where
  aMethod :: a -> (# Int, a #)

instance AClass Int where
  aMethod x = (# x, x #)

Следующее завершится ошибкой «Незаконная необрамлённая кортеж», поскольку сгенерированная компилятором производная реализация использует синтаксис необрамлённых кортежей,

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

import Lib

newtype Int' = Int' Int
             deriving (AClass)

Однако включение расширения UnboxedTuples позволяет модулю успешно компилироваться. Аналогичные ошибки могут возникнуть при использовании различных расширений, включая:

  • UnboxedTuples
  • PolyKinds
  • MultiParamTypeClasses
  • FlexibleContexts

10.6.5.2. Более точное описание

Производная реализация выводится только для объявлений этих форм (после расширения любых синонимов типов)

newtype T v1..vn                   = MkT (t vk+1..vn) deriving (C t1..tj)
newtype instance T s1..sk vk+1..vn = MkT (t vk+1..vn) deriving (C t1..tj)

где

  • v1..vn — это переменные типа, а t, s1..sk, t1..tj — это типы.
  • (C t1..tj) — это частичное применение класса C, где арность C равна j+1. То есть C недостаёт ровно одного аргумента типа.
  • k выбирается так, что C t1..tj (T v1...vk) имеет правильный тип. (Или, в случае data instance, так что C t1..tj (T s1..sk) имеет правильный тип.)
  • Тип t — это произвольный тип.
  • Переменные типа vk+1...vn не встречаются в типах t, s1..sk или t1..tj.
  • C не равно Read, Show, Typeable или Data. Эти классы не должны «смотреть сквозь» тип или его конструктор. Вы всё ещё можете вывести эти классы для newtype, но это происходит обычным способом, а не с помощью этого нового механизма. Обратитесь к разделу Стратегия вывода по умолчанию.
  • Безопасно привести каждый из методов C. То есть отсутствующий последний аргумент у C не используется в номинальном роли ни в одном из методов C (см. Роли).
  • C может иметь связанные семейства типов, при условии, что они соответствуют требованиям, изложенным в разделе GND и связанные типы.

Тогда объявление производной реализации имеет вид

instance C t1..tj t => C t1..tj (T v1...vk)

Обратите внимание, что если C не содержит методов класса, контекст реализации полностью не нужен, и поэтому GHC вместо этого сгенерирует:

instance C t1..tj (T v1..vk)

В качестве примера, который не работает, рассмотрим

newtype NonMonad m s = NonMonad (State s m s) deriving Monad

Здесь мы не можем вывести реализацию

instance Monad (State s m) => Monad (NonMonad m)

потому что переменная типа s встречается в State s m, и поэтому ее нельзя «эпсилон-преобразовать». Хорошо, что эта deriving строка отклоняется, потому что NonMonad m на самом деле не является монадой — по той же причине. Попробуйте определить >>= с правильным типом: у вас не получится.

Заметьте также, что порядок параметров класса становится важным, поскольку мы можем вывести экземпляры только для последнего. Если класс StateMonad выше был определен как

class StateMonad m s | m -> s where ...

то мы не смогли бы вывести экземпляр для типа Parser выше. Мы предполагаем, что классы с несколькими параметрами обычно имеют один «главный» параметр, для которого выведение новых экземпляров наиболее интересно.

Наконец, все это относится только к классам, отличным от Read, Show, Typeable, и Data, для которых применяется стандартное выведение (раздел 4.3.3. отчета Haskell). (Для стандартных классов Eq, Ord, Ix, и Bounded не имеет значения, используется ли стандартный метод или описанный здесь метод.)

10.6.5.3. Семейства связанных типов

GeneralizedNewtypeDeriving также работает для некоторых типов классов со связанными семействами типов. Вот пример:

class HasRing a where
  type Ring a

newtype L1Norm a = L1Norm a
  deriving HasRing

Выведенный экземпляр HasRing будет выглядеть так

instance HasRing (L1Norm a) where
  type Ring (L1Norm a) = Ring a

Точнее, если класс, который выводится, имеет вид

class C c_1 c_2 ... c_m where
  type T1 t1_1 t1_2 ... t1_n
  ...
  type Tk tk_1 tk_2 ... tk_p

а новый тип имеет вид

newtype N n_1 n_2 ... n_q = MkN <rep-type>

то вы можете вывести экземпляр C c_1 c_2 ... c_(m-1) для N n_1 n_2 ... n_q, при условии, что:

  • Параметр типа c_m встречается один раз в каждой из переменных типа T1 до Tk. Представьте класс, где это условие не выполняется. Например:

    class Bad a b where
      type B a
    
    instance Bad Int a where
      type B Int = Char
    
    newtype Foo a = Foo a
      deriving (Bad Int)
    

    Для выведенного экземпляра Bad Int GHC потребуется сгенерировать что-то вроде этого:

    instance Bad Int (Foo a) where
      type B Int = B ???
    

    Теперь мы зашли в тупик, так как у нас нет способа сослаться на a в правой части экземпляра семейства B, поэтому этот экземпляр не имеет смысла в контексте GeneralizedNewtypeDeriving.

  • C не имеет связанных семейств данных (только семейства типов). Чтобы понять, почему семейства данных запрещены, представьте следующую ситуацию:

    class Ex a where
      data D a
    
    instance Ex Int where
      data D Int = DInt Bool
    
    newtype Age = MkAge Int deriving Ex
    

    Для выведенного экземпляра Ex GHC потребуется сгенерировать что-то вроде этого:

    instance Ex Age where
      data D Age = ???
    

    Но неясно, что GHC заполнит для ???, так как каждый экземпляр семейства данных должен генерировать новые конструкторы данных.

Если оба этих условия выполнены, GHC сгенерирует этот экземпляр:

instance C c_1 c_2 ... c_(m-1) <rep-type> =>
         C c_1 c_2 ... c_(m-1) (N n_1 n_2 ... n_q) where
  type T1 t1_1 t1_2 ... (N n_1 n_2 ... n_q) ... t1_n
     = T1 t1_1 t1_2 ... <rep-type>          ... t1_n
  ...
  type Tk tk_1 tk_2 ... (N n_1 n_2 ... n_q) ... tk_p
     = Tk tk_1 tk_2 ... <rep-type>          ... tk_p

Опять же, если C не содержит методов класса, контекст экземпляра будет избыточным, поэтому GHC вместо этого сгенерирует instance C c_1 c_2 ... c_(m-1) (N n_1 n_2 ... n_q).

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

class C a where
  type T a

newtype Loop = MkLoop Loop
  deriving C

Это сгенерирует выведенный экземпляр:

instance C Loop where
  type T Loop = T Loop

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

instance C Int where
  type C Int = Int

newtype MyInt = MyInt Int
  deriving C

Это сгенерирует выведенный экземпляр:

instance C MyInt where
  type T MyInt = T Int

Хотя типовая проверка T MyInt завершится, проверщик завершения GHC недостаточно сложен, чтобы определить это, поэтому вам нужно включить UndecidableInstances, чтобы использовать этот выведенный экземпляр. Если вы решите пойти по этому пути, убедитесь, что вы можете убедиться, что все экземпляры семейства типов, которые вы выводите, в конечном итоге завершатся, если они будут использованы!

Обратите внимание, что DerivingVia (см. Вывод через) использует в основном ту же спецификацию для вывода экземпляров связанных семейств типов (за исключением того, что он использует тип via вместо базового rep-type нового типа).

10.6.6. Вывод любого другого класса

DeriveAnyClass
С

7.10.1

Разрешить использование любого типа класса в deriving строках.

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

В качестве примера рассмотрим простой класс форматирования вывода SPretty, который выводит красивые строки:

{-# LANGUAGE DefaultSignatures, DeriveAnyClass #-}

class SPretty a where
  sPpr :: a -> String
  default sPpr :: Show a => a -> String
  sPpr = show

Если пользователь не предоставляет ручного имплементирования для sPpr, то он будет по умолчанию show. Теперь мы можем использовать расширение DeriveAnyClass, чтобы легко реализовать экземпляр SPretty для нового типа данных:

data Foo = Foo deriving (Show, SPretty)

Вышеприведенный код эквивалентен:

data Foo = Foo deriving Show
instance SPretty Foo

То есть, экземпляр SPretty Foo будет создан с пустыми реализациями для всех методов. Поскольку в этом примере используется DefaultSignatures, по умолчанию автоматически заполняется реализация sPpr.

Обратите внимание на следующие детали

  • В случае, если вы пытаетесь вывести какой-либо класс для нового типа, и GeneralizedNewtypeDeriving также включен, DeriveAnyClass имеет приоритет.
  • Контекст экземпляра определяется сигнатурами типов методов выводимого класса. Например, если класс:

    class Foo a where
      bar :: a -> String
      default bar :: Show a => a -> String
      bar = show
    
      baz :: a -> a -> Bool
      default baz :: Ord a => a -> a -> Bool
      baz x y = compare x y == EQ
    

    И вы пытаетесь вывести его с помощью DeriveAnyClass:

    instance Eq   a => Eq   (Option a) where ...
    instance Ord  a => Ord  (Option a) where ...
    instance Show a => Show (Option a) where ...
    
    data Option a = None | Some a deriving Foo
    

    то выведенный экземпляр Foo будет:

    instance (Show a, Ord a) => Foo (Option a)
    

    Поскольку сигнатуры типов по умолчанию для bar и baz соответственно требуют ограничений Show a и Ord a.

    Ограничения в сигнатурах типов, отличных от значений по умолчанию, также могут повлиять на вывод контекста экземпляра. Например, если у вас есть такой класс:

    class HigherEq f where
      (==#) :: f a -> f a -> Bool
      default (==#) :: Eq (f a) => f a -> f a -> Bool
      x ==# y = (x == y)
    

    И вы попытались вывести экземпляр для него:

    instance Eq a => Eq (Option a) where ...
    data Option a = None | Some a deriving HigherEq
    

    Тогда он завершится ошибкой следующего содержания:

    No instance for (Eq a)
        arising from the 'deriving' clause of a data type declaration
    

    Это потому, что нам требуется экземпляр Eq (Option a) из сигнатуры типа по умолчанию для (==#), который, в свою очередь, требует экземпляра Eq a , которого у нас нет в области видимости. Но если вы немного измените определение HigherEq:

    class HigherEq f where
      (==#) :: Eq a => f a -> f a -> Bool
      default (==#) :: Eq (f a) => f a -> f a -> Bool
      x ==# y = (x == y)
    

    Тогда становится возможным вывести экземпляр HigherEq Option . Обратите внимание, что единственное различие состоит в том, что теперь сигнатура типа, отличная от значения по умолчанию, для (==#) вводит ограничение Eq a. Ограничения из сигнатур типов, отличных от значений по умолчанию, никогда не отображаются в контексте выведенного экземпляра, но они могут использоваться для выполнения обязательств, требуемых сигнатурами типов по умолчанию. В приведенном выше примере сигнатура типа по умолчанию требовала экземпляра Eq a , а сигнатура, отличная от значения по умолчанию, смогла удовлетворить этот запрос, поэтому выведенный экземпляр имеет вид:

    instance HigherEq Option
    
  • DeriveAnyClass может использоваться с частично примененными классами, такими как

    data T a = MKT a deriving( D Int )
    

    что генерирует

    instance D Int a => D Int (T a) where {}
    
  • DeriveAnyClass может использоваться для заполнения экземпляров по умолчанию для связанных семейств типов:

    {-# LANGUAGE DeriveAnyClass, TypeFamilies #-}
    
    class Sizable a where
      type Size a
      type Size a = Int
    
    data Bar = Bar deriving Sizable
    
    doubleBarSize :: Size Bar -> Size Bar
    doubleBarSize s = 2*s
    

    deriving( Sizable ) эквивалентно тому, чтобы сказать

    instance Sizeable Bar where {}
    

    а затем применятся обычные правила для заполнения связанных типов по умолчанию, что делает Size Bar равным Int.

10.6.7. Стратегии вывода

DerivingStrategies
С

8.2.1

Разрешить несколько deriving, каждый из которых необязательно квалифицируется стратегией.

В большинстве случаев каждое утверждение deriving порождает экземпляр типа класса однозначным способом. Однако существует крайний случай, когда одновременное включение расширений GeneralizedNewtypeDeriving и DeriveAnyClass может сделать вывод неоднозначным. Рассмотрим следующий пример

{-# LANGUAGE DeriveAnyClass, GeneralizedNewtypeDeriving #-}
newtype Foo = MkFoo Bar deriving C

Можно выбрать подход DeriveAnyClass для вывода C или подход GeneralizedNewtypeDeriving для вывода C, оба из которых были бы одинаково допустимыми. GHC по умолчанию отдает предпочтение DeriveAnyClass в таких спорах, но это не удовлетворительное решение, поскольку это лишает пользователей возможности использовать оба расширения языка в одном модуле.

Чтобы сделать это более надежным, GHC имеет понятие стратегий вывода, которые позволяют пользователю явно указать, какой подход использовать при выводе экземпляра. Чтобы включить эту функцию, необходимо включить языковое расширение DerivingStrategies. Стратегия вывода может быть указана в инструкции вывода

newtype Foo = MkFoo Bar
  deriving newtype C

Или в отдельном объявлении вывода

deriving anyclass instance C Foo

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

newtype Baz = Baz Quux
  deriving          (Eq, Ord)
  deriving stock    (Read, Show)
  deriving newtype  (Num, Floating)
  deriving anyclass C

В настоящее время стратегии вывода:

  • stock: Позволяет GHC реализовать «стандартный» экземпляр для типа данных, если это возможно (например, Eq, Ord, Generic, Data, Functor, и т. д.)
  • anyclass: Используйте DeriveAnyClass (см. Вывод любого другого класса)
  • newtype: Use GeneralizedNewtypeDeriving

    (см. Обобщенные экземпляры вывода для newtypes)

  • via: Используйте DerivingVia (см. Вывод через)

10.6.7.1. По умолчанию стратегия вывода

Если явная стратегия вывода не задана, могут применяться несколько стратегий. В этом случае GHC выбирает стратегию следующим образом:

  1. Типы стандартных классов, т.е. те, которые указаны в отчете и те, которые включены с помощью языковых расширений, выводятся с помощью стратегии stock, за исключением следующего:

    • Для newtypes Eq, Ord, Ix и Bounded всегда выводятся с помощью стратегии newtype, даже без включения GeneralizedNewtypeDeriving. (Не должно быть заметных различий в экземплярах, выведенных с помощью стандартной стратегии.)
    • Также для newtypes Functor, Foldable и Enum выводятся с помощью стратегии newtype если GeneralizedNewtypeDeriving включено и вывод завершен успешно.
  2. Для других классов любого типа:

    1. При включенном DeriveAnyClass используется anyclass.
    2. При включенном GeneralizedNewtypeDeriving и выводе для newtype используется newtype.

    Если оба правила применяются к инструкции вывода, используется anyclass, и пользователю выдается предупреждение об неоднозначности. Предупреждение можно избежать, явно указав желаемую стратегию вывода.

10.6.8. Вывод через

DerivingVia
Подразумевает

DerivingStrategies

С

8.6.1

Это позволяет deriving экземпляр класса для типа, указав другой тип с одинаковым представлением во время выполнения (таким образом, существует экземпляр Coercible между двумя типами: см. Ограничение Coercible), который уже является экземпляром этого класса.

DerivingVia указывается с помощью стратегии вывода via. via требует указания другого типа (типа via), чтобы coerce через него. Например, этот код:

{-# LANGUAGE DerivingVia #-}

import Numeric

newtype Hex a = Hex a

instance (Integral a, Show a) => Show (Hex a) where
  show (Hex a) = "0x" ++ showHex a ""

newtype Unicode = U Int
  deriving Show
    via (Hex Int)

-- >>> euroSign
-- 0x20ac
euroSign :: Unicode
euroSign = U 0x20ac

Генерирует следующий экземпляр

instance Show Unicode where
  show :: Unicode -> String
  show = Data.Coerce.coerce
    @(Hex Int -> String)
    @(Unicode -> String)
    show

Это расширение обобщает GeneralizedNewtypeDeriving. Чтобы вывести Num Unicode с GND (deriving newtype Num) он должен повторно использовать экземпляр Num Int. С DerivingVia, мы можем явно указать тип представления Int:

newtype Unicode = U Int
  deriving Num
    via Int

  deriving Show
    via (Hex Int)

euroSign :: Unicode
euroSign = 0x20ac

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

instance (Applicative f, Semigroup a) => Semigroup (f a) ..
instance (Applicative f, Monoid    a) => Monoid    (f a) ..

Мы можем вместо этого создать новый тип App (где App f a и f a представлены одинаково в памяти) и использовать DerivingVia, чтобы явно включить использование этой схемы:

{-# LANGUAGE DerivingVia, DeriveFunctor, GeneralizedNewtypeDeriving #-}

import Control.Applicative

newtype App f a = App (f a) deriving newtype (Functor, Applicative)

instance (Applicative f, Semigroup a) => Semigroup (App f a) where
  (<>) = liftA2 (<>)

instance (Applicative f, Monoid a) => Monoid (App f a) where
  mempty = pure mempty

data Pair a = MkPair a a
  deriving stock
    Functor

  deriving (Semigroup, Monoid)
    via (App Pair a)

instance Applicative Pair where
  pure a = MkPair a a

  MkPair f g <*> MkPair a b = MkPair (f a) (g b)

Обратите внимание, что тип via не обязательно должен быть newtype. Единственное ограничение заключается в том, что он должен быть совместим с исходным типом данных. Это означает, что может быть произвольная вложенность newtypes, как в следующем примере:

newtype Kleisli m a b = (a -> m b)
  deriving (Semigroup, Monoid)
    via (a -> App m b)

Здесь мы используем экземпляр Monoid ((->) a).

При использовании в сочетании с StandaloneDeriving мы меняем порядок экземпляра, на котором мы основываем наш вывод, и экземпляра, который мы определяем, например:

deriving via (a -> App m b) instance Monoid (Kleisli m a b)

10.7. Псевдонимы шаблонов

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, связанное с умными конструкторами, по сути функциями, которые оборачивают валидацию вокруг конструктора и избегают раскрытия его представления. Недостатком является то, что базовый конструктор не может использоваться в качестве матчера. Паттерн-синонимы могут использоваться в качестве подлинных умных конструкторов как для валидации, так и для сопоставления.

В таблице ниже обобщены области применения каждого вида паттерн-синонима.

Контекст

Однонаправленный

Двунаправленный

Явно двунаправленный

Шаблон

Да

Да

Да

Выражение

Нет

Да (Выводится)

Да (Явно)

10.7.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. Точно так же, как и обычный конструктор записи.

Использование

Пример

В качестве конструктора

zero = Point 0 0

В качестве конструктора с синтаксисом записи

zero = Point { x = 0, y = 0}

В контексте шаблона

isZero (Point 0 0) = True

В контексте шаблона с синтаксисом записи

isZero (Point { x = 0, y = 0 }

В контексте шаблона с pun-полями

getX (Point {x}) = x

В обновлении записи

(0, 0) { x = 1 } == (1,0)

Использование селекторов записей

x (0,0) == 0

Для однонаправленного паттерн-синонима записи мы определяем селекторы записей, но не допускаем обновлений или создания записей.

Синтаксис и семантика паттерн-синонимов подробно рассмотрены в следующих подразделах. Более подробная информация содержится в статье.

Дополнительные сведения см. на странице Википедии.

10.7.2. Синтаксис и область действия паттерн-синонимов

Декларация паттерн-синонима может быть однонаправленной, двунаправленной или явно двунаправленной. Синтаксис однонаправленных паттерн-синонимов:

pattern pat_lhs <- pat

синтаксис двунаправленных паттерн-синонимов:

pattern pat_lhs = pat

и синтаксис явно двунаправленных паттерн-синонимов:

pattern pat_lhs <- pat where
  pat_lhs = expr                      -- lhs restricted, see below

Мы можем определить префиксные, инфиксные или паттерн-синонимы записей, изменив форму pat_lhs. Синтаксис для них следующий:

Префикс

Name args

Инфикс

arg1 `Name` arg2 или arg1 op arg2

Запись

Name{arg1,arg2,...,argn}

pat_lhs для явно двунаправленного построения не может использовать синтаксис записи. (Поскольку правая часть expr может строить разные конструкторы данных.) Она может использовать условные выражения с несколькими уравнениями.

Декларации паттерн-синонимов могут встречаться только на верхнем уровне модуля. В частности, они не допускаются в качестве локальных определений.

Переменные в левой части определения связаны шаблоном в правой части. Для двунаправленных паттерн-синонимов все переменные правой части также должны присутствовать в левой части; также не допускаются символьные шаблоны и шаблоны просмотров. Для однонаправленных и явно двунаправленных паттерн-синонимов ограничений на шаблон в правой части нет.

Паттерн-синонимы не могут быть определены рекурсивно.

Полные псевдонимы могут быть указаны, чтобы сообщить проверяющему exhaustiveness сопоставления шаблонов, что набор паттерн-синонимов является полным.

10.7.3. Импорт и экспорт паттерн-синонимов

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

Чтобы экспортировать их самостоятельно, в спецификации экспорта или импорта необходимо префиксровать имена паттернов ключевым словом pattern, например:

module Example (pattern Zero) where

data MyNum = MkNum Int

pattern Zero :: MyNum
pattern Zero = MkNum 0

Без префикса pattern, Zero интерпретировалась бы как строковый конструктор в списке экспорта.

Вы также можете использовать ключевое слово pattern в спецификации импорта/экспорта для импорта или экспорта обычного конструктора данных. Например:

import Data.Maybe( pattern Just )

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

Для объединения паттерн-синонима со строковым конструктором мы перечисляем паттерн-синоним в списке экспорта модуля, который экспортирует строковый конструктор. Например, чтобы объединить 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. Более полную спецификацию можно найти на странице вики.

10.7.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 (который оборачивает GADT S) предоставляет локальное равенство (a~Bool). Если вы укажете явную сигнатуру шаблона P2 :: Bool -> S Bool, тогда P2 станет менее полиморфной и будет вести себя точно так же, как P1, так что g будет отклонена.

    Короче говоря, если вы хотите получить поведение, подобное GADT, для синонимов шаблонов, то (в отличие от конкретных конструкторов данных, таких как S1) вы должны записать его тип с явными предоставленными равенствами. Для конкретного конструктора данных, такого как S1 , вы можете записать его сигнатуру типа как S1 :: Bool -> S Bool или S1 :: (b~Bool) => Bool -> S b; эти два варианта эквивалентны. Не так для синонимов шаблонов: два формата различны, чтобы различать оба указанных выше случая. (См. #9953 для обсуждения этого выбора.)

10.7.5. Сопоставление синонимов шаблонов

Вхождение синонима шаблона в шаблон оценивается путем сначала сопоставления с самим синонимом шаблона, а затем с аргументами шаблонов.

Более точно, семантика сопоставления шаблонов приведена в разделе 3.17 отчета Haskell 2010. К неформальной семантике в разделе 3.17.2 мы добавляем это дополнительное правило:

  • Если шаблон — это шаблон конструктора (P p1 ... pn), где P — это синоним шаблона, определенный P x1 ... xn = p или P x1 ... xn <- p, то:

    1. Сопоставьте значение v с p. Если это сопоставление завершается неудачей или расходится, то и все сопоставление (синонима шаблона) завершается неудачей. В противном случае сопоставление с p должно связать переменные x1 ... xn; пусть они будут связаны со значениями v1 ... vn.
    2. Сопоставьте v1 с p1, v2 с p2 и так далее. Если любое из этих сопоставлений завершается неудачей или расходится, то и все сопоставление завершается неудачей.
    3. Если все сопоставления с 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

10.8. Декларации классов и экземпляров

10.8.1. Декларации классов

В этом разделе и следующем документируются расширения типов GHC. Большое количество информации содержится в статье Type classes: exploring the design space (Simon Peyton Jones, Mark Jones, Erik Meijer).

10.8.1.1. Многопараметрические типы классов

MultiParamTypeClasses
Подразумевает

ConstrainedClassMethods

С

6.8.1

Разрешает определение типов классов с более чем одним параметром.

Многопараметрические типы классов разрешены с расширением MultiParamTypeClasses. Например:

class Collection c a where
    union :: c a -> c a -> c a
    ...etc.

10.8.1.2. Суперклассы декларации класса

FlexibleContexts
С

6.8.1

Разрешает использование сложных ограничений в контекстах декларации классов.

В Haskell 98 контекст декларации класса (который вводит суперклассы) должен быть простым; то есть каждый предикат должен состоять из класса, примененного к переменным типов. Расширение FlexibleContexts (Контекст сигнатуры типа) снимает это ограничение, так что единственное ограничение на контекст в декларации класса состоит в том, что иерархия классов должна быть ациклической. Таким образом, эти декларации классов допустимы:

class Functor (m k) => FiniteMap m k where
  ...

class (Monad m, Monad (t m)) => Transform t m where
  lift :: m a -> (t m) a

Как и в Haskell 98, иерархия классов должна быть ациклической. Однако определение «ациклическая» включает только отношения суперклассов. Например, это допустимо:

class C a where
  op :: D b => a -> b -> b

class C a => D a where ...

Здесь C — суперкласс D, но в порядке вещей, если операция класса op C упоминает D. (Это не будет допустимо, если D будет суперклассом C.)

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

class A cls c where
  meth :: cls c => c -> c

class A B c => B c where

Контекст суперклассов для класса C допустим, если после развертывания синонимов типов до их правых частей и использования классов (кроме C) до их суперклассов, C не встречается синтаксически в контексте.

10.8.1.3. Типы методов класса с ограничениями

ConstrainedClassMethods
С

6.8.1

Разрешает определение дополнительных ограничений на отдельные методы класса.

Haskell 98 запрещает методам класса упоминать ограничения на переменную типа класса, следовательно:

class Seq s a where
  fromList :: [a] -> s a
  elem     :: Eq a => a -> s a -> Bool

Тип elem недопустим в Haskell 98, потому что он содержит ограничение Eq a, которое ограничивает только переменную типа класса (в этом случае a). в этом случае a). Более точно, ограничение в сигнатуре метода класса отклоняется, если

  • Ограничение упоминает как минимум одну переменную типа. Поэтому это разрешено:

    class C a where
      op1 :: HasCallStack => a -> a
      op2 :: (?x::Int) => Int -> a
    
  • Все упоминаемые переменные типов связаны объявлением класса и ни одна не является локально квантифицированной. Примеры:

    class C a where
      op3 :: Eq a => a -> a    -- Rejected: constrains class variable only
      op4 :: D b => a -> b     -- Accepted: constrains a locally-quantified variable `b`
      op5 :: D (a,b) => a -> b -- Accepted: constrains a locally-quantified variable `b`
    

GHC снимает это ограничение с помощью языка расширения ConstrainedClassMethods. Это довольно глупое ограничение в первую очередь, поэтому ConstrainedClassMethods подразумевается MultiParamTypeClasses.

10.8.1.4. Сигнатуры методов по умолчанию

DefaultSignatures
С

7.2.1

Разрешает определение сигнатур методов по умолчанию в определениях классов.

Haskell 98 позволяет определить реализацию по умолчанию при объявлении класса:

class Enum a where
  enum :: [a]
  enum = []

Тип метода enum — [a], и это также тип метода по умолчанию. Вы можете снять это ограничение и задать другой тип для метода по умолчанию, используя расширение DefaultSignatures. Например, если вы написали обобщённую реализацию перечисления в классе GEnum с методом genum в терминах GHC.Generics, вы можете указать метод по умолчанию, использующий эту обобщённую реализацию:

class Enum a where
  enum :: [a]
  default enum :: (Generic a, GEnum (Rep a)) => [a]
  enum = map to genum

Мы повторно используем ключевое слово default для обозначения того, что сигнатура относится только к методу по умолчанию; при определении экземпляров класса Enum исходный тип [a] для enum всё ещё применяется. Однако при создании пустого экземпляра заполняется реализация по умолчанию (map to genum), и она проверяется по типу (Generic a, GEnum (Rep a)) => [a].

Сигнатура типа для метода по умолчанию класса должна иметь ту же форму, что и сигнатура типа соответствующего основного метода. В противном случае, средство проверки типов отклонит определение класса. Под «той же формой» мы подразумеваем, что сигнатура типа по умолчанию должна отличаться от основной сигнатуры типа только своими контекстами. Следовательно, если у вас есть метод bar:

class Foo a where
  bar :: forall b. C => a -> b -> b

Тогда метод по умолчанию для bar должен иметь форму:

default bar :: forall b. C' => a -> b -> b

C может отличаться от C', но правые части сигнатур типов должны совпадать. Мы требуем этого, потому что при объявлении пустого экземпляра для класса, использующего DefaultSignatures, GHC неявно заполняет реализацию по умолчанию таким образом:

instance Foo Int where
  bar = default_bar @Int

Где @Int использует видимое применение типов (Видимое применение типов) для инстанцирования b в default bar :: forall b. C' => a -> b -> b. Чтобы это применение типов работало, сигнатура типа по умолчанию для bar должна иметь тот же порядок переменных типа, что и сигнатура без значения по умолчанию! Но нет обязательства для C и C' быть одинаковыми (см., например, пример Enum выше, который на этом основан).

Для дальнейшего объяснения этого примера правая часть сигнатуры типа по умолчанию для bar должна быть чем-то, что является альфа-эквивалентным к forall b. a -> b -> b (где a связано самим классом и, следовательно, свободно в сигнатурах типов методов). Таким образом, это также будет приемлемая сигнатура типа по умолчанию:

default bar :: forall x. C' => a -> x -> x

Но не это (поскольку свободная переменная a находится в неправильном месте):

default bar :: forall b. C' => b -> a -> b

И не это, так как мы не можем сопоставить переменную типа b с конкретным типом Int:

default bar :: C' => a -> Int -> Int

Однако последний случай заслуживает особого упоминания, так как a -> Int -> Int является простым инстанцированием forall b. a -> b -> b. Вы всё ещё можете написать такую сигнатуру типа по умолчанию, но теперь вы должны использовать равенства типов для этого:

default bar :: forall b. (C', b ~ Int) => a -> b -> b

Мы используем сигнатуры по умолчанию для упрощения обобщённого программирования в GHC (Обобщённое программирование).

10.8.1.5. Нульарные классы типов

NullaryTypeClasses
С

7.8.1

Разрешает использовать определения классов типов без параметров. Это расширение было заменено на MultiParamTypeClasses.

Нульарные (без параметров) классы типов разрешены с MultiParamTypeClasses; исторически они разрешались с (теперь устаревшим) NullaryTypeClasses. Поскольку доступных параметров нет, экземпляров нульарного класса может быть не более одного. Нульарный класс типов может использоваться для документирования некоторых предположений в сигнатуре типа (например, использования гипотезы Римана) или добавления некоторых глобально настраиваемых параметров в программу. Например,

class RiemannHypothesis where
  assumeRH :: a -> a

-- Deterministic version of the Miller test
-- correctness depends on the generalised Riemann hypothesis
isPrime :: RiemannHypothesis => Integer -> Bool
isPrime n = assumeRH (...)

Сигнатура типа isPrime информирует пользователей, что её правильность зависит от не доказанной гипотезы. Если функция используется, пользователь должен признать эту зависимость с:

instance RiemannHypothesis where
  assumeRH = id

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

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”, Mark P. Jones, В Proceedings of the 9th European Symposium on Programming, ESOP 2000, Берлин, Германия, март 2000, Springer-Verlag LNCS 1782, .

10.8.2.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

10.8.2.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, которая может даже находиться в другом модуле.

10.8.2.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.

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

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

Начнём с абстрактного примера, рассмотрим объявление:

class C a b where ...
Jones1999

«Exploring the Design Space for Type-based Implicit Parameterization», Марк П. Джонс, 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 теперь будет помечено как ошибка типа.

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

10.8.3. Объявления экземпляров

Объявление экземпляра имеет вид

instance ( assertion1, ..., assertionn) => class type1 ... typem where ...

Часть перед «=>» — это контекст, а часть после «=>» — это заголовок объявления экземпляра.

10.8.3.1. Разрешение экземпляров

Когда GHC пытается разрешить, скажем, ограничение C Int Bool, он пытается сопоставить каждое объявление экземпляра с ограничением, подставив заголовок объявления экземпляра. Рассмотрим следующие объявления:

instance context1 => C Int a     where ...  -- (A)
instance context2 => C a   Bool  where ...  -- (B)

По умолчанию GHC требует, чтобы ровно один экземпляр соответствовал ограничению, которое он пытается разрешить. Например, ограничение C Int Bool соответствует экземплярам (A) и (B) и, следовательно, будет отклонено; а C Int Char соответствует только (A) и поэтому выбирается (A).

Обратите внимание, что

  • При сопоставлении GHC не учитывает контекст объявления экземпляра (context1 и т.д.).
  • Вполне допустимо, что существует возможность перекрытия (включением как объявлений (A), так и (B), скажем); ошибка сообщается только в том случае, если конкретное ограничение соответствует более чем одному.

См. также Перекрывающиеся экземпляры для флагов, которые ослабляют правила разрешения экземпляров.

10.8.3.2. Расслабленные правила для заголовка экземпляра

TypeSynonymInstances
С

6.8.1

Разрешает определение экземпляров класса типа для синонимов типов.

FlexibleInstances
Подразумевает

TypeSynonymInstances

С

6.8.1

Разрешает определение экземпляров класса типов со произвольными вложенными типами в заголовке экземпляра.

В Haskell 98 заголовок объявления экземпляра должен иметь вид C (T a1 ... an), где C — класс, T — конструктор типа данных, а a1 ... an — различные переменные типа. В случае многопараметрических классов это правило применяется к каждому параметру заголовка экземпляра (можно утверждать, что достаточно, если только один имеет этот вид, а остальные — переменные типа, но на данный момент это правила).

GHC смягчает это правило двумя способами:

  • С расширением TypeSynonymInstances заголовки экземпляров могут использовать синонимы типов. Как всегда, использование синонима типа — просто сокращение для записи правой части определения синонима типа. Например:

    type Point a = (a,a)
    instance C (Point a)   where ...
    

    является законным. Объявление экземпляра эквивалентно

    instance C (a,a) where ...
    

    Как всегда, синонимы типов должны быть полностью применены. Например, вы не можете написать:

    instance Monad Point where ...
    
  • Расширение FlexibleInstances позволяет заголовку объявления экземпляра упоминать произвольные вложенные типы. Например, это становится законным объявлением экземпляра

    instance C (Maybe Int) where ...
    

    См. также правила перекрытия.

    Расширение FlexibleInstances подразумевает TypeSynonymInstances.

Однако, объявление экземпляра должно соответствовать правилам завершения экземпляров: см. Правила завершения экземпляров.

10.8.3.3. Смягченные правила для контекстов экземпляров

В Haskell 98 ограничения класса в контексте объявления экземпляра должны иметь вид C a, где a — переменная типа, которая встречается в заголовке.

Расширение FlexibleContexts смягчает это правило, а также соответствующее правило для сигнатур типов (см. Контекст сигнатуры типа). В частности, FlexibleContexts допускает (хорошо типизированные) ограничения классов вида (C t1 ... tn) в контексте объявления экземпляра.

Обратите внимание, что расширение не влияет на ограничения равенства в контексте экземпляра; они допускаются TypeFamilies или GADTs.

Однако, объявление экземпляра должно соответствовать правилам завершения экземпляров: см. Правила завершения экземпляров.

10.8.3.4. Правила завершения экземпляров

UndecidableInstances
Since

6.8.1

Разрешить определение экземпляров, которые могут привести к невыполнению проверки типов.

Независимо от FlexibleInstances и FlexibleContexts, объявления экземпляров должны соответствовать некоторым правилам, гарантирующим завершение разрешения экземпляров. Ограничения могут быть сняты с помощью UndecidableInstances (см. Неопределяемые экземпляры).

Эти правила:

  1. Условия Патерсона: для каждого ограничения класса (C t1 ... tn) в контексте

    1. Ни одна переменная типа не имеет больше вхождений в ограничении, чем в заголовке
    2. Ограничение имеет меньше конструкторов и переменных (вместе, считая повторения), чем заголовок
    3. Ограничение не упоминает функции типа. Применение функции типа может, в принципе, расшириться до типа произвольного размера, и поэтому они отвергаются сразу
  2. Условие покрытия. Для каждой функциональной зависимости ⟨tvs⟩left -> ⟨tvs⟩right класса, каждая переменная типа в S(⟨tvs⟩right) должна появляться в S(⟨tvs⟩left), где S — подстановка, сопоставляющая каждой переменной типа в объявлении класса соответствующий тип в заголовке экземпляра.

Эти ограничения гарантируют завершение разрешения экземпляров: каждый шаг редукции уменьшает проблему по крайней мере на один конструктор. Вы можете найти много справочного материала о причинах этих ограничений в статье Understanding functional dependencies via Constraint Handling Rules.

Например, это допустимо:

instance C Int [a]          -- Multiple parameters
instance Eq (S [a])         -- Structured type in head

    -- Repeated type variable in head
instance C4 a a => C4 [a] [a]
instance Stateful (ST s) (MutVar s)

    -- Head can consist of type variables only
instance C a
instance (Eq a, Show b) => C2 a b

    -- Non-type variables in context
instance Show (s a) => Show (Sized s a)
instance C2 Int a => C3 Bool [a]
instance C2 Int a => C3 [a] b

Но это не так:

    -- Context assertion no smaller than head
instance C a => C a where ...
    -- (C b b) has more occurrences of b than the head
instance C b b => Foo [b] where ...

Те же ограничения применяются к экземплярам, сгенерированным с помощью deriving положений. Таким образом, следующее принимается:

data MinHeap h a = H a (h a)
  deriving (Show)

потому что производный экземпляр

instance (Show a, Show (h a)) => Show (MinHeap h a)

соответствует вышеупомянутым правилам.

Полезный идиома, допускаемый вышеуказанными правилами, следующий. Если разрешить перекрывающиеся объявления экземпляров, то довольно удобно иметь «экземпляр по умолчанию», который применяется, если нет ничего более конкретного:

instance C a where
  op = ... -- Default

10.8.3.5. Неопределяемые экземпляры

Иногда даже правила завершения экземпляров Правила завершения экземпляров слишком обременительны. Поэтому GHC позволяет экспериментировать с более либеральными правилами: если вы используете экспериментальное расширение UndecidableInstances, как условия Патерсона, так и условие покрытия (описанные в Правила завершения экземпляров) снимаются. Завершение всё ещё гарантируется наличием стека рекурсии с фиксированной глубиной. Если вы превысите глубину стека, вы получите своего рода трассировку стека и возможность увеличить глубину стека с помощью -freduction-depth=⟨n⟩. Однако, если вы превысите предел глубины редукции по умолчанию, лучше всего просто отключить проверку глубины с помощью -freduction-depth=0. Точная глубина, необходимая вашей программе, зависит от деталей вашего кода, и она может меняться между небольшими выпусками GHC. Самый безопасный вариант для релизного кода — если вы уверены, что он должен компилироваться за конечное время — просто отключить проверку.

Например, иногда вы можете использовать следующее, чтобы получить эффект «синонима класса»:

class (C1 a, C2 a, C3 a) => C a where { }

instance (C1 a, C2 a, C3 a) => C a where { }

Это позволяет писать более короткие сигнатуры:

f :: C a => ...

вместо

f :: (C1 a, C2 a, C3 a) => ...

Ограничения на функциональные зависимости (Функциональные зависимости) особенно сложны. Искушение состоит в том, чтобы ввести переменные типа в контекст, которые не появляются в заголовке, что исключается обычными правилами. Например:

class HasConverter a b | a -> b where
   convert :: a -> b

data Foo a = MkFoo a

instance (HasConverter a b,Show b) => Show (Foo a) where
   show (MkFoo value) = show (convert value)

Однако это опасная территория. Вот, например, программа, которая заставит проверку типов циклить:

class D a
class F a b | a->b
instance F [a] [[a]]
instance (D c, F a c) => D [a]   -- 'c' is not mentioned in the head

Аналогично, можно попытаться снять условие покрытия:

class Mul a b c | a b -> c where
  (.*.) :: a -> b -> c

instance Mul Int Int Int where (.*.) = (*)
instance Mul Int Float Float where x .*. y = fromIntegral x * y
instance Mul a b c => Mul a [b] [c] where x .*. v = map (x.*.) v

Третье объявление экземпляра не соответствует условию покрытия; и, действительно, (несколько странное) определение:

f = \ b x y -> if b then x .*. [y] else y

приводит к циклу в выводе экземпляров, потому что оно требует ограничения (Mul a [b] b).

Расширение UndecidableInstances также используется для снятия некоторых ограничений, наложенных на экземпляры семейств типов. См. Определяемость экземпляров синонимов типов.

10.8.3.6. Перекрывающиеся экземпляры

OverlappingInstances

Устаревшее расширение для ослабления проверок, предназначенных для обеспечения завершения разрешения экземпляров.

IncoherentInstances
Since

6.8.1

Устаревшее расширение для ослабления проверок, предназначенных для обеспечения завершения разрешения экземпляров.

В общем случае, как обсуждалось в Разрешение экземпляров, GHC требует, чтобы было однозначно, какой экземпляр должен быть использован для разрешения ограничения класса. GHC также предоставляет способ ослабить разрешение экземпляров, разрешая больше одного экземпляра для соответствия, при условии, что есть наиболее конкретный. Более того, это может быть ещё больше ослаблено, разрешая больше одного экземпляра для соответствия независимо от того, есть ли наиболее конкретный. Этот раздел содержит подробности.

Для управления выбором экземпляра можно указать поведение перекрытия для отдельных экземпляров с помощью псевдонима, написанного сразу после ключевого слова instance. Псевдоним может быть одним из: {-# OVERLAPPING #-}, {-# OVERLAPPABLE #-}, {-# OVERLAPS #-}, или {-# INCOHERENT #-}.

Поведение соответствия также зависит от двух флагов расширений языка на уровне модуля: OverlappingInstances и IncoherentInstances. Эти расширения теперь устарели (с GHC 7.10) в пользу мелкозернистых псевдонимов на уровне экземпляра.

Более точное описание таково. Готовность к перекрытию или несогласованности — это свойство самого объявления экземпляра, управляемое следующим образом:

  • Экземпляр считается несогласованным, если: у него есть INCOHERENT пragma; или если у экземпляра нет pragmy, и он появляется в модуле, скомпилированном с IncoherentInstances.
  • Экземпляр считается перекрываемым, если: у него есть OVERLAPPABLE или OVERLAPS пragma; или если у экземпляра нет pragmy, и он появляется в модуле, скомпилированном с OverlappingInstances; или если экземпляр несогласованный.
  • Экземпляр считается перекрывающимся, если: у него есть OVERLAPPING или OVERLAPS пragma; или если у экземпляра нет pragmy, и он появляется в модуле, скомпилированном с OverlappingInstances; или если экземпляр несогласованный.

Предположим, что в каком-то клиентском модуле мы ищем экземпляр целевого ограничения (C ty1 .. tyn). Поиск выполняется следующим образом:

  • Найдите все экземпляры \(I\), которые соответствуют целевому ограничению; то есть, целевое ограничение является замещающим экземпляром \(I\). Эти объявления экземпляров являются кандидатами.
  • Если кандидатов не осталось, поиск завершается неудачей
  • Удалите любого кандидата \(IX\), для которого существует другой кандидат \(IY\), удовлетворяющий следующим двум условиям:

    • \(IY\) строго более специфичен, чем \(IX\). То есть, \(IY\) является замещающим экземпляром \(IX\), но не наоборот.
    • Либо \(IX\) перекрываемый, либо \(IY\) перекрывающийся. (Такой дизайн «либо/либо», а не «и то, и другое», позволяет клиенту намеренно переопределять экземпляр из библиотеки, не требуя изменения в библиотеке.)
  • Если все оставшиеся кандидаты несогласованы, поиск завершается успешно, возвращая произвольного оставшегося кандидата.
  • Если остается более одного не-несогласованного кандидата, поиск завершается неудачей.
  • В противном случае остается ровно один не-несогласованный кандидат; назовем его «главным кандидатом».
  • Теперь найдите все экземпляры, или входящие в область действия заданные ограничения, которые унифицируются с целевым ограничением, но не соответствуют ему. Такие не-кандидатные экземпляры могут соответствовать, когда целевое ограничение дополнительно инстанцируется. Если все они являются несогласованными экземплярами верхнего уровня, поиск завершается успешно, возвращая главного кандидата. В противном случае поиск завершается неудачей.

Обратите внимание, что эти правила не зависят от настроек флагов в клиентском модуле, где используются экземпляры. Эти правила позволяют автору библиотеки создавать библиотеку, которая полагается на перекрывающиеся экземпляры, без необходимости, чтобы клиент это знал.

Ошибки сообщаются лениво (при попытке решить ограничение), а не жадно (когда сами экземпляры определены). Рассмотрим, например

instance C Int  b where ..
instance C a Bool where ..

Эти потенциально перекрываются, но GHC не будет жаловаться на сами объявления экземпляров, независимо от настроек флагов. Если мы позже попытаемся решить ограничение (C Int Char), то только первый экземпляр соответствует, и все хорошо. Аналогично с (C Bool Bool). Но если мы попытаемся решить (C Int Bool), оба экземпляра соответствуют, и об этом сообщается об ошибке.

В качестве более существенного примера правил в действии рассмотрим

instance {-# OVERLAPPABLE #-} context1 => C Int b     where ...  -- (A)
instance {-# OVERLAPPABLE #-} context2 => C a   Bool  where ...  -- (B)
instance {-# OVERLAPPABLE #-} context3 => C a   [b]   where ...  -- (C)
instance {-# OVERLAPPING  #-} context4 => C Int [Int] where ...  -- (D)

Теперь предположим, что движок вывода типов должен решить ограничение C Int [Int]. Это ограничение соответствует экземплярам (A), (C) и (D), но последний более специфичен и, следовательно, выбирается.

Если (D) не существует, то (A) и (C) все равно будут соответствовать, но ни один не является наиболее специфичным. В этом случае программа будет отклонена, если IncoherentInstances не включено, в противном случае она будет принята, и (A) или (C) будет выбрано произвольно.

Объявление экземпляра является более специфичным, чем другое, если заголовок первого является замещающим экземпляром последнего. Например, (D) «более специфичен», чем (C), потому что вы можете перейти от (C) к (D), заменив a := Int.

Последний пункт (о унификации экземпляров) заставляет GHC быть консервативным при принятии перекрывающегося экземпляра. Например:

f :: [b] -> [b]
f x = ...

Предположим, что из правой части f мы получаем ограничение C b [b]. Но GHC не принимает экземпляр (C), потому что в определенном вызове f, b может быть инстанцировано как Int, в этом случае экземпляр (D) все еще будет более специфичным. Таким образом, GHC отклоняет программу.

Однако, если вы включите расширение IncoherentInstances при компиляции модуля, содержащего (D), GHC вместо этого выберет (C), не жалуясь на проблему последующих инстанциаций.

Обратите внимание, что мы предоставили тип подписи для f, поэтому GHC должен был проверить, что f имеет указанный тип. Предположим, что вместо этого мы не предоставляем тип подписи, попросив GHC вывести его вместо этого. В этом случае GHC воздержится от упрощения ограничения C Int [b] (по той же причине, что и раньше), но вместо отклонения программы он выведет тип

f :: C b [b] => [b] -> [b]

Это откладывает вопрос о том, какой экземпляр выбрать, до места вызова f, к тому времени будет известно больше о типе b. Вы можете написать эту подпись типа самостоятельно, если используете расширение FlexibleContexts.

Точно такая же ситуация может возникнуть в самих объявлениях экземпляров. Предположим, у нас есть

class Foo a where
   f :: a -> a
instance Foo [b] where
   f x = ...

и, как и прежде, ограничение C Int [b] возникает из правой части f. GHC отклонит экземпляр, пожаловавшись, как и раньше, что не знает, как разрешить ограничение C Int [b], потому что оно соответствует более чем одному объявлению экземпляра. Решение заключается в отсрочке выбора, добавив ограничение в контекст объявления экземпляра, таким образом:

instance C Int [b] => Foo [b] where
   f x = ...

(Для этого требуется FlexibleInstances.)

При проверке унификации в последнем пункте GHC также использует «входящие в область действия заданные ограничения». Рассмотрим, например

instance C a Int

g :: forall b c. C b Int => blah
g = ...needs (C c Int)...

Здесь GHC не будет решать ограничение (C c Int) из экземпляра верхнего уровня, потому что определенный вызов g может инстанцировать как b , так и c в один и тот же тип, что позволило бы решить ограничение другим способом. Это последнее ограничение в основном для того, чтобы сделать решатель ограничений полным. (Заинтересованные могут прочитать Note [Instance and Given overlap] в TcInteract.) Его легко избежать: в типе подписи избегайте ограничения, которое соответствует экземпляру верхнего уровня. Флаг -Wsimplifiable-class-constraints предупреждает о таких подписях.

Предупреждение

Перекрывающиеся экземпляры необходимо использовать с осторожностью. Они могут привести к несогласованности (т.е. разные выборы экземпляров делаются в разных частях программы) даже без IncoherentInstances. Рассмотрим:

{-# LANGUAGE OverlappingInstances #-}
module Help where

    class MyShow a where
    myshow :: a -> String

    instance MyShow a => MyShow [a] where
    myshow xs = concatMap myshow xs

    showHelp :: MyShow a => [a] -> String
    showHelp xs = myshow xs

{-# LANGUAGE FlexibleInstances, OverlappingInstances #-}
module Main where
    import Help

    data T = MkT

    instance MyShow T where
    myshow x = "Used generic instance"

    instance MyShow [T] where
    myshow xs = "Used more specific instance"

    main = do { print (myshow [MkT]); print (showHelp [MkT]) }

В функции showHelp GHC не видит перекрывающихся экземпляров и, следовательно, использует экземпляр MyShow [a] без нареканий. При вызове myshow в main, GHC разрешает ограничение MyShow [T] с использованием перекрывающегося объявления экземпляра в модуле Main. В результате программа выводит

"Used more specific instance"
"Used generic instance"

(Альтернативное возможное поведение, в настоящее время не реализованное, заключалось бы в отклонении модуля Help на том основании, что более позднее объявление экземпляра может перекрыть локальное.)

10.8.3.7. Подписи экземпляров: типы подписи в объявлениях экземпляров

InstanceSigs
Since

7.6.1

Разрешить тип подписи для членов в определениях экземпляров.

В Haskell нельзя писать тип подписи в объявлении экземпляра, но иногда это удобно, и расширение языка InstanceSigs позволяет это сделать. Например:

data T a = MkT a a
instance Eq a => Eq (T a) where
  (==) :: T a -> T a -> Bool   -- The signature
  (==) (MkT x1 x2) (MkTy y1 y2) = x1==y1 && x2==y2

Некоторые детали

  • Тип подписи в объявлении экземпляра должен быть более полиморфным (или таким же), как и в объявлении класса, инстанцированном с типом экземпляра. Например, это нормально:

    instance Eq a => Eq (T a) where
       (==) :: forall b. b -> b -> Bool
       (==) x y = True
    

    Здесь подпись в объявлении экземпляра более полиморфна, чем требуется для инстанцированного метода класса.

  • Код метода в объявлении экземпляра проверяется по типу подписи, предоставленной в объявлении экземпляра, как вы и ожидаете. Поэтому, если подпись экземпляра более полиморфна, чем требуется, код тоже должен быть.
  • Одна стилистическая причина для написания типа подписи — простая документация. Другая — вы можете ввести в область видимости переменные типа с областью действия. Например:

    class C a where
      foo :: b -> a -> (a, [b])
    
    instance C a => C (T a) where
      foo :: forall b. b -> T a -> (T a, [b])
      foo x (T y) = (T y, xs)
         where
           xs :: [b]
           xs = [x,x,x]
    

    При условии, что вы также укажете ScopedTypeVariables (Переменные типа с лексическим объявлением области видимости), forall b действует на определение foo, и, в частности, на подпись типа для xs.

10.8.4. Перегруженные строковые литералы

OverloadedStrings
Since

6.8.1

Включить перегруженные строковые литералы (например, строковые литералы, распакованные с помощью класса IsString).

GHC поддерживает перегруженные строковые литералы. Обычно строковый литерал имеет тип String, но при включённых перегруженных строковых литералах (с помощью OverloadedStrings) строковый литерал имеет тип (IsString a) => a.

Это означает, что обычный синтаксис строк может использоваться, например, для ByteString, Text, и других вариаций типов строк. Строковые литералы ведут себя очень похоже на целочисленные литералы, то есть их можно использовать как в выражениях, так и в шаблонах. Если строковый литерал используется в шаблоне, он будет заменён на проверку на равенство, так же как и целочисленный литерал.

Класс IsString определён как:

class IsString a where
    fromString :: String -> a

Единственный предопределённый экземпляр — очевидный, чтобы строки работали как обычно:

instance IsString [Char] where
    fromString cs = cs

Класс IsString по умолчанию не находится в области видимости. Если вы хотите явно упомянуть его (например, для объявления экземпляра), вы можете импортировать его из модуля Data.String.

Механизм по умолчанию Haskell (Отчёт по Haskell, раздел 4.3.4) расширен для покрытия строковых литералов, когда указано OverloadedStrings. В частности:

  • Каждый тип в объявлении default должен быть экземпляром Num или IsString.
  • Если объявление default не дано, то это будет так, как будто модуль содержит объявление default( Integer, Double, String).
  • Стандартное правило по умолчанию расширено следующим образом: по умолчанию применяются, когда все нерешённые ограничения связаны со стандартными классами или IsString; и по крайней мере одно из них — числовой класс или IsString.

Например, выражение length "foo" приведёт к неоднозначному использованию IsString a0, которое, в соответствии с вышеуказанными правилами, будет по умолчанию равно String.

Небольшой пример:

module Main where

import Data.String( IsString(..) )

newtype MyString = MyString String deriving (Eq, Show)
instance IsString MyString where
    fromString = MyString

greet :: MyString -> MyString
greet "hello" = "world"
greet other = other

main = do
    print $ greet "hello"
    print $ greet "fool"

Обратите внимание, что вывод Eq необходим для работы сопоставления с образцом, поскольку он преобразуется в сравнение на равенство.

10.8.5. Перегруженные метки

OverloadedLabels
Since

8.0.1

Включить использование синтаксиса перегруженных меток #foo.

GHC поддерживает перегруженные метки, вид идентификаторов, чьё толкование может зависеть как от его типа, так и от его текстового значения. При включении расширения OverloadedLabels перегруженную метку можно записать с префиксом решётка, например #foo. Тип этого выражения IsLabel "foo" a => a.

Класс IsLabel определён как:

class IsLabel (x :: Symbol) a where
  fromLabel :: a

Это довольно похоже на класс IsString (см. Перегруженные строковые литералы), но с дополнительным типом параметра, который делает текст метки доступным как строка на уровне типа (см. Литералы на уровне типа). Обратите внимание, что fromLabel имел дополнительный аргумент Proxy# x в GHC 8.0, но это было удалено в GHC 8.2, так как вместо этого можно использовать применение типа (см. Видимое применение типа).

Нет предопределённых экземпляров этого класса. Он не находится в области видимости по умолчанию, но может быть включён в область видимости импортом GHC.OverloadedLabels. В отличие от IsString, для IsLabel нет специальных правил по умолчанию.

Во время проверки типов GHC заменит появление перегруженной метки, например #foo на fromLabel @"foo". Это будет иметь некоторый тип alpha и потребует решения ограничения класса IsLabel "foo" alpha.

Целью IsLabel является поддержка перегруженных полей записей и, возможно, анонимных записей. Таким образом, в будущем ему могут быть даны экземпляры для базовых типов данных (в частности, (->)).

Если включено RebindableSyntax, перегруженные метки будут разложены с использованием функции fromLabel, которая находится в области видимости, а не всегда GHC.OverloadedLabels.fromLabel.

При написании перегруженной метки не должно быть пробела между знаком решётки и последующим идентификатором. Расширение MagicHash использует знаки решётки в качестве постфикса; если включены и OverloadedLabels, и MagicHash, то x#y означает x# y, но если включён только OverloadedLabels, то он означает x #y.

Расширение UnboxedTuples делает (# одним лексемой, поэтому при включенном UnboxedTuples между открывающей скобкой и перегруженной меткой необходимо вставить пробел. Для избежания путаницы настоятельно рекомендуется вставлять пробел перед знаком решётки при использовании OverloadedLabels.

При использовании OverloadedLabels (или других расширений, которые используют знаки решётки) в файле .hsc (см. Написание интерфейсов Haskell для кода C: hsc2hs) знаки решётки должны быть удвоены (напишите ##foo вместо #foo), чтобы они не обрабатывались как директивы hsc2hs.

Ниже приведён пример расширения примера доступа к записи в Литералы на уровне типа, демонстрирующий, как перегруженная метка может быть использована в качестве селектора записи:

{-# LANGUAGE DataKinds, KindSignatures, MultiParamTypeClasses,
             FunctionalDependencies, FlexibleInstances,
             OverloadedLabels, ScopedTypeVariables #-}

import GHC.OverloadedLabels (IsLabel(..))
import GHC.TypeLits (Symbol)

data Label (l :: Symbol) = Get

class Has a l b | a l -> b where
  from :: a -> Label l -> b

data Point = Point Int Int deriving Show

instance Has Point "x" Int where from (Point x _) _ = x
instance Has Point "y" Int where from (Point _ y) _ = y

instance Has a l b => IsLabel l (a -> b) where
  fromLabel x = from x (Get :: Label l)

example = #x (Point 1 2)

10.8.6. Перегруженные списки

OverloadedLists
Since

7.8.1

Включить перегруженный синтаксис списков (например, развёртывание списков через класс IsList).

GHC поддерживает перегрузку списочного обозначения. Давайте вспомним обозначение для построения списков. В Haskell списочное обозначение можно использовать семью способами:

[]          -- Empty list
[x]         -- x : []
[x,y,z]     -- x : y : z : []
[x .. ]     -- enumFrom x
[x,y ..]    -- enumFromThen x y
[x .. y]    -- enumFromTo x y
[x,y .. z]  -- enumFromThenTo x y z

Когда включено расширение OverloadedLists вышеупомянутые семь способов записи будут развёрнуты следующим образом:

[]          -- fromListN 0 []
[x]         -- fromListN 1 (x : [])
[x,y,z]     -- fromListN 3 (x : y : z : [])
[x .. ]     -- fromList (enumFrom x)
[x,y ..]    -- fromList (enumFromThen x y)
[x .. y]    -- fromList (enumFromTo x y)
[x,y .. z]  -- fromList (enumFromThenTo x y z)

Это расширение позволяет программистам использовать списочное обозначение для построения структур, таких как: Set, Map, IntMap, Vector, Text и Array. Следующий фрагмент кода приводит несколько примеров:

['0' .. '9']             :: Set Char
[1 .. 10]                :: Vector Int
[("default",0), (k1,v1)] :: Map String Int
['a' .. 'z']             :: Text

Шаблоны списков также перегружены. При включении расширения OverloadedLists эти определения будут развёрнуты следующим образом

f [] = ...          -- f (toList -> []) = ...
g [x,y,z] = ...     -- g (toList -> [x,y,z]) = ...

(Здесь мы используем синтаксис шаблонов просмотра для преобразования, см. Шаблоны просмотра.)

10.8.6.1. Класс IsList

В вышеприведённых развёртываниях функции toList, fromList и fromListN — все методы класса IsList, который сам экспортируется из модуля GHC.Exts.

Класс типов определён следующим образом:

class IsList l where
  type Item l

  fromList :: [Item l] -> l
  toList   :: l -> [Item l]

  fromListN :: Int -> [Item l] -> l
  fromListN _ = fromList

Класс IsList и его методы предназначены для совместного использования с расширением OverloadedLists.

  • Функция типа Item возвращает тип элементов структуры l.
  • Функция fromList строит структуру l из данного списка Item l.
  • Функция fromListN принимает длину входного списка в качестве подсказки. Её поведение должно быть эквивалентно fromList. Подсказка может быть использована для более эффективного построения структуры l по сравнению с fromList. Если заданная подсказка не равна длине входного списка, поведение fromListN не определено.
  • Функция toList должна быть обратной к fromList.

Совершенно нормально объявлять новые экземпляры IsList, чтобы списочное обозначение стало полезным для совершенно новых типов данных. Вот несколько примеров экземпляров:

instance IsList [a] where
  type Item [a] = a
  fromList = id
  toList = id

instance (Ord a) => IsList (Set a) where
  type Item (Set a) = a
  fromList = Set.fromList
  toList = Set.toList

instance (Ord k) => IsList (Map k v) where
  type Item (Map k v) = (k,v)
  fromList = Map.fromList
  toList = Map.toList

instance IsList (IntMap v) where
  type Item (IntMap v) = (Int,v)
  fromList = IntMap.fromList
  toList = IntMap.toList

instance IsList Text where
  type Item Text = Char
  fromList = Text.pack
  toList = Text.unpack

instance IsList (Vector a) where
  type Item (Vector a) = a
  fromList  = Vector.fromList
  fromListN = Vector.fromListN
  toList = Vector.toList

10.8.6.2. Переопределяемый синтаксис

При развёртывании списочного обозначения с OverloadedLists GHC использует методы fromList (и т. д.) из модуля GHC.Exts.

Однако если вы используете RebindableSyntax, GHC вместо этого использует то, что находится в области видимости, с именами toList, fromList и fromListN.

То есть, эти функции переопределяемы; см. Переопределяемый синтаксис и неявный импорт Prelude.

10.8.6.3. По умолчанию

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

default ([a])

10.8.6.4. Предположения о будущем

Текущая реализация расширения OverloadedLists может быть улучшена путём специального обработки списков, заполненных только литералами. Более конкретно, компилятор мог бы статически выделять такие списки с использованием компактного представления и позволить экземплярам IsList использовать это компактное представление. Обладая этой возможностью, расширение OverloadedLists будет хорошо подготовлено к тому, чтобы поглотить расширение OverloadedStrings (в настоящее время строковые литералы, как особый случай, получают выгоду от статически выделенного компактного представления).

10.8.7. Неразрешимые (или рекурсивные) суперклассы

UndecidableSuperClasses
С тех пор

8.0.1

Разрешить все ограничения на суперклассы, включая те, которые могут привести к не завершению работы типового проверяющего.

Расширение языка UndecidableSuperClasses позволяет намного более гибкие ограничения в суперклассах.

Класс не может, как правило, иметь себя в качестве суперкласса. Поэтому это незаконно

class C a => D a where ...
class D a => C a where ...

GHC реализует этот тест консервативно, когда участвуют функции типов или переменные типов. Например

type family F a :: Constraint
class F a => C a where ...

GHC будет жаловаться на это, потому что вы можете позже добавить

type instance F Int = C Int

и теперь мы попадем в цикл суперклассов. Вот пример с участием переменной типа

class f (C f) => C f
class c       => Id c

Если мы расширили суперклассы C Id , то получим сначала Id (C Id) и затем C Id снова.

Но такие ограничения суперклассов иногда полезны, а консервативная проверка раздражает, когда фактической рекурсии нет.

Кроме того, по-настоящему рекурсивные суперклассы иногда полезны. Вот пример из реальной жизни (#10318)

class (Frac (Frac a) ~ Frac a,
       Fractional (Frac a),
       IntegralDomain (Frac a))
    => IntegralDomain a where
 type Frac a :: Type

Здесь цикл суперклассов завершается, но не совсем очевидно, что он это делает.

С расширением языка UndecidableSuperClasses GHC снимает все ограничения на ограничения суперклассов. Если действительно есть цикл, GHC будет расширять его только до конечной глубины.

10.9. Семейства типов

TypeFamilies
Подразумевает

MonoLocalBinds, KindSignatures, ExplicitNamespaces

С тех пор

6.8.1

Разрешить использование и определение индексированных типов и семейств данных.

Индексированные семейства типов — это расширение, предназначенное для облегчения программирования на уровне типов. Семейства типов представляют собой обобщение ассоциированных типов данных [AssocDataTypes2005] и ассоциированных синонимов типов [AssocTypeSyn2005]. Сами семейства типов описаны в Schrijvers 2008 [TypeFamilies2008]. Семейства типов по сути предоставляют индексированные типы данных и именованные функции над типами, которые полезны для обобщенного программирования и высокопараметризованных интерфейсов библиотек, а также интерфейсов с улучшенной статической информацией, очень похожие на зависимые типы. Они также могут рассматриваться как альтернатива функциональным зависимостям, но предоставляют более функциональный стиль программирования на уровне типов, чем реляционный стиль функциональных зависимостей.

Индексированные семейства типов, или семейства типов вкратце, — это конструкторы типов, которые представляют множества типов. Члены множества обозначаются путём предоставления конструктору семейства типов параметров типа, которые называются индексами типа. Разница между обычными параметризованными конструкторами типов и конструкторами семейств очень похожа на разницу между параметрически полиморфными функциями и (ад-хок полиморфными) методами классов типов. Параметрически полиморфные функции ведут себя одинаково во всех экземплярах типов, тогда как методы класса могут изменять своё поведение в зависимости от параметров типа класса. Аналогично, обычные конструкторы типов подразумевают одинаковое представление данных для всех экземпляров типов, но конструкторы семейств могут иметь различные типы представления для различных индексов типов.

Индексированные семейства типов бывают трёх видов: семейства данных, открытые семейства синонимов типов и закрытые семейства синонимов типов. Это индексированные варианты алгебраических типов данных и синонимов типов соответственно. Экземпляры семейств данных могут быть типами данных и newtype.

Семейства типов включаются с помощью расширения языка TypeFamilies. Дополнительную информацию об использовании семейств типов в GHC можно найти на странице вики-проекта Haskell по семействам типов.

AssocDataTypes2005

«Associated Types with Class», M. Chakravarty, G. Keller, S. Peyton Jones, and S. Marlow. В материалах «32-го ежегодного симпозиума ACM SIGPLAN-SIGACT по принципам программирования (POPL’05)», страницы 1-13, ACM Press, 2005.

AssocTypeSyn2005

«Type Associated Type Synonyms”. M. Chakravarty, G. Keller, and S. Peyton Jones. В материалах «Десятой международной конференции ACM SIGPLAN по функциональному программированию», ACM Press, страницы 241-253, 2005.

TypeFamilies2008

«Type Checking with Open Type Functions”, T. Schrijvers, S. Peyton-Jones, M. Chakravarty, and M. Sulzmann, в материалах «ICFP 2008: 13-я международная конференция ACM SIGPLAN по функциональному программированию», ACM Press, страницы 51-62, 2008.

10.9.1. Семейства данных

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

10.9.1.1. Объявления семейств данных

Индексированные семейства данных вводятся с помощью сигнатуры, например

data family GMap k :: Type -> Type

Специальный family отличает семейства от обычных объявлений данных. Аннотация результативного типа необязательна и, как обычно, по умолчанию устанавливается в Type при её отсутствии. Пример

data family Array e

Имена аргументов также могут иметь явные сигнатуры типов, если это необходимо. Так же как и с объявлениями GADTs, именованные аргументы полностью необязательны, так что мы можем объявить Array альтернативно с

data family Array :: Type -> Type

В отличие от обычных определений данных, результативный тип семейства данных не обязательно должен быть Type. Он может быть также:

  • В форме TYPE r для некоторого r (см. Полиморфизм легкости). Например:

    data family DF1 :: TYPE IntRep
    data family DF2 (r :: RuntimeRep)  :: TYPE r
    data family DF3 :: Type -> TYPE WordRep
    
  • Голым именем переменной типа (при включённом PolyKinds). Например:

    data family DF4 :: k
    data family DF5 (a :: k) :: k
    data family DF6 :: (k -> Type) -> k
    

Однако типы экземпляров данных должны заканчиваться Type. Это ограничение несколько ослаблено при включении расширения UnliftedNewtypes, так как оно позволяет типу newtype instance заканчиваться TYPE r для некоторого r.

10.9.1.2. Объявления экземпляров данных

Объявления экземпляров семейств данных и newtype очень похожи на стандартные объявления данных и newtype. Единственные два различия заключаются в том, что ключевое слово data или newtype следует за instance, и что некоторые или все аргументы типа могут быть не переменными типами, но не могут содержать универсальные типы или семейства синонимов типов. Однако семейства данных, как правило, допускаются в параметрах типа, а синонимы типов допускаются, пока они полностью применены и расширяются до типа, который сам по себе допустим — точно так же, как это требуется для случаев синонимов типов в параметрах экземпляров классов. Например, экземпляр Either для GMap

data instance GMap (Either a b) v = GMapEither (GMap a v) (GMap b v)

В этом примере объявление имеет только один вариант. В общем случае их может быть любое количество.

Когда ExplicitForAll включено, переменные типа и вида, используемые в левой части, могут быть явно связаны. Например:

data instance forall a (b :: Proxy a). F (Proxy b) = FProxy Bool

Когда присутствует явная forall, все переменные типов и видов, упомянутые, но не имеющие области видимости, должны быть связаны forall:

data instance forall (a :: k). F a = FOtherwise — отклонено: k не в области видимости data instance forall k (a :: k). F a = FOtherwise — принято

Когда включён флаг -Wunused-type-patterns, переменные типа, упомянутые в шаблонах слева, но не используемые в правой части, сообщаются об ошибках. Переменные, которые встречаются несколько раз в левой части, также считаются используемыми. Чтобы подавить предупреждения, неиспользуемые переменные должны быть либо заменены, либо иметь префикс подчёркиванием. Переменные типа, начинающиеся с подчёркивания (_x) в противном случае обрабатываются как обычные переменные типа.

Это напоминает подстановочные знаки, которые могут быть использованы в Частичных сигнатурах типов. Однако есть некоторые различия. Не генерируются сообщения об ошибках, сообщающие о выведенных типах, и расширение PartialTypeSignatures не оказывает никакого влияния.

Явно связанная переменная типа или рода с помощью ExplicitForAll, но не используемая в левой части, вызовет ошибку, а не предупреждение.

Объявления экземпляров данных и newtype допускаются только при наличии соответствующего объявления семейства — так же, как объявление экземпляра класса требует видимости объявления класса. Более того, каждое объявление экземпляра должно соответствовать роду, определяемому объявлением семейства. Это означает, что количество параметров в объявлении экземпляра соответствует арности, определяемой родом семейства.

Объявление экземпляра семейства данных может использовать всю выразительность обычных data или newtype объявлений:

  • Хотя семейство данных вводится с ключевым словом «data», экземпляр семейства данных может использовать data или newtype. Например:

    data family T a
    data    instance T Int  = T1 Int | T2 Bool
    newtype instance T Char = TC Bool
    
  • Семейство data instance может использовать синтаксис GADT для конструкторов данных и, по сути, может определять GADT. Например:

    data family G a b
    data instance G [a] b where
       G1 :: c -> G [Int] b
       G2 :: G [a] Bool
    
  • Вы можете использовать deriving клаузу в объявлении data instance или newtype instance.

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

data family T a
data instance T Int  = A
data instance T Char = B
foo :: T a -> Int
foo A = 1
foo B = 2

Вместо этого вам нужно будет написать foo как операцию класса, таким образом:

class Foo a where
  foo :: T a -> Int
instance Foo Int where
  foo A = 1
instance Foo Char where
  foo B = 2

Учитывая функциональность, предоставляемую GADTs (обобщенными алгебраическими типами данных), может показаться, что определение, такое как выше, должно быть осуществимым. Однако семейства типов, в отличие от GADTs, являются открытыми; т. е., новые экземпляры всегда могут быть добавлены, возможно, в других модулях. Поддержка сопоставления с образцом через различные экземпляры данных потребовала бы формы расширяемого конструктора case.

10.9.1.3. Перекрытие экземпляров данных

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

10.9.2. Семейства синонимов

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

10.9.2.1. Объявления семейств типов

Открытые индексированные семейства типов вводятся с помощью сигнатуры, такой как

type family Elem c :: Type

Специальный family отличает семейство от стандартных объявлений типов. Аннотация рода результата необязательна и, как обычно, по умолчанию равна Type при опущении. Пример:

type family Elem c

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

type family F a b :: Type -> Type
  -- F's arity is 2,
  -- although its overall kind is Type -> Type -> Type -> Type

Учитывая это объявление, ниже приведены примеры корректных и некорректных типов:

F Char [Int]       -- OK!  Kind: Type -> Type
F Char [Int] Bool  -- OK!  Kind: Type
F IO Bool          -- WRONG: kind mismatch in the first argument
F Bool             -- WRONG: unsaturated application

Аннотация рода результата необязательна и по умолчанию равна Type (как и роды аргументов) при опущении. Полиродные семейства типов могут быть объявлены с использованием параметра в аннотации рода:

type family F a :: k

В этом случае параметр рода k фактически является неявным параметром семейства типов.

В месте определения арность определяет, какие входные данные можно сопоставить:

data PT (a :: Type)

type family F1 :: k -> Type
type instance F1 = PT
  -- OK, 'k' can be matched on.

type family F0 :: forall k. k -> Type
type instance F0 = PT
  -- Error:
  --   • Expected kind ‘forall k. k -> Type’,
  --       but ‘PT’ has kind ‘Type -> Type’
  --   • In the type ‘PT’
  --     In the type instance declaration for ‘F0’

Как F1, так и F0 имеют род forall k. k -> Type, но их арности различаются.

В местах использования арность определяет, можно ли использовать определение в высокоранговой ситуации:

type HRK (f :: forall k. k -> Type) = (f Int, f Maybe, f True)

type H1 = HRK F0  -- OK
type H2 = HRK F1
  -- Error:
  --   • Expected kind ‘forall k. k -> Type’,
  --       but ‘F1’ has kind ‘k0 -> Type’
  --   • In the first argument of ‘HRK’, namely ‘F1’
  --     In the type ‘HRK F1’
  --     In the type declaration for ‘H2’

Это следствие требования, что все применения семейства типов должны быть полностью насыщены относительно их арности.

10.9.2.2. Объявления экземпляров типов

Объявления экземпляров семейств типов очень похожи на стандартные объявления синонимов типов. Единственные два различия заключаются в том, что ключевое слово type следует за instance, и что некоторые или все аргументы типа могут быть типами без переменных, но не могут содержать универсальные типы или семейства синонимов типов. Тем не менее, семейства данных, как правило, разрешены, и синонимы типов разрешены, пока они полностью применены и развернуты в тип, который допустим — это те же самые требования, что и для экземпляров данных. Например, экземпляр [e] для Elem:

type instance Elem [e] = e

Аргументы типа можно заменить подчеркиваниями (_) если имена аргументов не важны. Это то же самое, что и запись переменных типа с уникальными именами. Неиспользуемые аргументы типа можно заменить или добавить подчеркивания, чтобы избежать предупреждений, когда включен флаг -Wunused-type-patterns. Применяются те же правила, что и для Объявлений экземпляров данных.

Также, так же, как и для Объявлений экземпляров данных, при включении ExplicitForAll переменные типа и рода могут быть явно связаны в объявлении экземпляра типа.

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

10.9.2.3. Закрытые семейства типов

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

type family F a where
  F Int  = Double
  F Bool = Char
  F a    = String

Уравнения закрытого семейства типов проверяются по порядку сверху вниз при упрощении применения семейства типов. В этом примере мы объявляем экземпляр для F, такой что F Int упрощается до Double, F Bool упрощается до Char, и для любого другого типа a, о котором известно, что он не является Int или Bool, F a упрощается до String. Обратите внимание, что GHC должен убедиться, что a не может быть унифицирован с Int или Bool в последнем случае; если программист укажет только F a в своём коде, GHC не сможет упростить тип. В конце концов, a может быть позже инстанцирован с Int.

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

type family R a where
  forall t a. R (t a) = [a]
  forall a.   R a     = a

Закрытое семейство типов может быть объявлено без уравнений. Такие закрытые семейства типов являются неявными определениями на уровне типов, которые никогда не будут упрощаться, не обязательно инъективны (в отличие от пустых типов данных) и не могут иметь никаких экземпляров. Это отличается от пропуска уравнений закрытого семейства типов в файле hs-boot, который использует синтаксис where .., поскольку в этом случае уравнения могут или не могут быть указаны в файле hs.

10.9.2.4. Примеры семейств типов

Ниже приведены примеры допустимых и недопустимых экземпляров типов:

type family F a :: Type
type instance F [Int]   = Int   -- OK!
type instance F String  = Char  -- OK!
type instance F (F a)   = a     -- WRONG: type parameter mentions a type family
type instance
  F (forall a. (a, b))  = b     -- WRONG: a forall type appears in a type parameter
type instance
  F Float = forall a.a          -- WRONG: right-hand side may not be a forall type
type family H a where          -- OK!
  H Int  = Int
  H Bool = Bool
  H a    = String
type instance H Char = Char    -- WRONG: cannot have instances of closed family
type family K a where          -- OK!

type family G a b :: Type -> Type
type instance G Int            = (,)     -- WRONG: must be two type parameters
type instance G Int Char Float = Double  -- WRONG: must be two type parameters

10.9.2.5. Совместимость и обособленность уравнений семейств типов

Должны быть некоторые ограничения на уравнения семейств типов, чтобы мы не определили неоднозначную систему переписывания. Таким образом, уравнения открытых семейств типов ограничены совместимостью. Два шаблона типов совместимы, если

  1. все соответствующие типы и неявные роды в шаблонах отделены друг от друга, или
  2. два шаблона унифицируются, создавая подстановку, и правые части равны при этой подстановке.

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

Первый пункт «совместимости» более понятен. Он говорит, что шаблоны двух различных экземпляров семейства типов не могут перекрываться. Например, следующее запрещено:

type instance F Int = Bool
type instance F Int = Char

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

type instance F (a, Int) = [a]
type instance F (Int, b) = [b]   -- overlap permitted

type instance G (a, Int)  = [a]
type instance G (Char, a) = [a]  -- ILLEGAL overlap, as [Char] /= [Int]

Обратите внимание, что это условие совместимости не зависит от того, связано ли семейство типов, и это не только вопрос согласованности, но и безопасности типов.

Для поликиндной семейства типов, кинды проверяются на различие так же, как и типы. Например, следующее принимается:

type family J a :: k
type instance J Int = Bool
type instance J Int = Maybe

Эти экземпляры совместимы, потому что они различаются по своему неявному параметру кинда; первый использует Type, а второй использует Type -> Type.

Определение «совместимости» использует понятие «различие», определение которого, в свою очередь, опирается на редукцию семейства типов. Это условие «различия», как заявлено, невозможно проверить, поэтому мы используем это консервативное приближение: два типа считаются разными, когда два типа не могут быть унифицированы, даже бесконечным унификатором. Разрешение бесконечного унификатора запрещает следующую пару экземпляров:

type instance H x   x = Int
type instance H [x] x = Bool

Шаблоны типов в этой паре равны, если x заменен на бесконечное вложение списков. Отклонение таких экземпляров необходимо для типабезопасности.

Совместимость также влияет на закрытые семейства типов. При упрощении применения закрытого семейства типов GHC выберет уравнение только тогда, когда уверен, что ни одно несовместимое предыдущее уравнение никогда не будет применено. Вот несколько примеров:

type family F a where
  F Int = Bool
  F a   = Char

type family G a where
  G Int = Int
  G a   = a

В определении для F, два уравнения несовместимы — их шаблоны не различаются, а их правые части не совпадают. Таким образом, прежде чем GHC выберет второе уравнение, он должен быть уверен, что первое никогда не может быть применено. Поэтому тип F a не упрощается; только тип, такой как F Double упростится до Char. В G, с другой стороны, два уравнения совместимы. Таким образом, GHC может игнорировать первое уравнение при рассмотрении второго. Поэтому G a упростится до a.

Несовместимости между уравнениями закрытых семейств типов могут быть показаны в :info, когда -fprint-axiom-incomps включено.

Однако см. Типы, классы и другие объявления для правил перекрытия в GHCi.

10.9.2.6. Детерминируемость экземпляров синонимов типов

UndecidableInstances

Снять ограничения на детерминируемость экземпляров семейства синонимов типов.

Для обеспечения того, чтобы вывод типов в присутствии семейств типов был детерминированным, нам необходимо установить ряд дополнительных ограничений на формирование объявлений экземпляров типов (см. определение 5 (Упрощенные условия) «Проверка типов с открытыми функциями типов»). Объявления экземпляров имеют общий вид

type instance F t1 .. tn = t

где мы требуем, чтобы для каждого применения семейства типов (G s1 .. sm) в t,

  1. s1 .. sm не содержали конструкторов семейств типов,
  2. общее количество символов (конструкторы типов данных и переменные типов) в s1 .. sm строго меньше, чем в t1 .. tn, и
  3. для каждой переменной типа a, a встречается в s1 .. sm не чаще, чем в t1 .. tn.

Эти ограничения легко проверяются и гарантируют завершение вывода типов. Однако они не достаточны для обеспечения полноты вывода типов в присутствии так называемых «циклических равенств», таких как a ~ [F a], где рекуррентное вхождение переменной типа находится под применением семейства и применением конструктора данных — см. упомянутую выше статью для получения подробностей.

Если опция UndecidableInstances передается компилятору, вышеуказанные ограничения не применяются, и программист должен обеспечить завершение нормализации семейств типов во время вывода типов.

10.9.3. Подстановочные знаки слева от экземпляров данных и семейств типов

Когда имя аргумента типа данных или объявления экземпляра семейства типов не имеет значения, его можно заменить подчёркиванием (_). Это то же самое, что запись переменной типа с уникальным именем.

data family F a b :: Type
data instance F Int _ = Int
-- Equivalent to  data instance F Int b = Int

type family T a :: Type
type instance T (a,_) = a
-- Equivalent to  type instance T (a,b) = a

Это использование подчёркивания для подстановочных знаков в шаблоне типа точно такое же, как сопоставление с образцом в языке терминов, но значительно отличается от использования подчёркивания в частичной сигнатуре типа (см. Подстановочные знаки типов).

Переменная типа, начинающаяся с подчёркивания, не обрабатывается специально в объявлении экземпляра типа или данных. Например:

data instance F Bool _a = _a -> Int
-- Equivalent to  data instance F Bool a = a -> Int

Противопоставьте это специальной обработке именованных подстановочных знаков в сигнатурах типов (Именованные подстановочные знаки).

10.9.4. Связанные типы данных и семейства типов

Семейство типов данных или синонимов типов может быть объявлено в рамках класса типов, например:

class GMapKey k where
  data GMap k :: Type -> Type
  ...

class Collects ce where
  type Elem ce :: Type
  ...

При этом мы (по желанию) можем опустить ключевое слово «family».

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

class C a b c where
  type T c a x :: Type

Здесь c и a являются параметрами класса, но тип также индексируется по третьему параметру x.

10.9.4.1. Связанные экземпляры

Когда связанный тип данных или синоним семейства типов объявляется внутри экземпляра класса типов, мы (по желанию) можем опустить ключевое слово instance в экземпляре семейства:

instance (GMapKey a, GMapKey b) => GMapKey (Either a b) where
  data GMap (Either a b) v = GMapEither (GMap a v) (GMap b v)
  ...

instance Eq (Elem [e]) => Collects [e] where
  type Elem [e] = e
  ...

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

class Collects ce where
  type Elem ce :: Type

instance Eq (Elem [e]) => Collects [e] where
  -- Choose one of the following alternatives:
  type Elem [e] = e       -- OK
  type Elem [x] = x       -- BAD; '[x]' is different to '[e]' from head
  type Elem x   = x       -- BAD; 'x' is different to '[e]'
  type Elem [Maybe x] = x -- BAD: '[Maybe x]' is different to '[e]'

Обратите внимание на следующие моменты:

  • Экземпляр связанного семейства может появиться только как часть объявления экземпляра класса, в котором было объявлено семейство, так же как и уравнения методов класса.
  • Переменные типов в правой части уравнения семейства типов, как обычно, должны быть явно связаны левой частью. Однако это ограничение ослаблено для переменных киндов, поскольку правая часть может упоминать переменные киндов, которые неявно связаны. Например, это допустимо:

    data family Nat :: k -> k -> Type
    -- k is implicitly bound by an invisible kind pattern
    newtype instance Nat :: (k -> Type) -> (k -> Type) -> Type where
      Nat :: (forall xx. f xx -> g xx) -> Nat f g
    
    class Funct f where
      type Codomain f :: Type
    instance Funct ('KProxy :: KProxy o) where
      -- o is implicitly bound by the kind signature
      -- of the LHS type pattern ('KProxy)
      type Codomain 'KProxy = NatTr (Proxy :: o -> Type)
    
  • Экземпляр связанного типа может быть опущен в экземплярах класса. В этом случае, если нет экземпляра по умолчанию (см. Связанные типы синонимов по умолчанию), соответствующий тип экземпляра не обитаем; то есть, только расходящиеся выражения, такие как undefined, могут принять тип.
  • Хотя это необычно, (в настоящее время) может быть несколько экземпляров для связанного семейства в одном объявлении экземпляра. Например, это допустимо:

    instance GMapKey Flob where
      data GMap Flob [v] = G1 v
      data GMap Flob Int = G2 Int
      ...
    

    Здесь мы даем два объявления экземпляров данных, одно из которых имеет последний параметр [v], и одно для которого это Int. Поскольку вы не можете дать никаких последующих экземпляров для (GMap Flob ...), эта возможность наиболее полезна, когда свободная индексированная переменная является киндом с конечным числом вариантов (в отличие от Type).

  • Когда ExplicitForAll включено, переменные типа и кинда могут быть явно связаны в экземплярах связанных данных или семейств типов так же (и с теми же ограничениями), как и в Объявлениях экземпляров данных или Объявлениях экземпляров типов. Например, адаптируя вышеприведенное, следующее принимается:

    instance Eq (Elem [e]) => Collects [e] where
      type forall e. Elem [e] = e
    

10.9.4.2. Связанные типы синонимов по умолчанию

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

class IsBoolMap v where
  type Key v
  type instance Key v = Int

  lookupKey :: Key v -> v -> Maybe Bool

instance IsBoolMap [(Int, Bool)] where
  lookupKey = lookup

В объявлении instance для класса, если явное объявление type instance для связанного типа не указано, используется объявление по умолчанию, как и в случае с методами класса по умолчанию.

Обратите внимание на следующие моменты:

  • Ключевое слово instance необязательно.
  • Может быть не более одного объявления по умолчанию для связанного типа синонима.
  • Объявление по умолчанию не допускается для связанного типа данных.
  • В объявлении по умолчанию левая часть должна содержать только переменные типов, и переменные типа не могут повторяться в левой части. Правая часть может упоминать только переменные типа, которые явно связаны в левой части. Это ограничение ослаблено для переменных киндов, поскольку правая часть может упоминать переменные киндов, которые неявно связаны в левой части.

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

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

Вот несколько примеров:

class C (a :: Type) where
  type F1 a :: Type
  type instance F1 a = [a]     -- OK
  type instance F1 a = a->a    -- BAD; only one default instance is allowed

  type F2 b a                  -- OK; note the family has more type
                               --     variables than the class
  type instance F2 c d = c->d  -- OK; you don't have to use 'a' in the type instance

  type F3 a
  type F3 [b] = b              -- BAD; only type variables allowed on the
                                       LHS, and the argument to F3 is
                                       instantiated to [b], which is not
                                       a bare type variable

  type F4 x y
  type F4 x x = x              -- BAD; the type variable x is repeated on
                                       the LHS

  type F5 a
  type F5 b = a                -- BAD; 'a' is not in scope  in the RHS

  type F6 a :: [k]
  type F6 a = ('[] :: [x])     -- OK; the kind variable x is implicitly
                                      bound by an invisible kind pattern
                                      on the LHS

  type F7 a
  type F7 a =
    Proxy ('[] :: [x])         -- BAD; the kind variable x is not bound,
                                       even by an invisible kind pattern

  type F8 (x :: a) :: [a]
  type F8 x = ('[] :: [a])     -- OK; the kind variable a is implicitly
                                      bound by the kind signature of the
                                      LHS type pattern

  type F9 (a :: k)
  type F9 a = Maybe a          -- BAD; the kind variable k is
                                       instantiated to Type, which is not
                                       a bare kind variable

  type F10 (a :: j) (b :: k)
  type F10 (a :: z) (b :: z)
    = Proxy a                  -- BAD; the kind variable z is repeated,
                               -- as both j and k are instantiated to z

  type F11 a b
  type forall a b. F11 a b = a -- OK; LHS type variables can be
                                  explicitly bound with 'forall'

  type F12 (a :: k)
  type F12 @k a = Proxy a      -- OK; visible kind application syntax is
                                      permitted in default declarations

10.9.4.3. Область действия параметров класса

Видимость параметров класса в правой части экземпляров связанных семейств зависит исключительно от параметров семейства. В качестве примера рассмотрим простое объявление класса

class C a b where
  data T a

Только один из двух параметров класса является параметром семейства данных. Следовательно, следующее объявление экземпляра недействительно:

instance C [c] d where
  data T [c] = MkT (c, d)    -- WRONG!!  'd' is not in scope

Здесь правая часть экземпляра данных упоминает переменную типа d, которая не встречается в его левой части. Мы не можем допустить таких экземпляров данных, так как они нарушают безопасность типа.

Обратите внимание, что правая часть экземпляра связанной семьи также может содержать параметры рода (используя расширение PolyKinds). Например, этот класс и экземпляр являются допустимыми:

class C k where
  type T :: k

instance C (Maybe a) where
  type T = (Nothing :: Maybe a)

Здесь, хотя правая часть (Nothing :: Maybe a) упоминает переменную рода a, которая не встречается в левой части, это допустимо, поскольку a неявным образом связана паттерном рода T.

Переменная рода также может быть связана неявно в паттерне типа ЛС, как в этом примере:

class C a where
  type T (x :: a) :: [a]

instance C (Maybe a) where
  type T x = ('[] :: [Maybe a])

В ('[] :: [Maybe a]), переменная рода a неявно связана подписью рода паттерна типа ЛС x.

10.9.4.4. Контексты экземпляров и связанные типы и данные экземпляры

Объявления связанных типов и данных экземпляров не наследуют контекст, указанный в содержащем экземпляре. Для объявлений экземпляров типов неясно, что означает контекст. Для объявлений экземпляров данных маловероятно, что пользователь захочет повторять контекст для каждого конструктора данных. Единственное место, где контекст, вероятно, будет полезен, — это в пункте deriving связанного экземпляра данных. Однако даже здесь роль контекста внешнего экземпляра неясна. Поэтому, для ясности, мы просто придерживаемся вышеупомянутого правила: контекст содержащего экземпляра игнорируется. Если вам нужно использовать ненулевой контекст в производном экземпляре, используйте пункт standalone deriving (на верхнем уровне).

10.9.5. Импорт и экспорт

Правила для списков экспорта (документ Haskell Раздел 5.2) требуют корректировки для семейств типов:

  • Форма T(..), где T — семейство данных, называет семейство T и все конструкторы, находящиеся в области видимости (как с квалификацией, так и без), которые являются экземплярами данных T.
  • Форма T(.., ci, .., fj, ..), где T — семейство данных, называет T и указанные конструкторы ci и поля fj как обычно. Конструкторы и имена полей должны принадлежать к некоторому экземпляру данных T, но не обязательно к одному и тому же экземпляру.
  • Форма C(..), где C — класс, называет класс C и все его методы и связанные типы.
  • Форма C(.., mi, .., type Tj, ..), где C — класс, называет класс C, и указанные методы mi и связанные типы Tj. Для типов требуется ключевое слово «type», чтобы отличить их от конструкторов данных.
  • В тех случаях, когда нет списка экспорта, а определен экземпляр данных, соответствующий конструктор типов семейства данных экспортируется вместе с новыми конструкторами данных, независимо от того, определено ли семейство данных локально или в другом модуле.

10.9.5.1. Примеры

Вспомним наш пример класса GMapKey:

class GMapKey k where
  data GMap k :: Type -> Type
  insert :: GMap k v -> k -> v -> GMap k v
  lookup :: GMap k v -> k -> Maybe v
  empty  :: GMap k v

instance (GMapKey a, GMapKey b) => GMapKey (Either a b) where
  data GMap (Either a b) v = GMapEither (GMap a v) (GMap b v)
  ...method declarations...

Вот некоторые списки экспорта и их значение:

  • module GMap( GMapKey )
    

    Экспортирует только имя класса.

  • module GMap( GMapKey(..) )
    

    Экспортирует класс, связанный тип GMap и члены-функции empty, lookup, и insert. Конструкторы данных GMap (в данном случае GMapEither ) не экспортируются.

  • module GMap( GMapKey( type GMap, empty, lookup, insert ) )
    

    То же, что и предыдущий пункт. Обратите внимание на ключевое слово «type».

  • module GMap( GMapKey(..), GMap(..) )
    

    То же, что и предыдущий пункт, но также экспортирует все конструкторы данных для GMap, а именно GMapEither.

  • module GMap ( GMapKey( empty, lookup, insert), GMap(..) )
    

    То же, что и предыдущий пункт.

  • module GMap ( GMapKey, empty, lookup, insert, GMap(..) )
    

    То же, что и предыдущий пункт.

Два момента, на которые следует обратить внимание:

  • Вы не можете написать GMapKey(type GMap(..)) — т. е. вложенные спецификации компонентов недопустимы. Для указания конструкторов данных GMap необходимо указать их отдельно.
  • Рассмотрим этот пример:

    module X where
      data family D
    
    module Y where
      import X
      data instance D Int = D1 | D2
    

    Модуль Y экспортирует все сущности, определённые в Y, а именно конструкторы данных D1 и D2, и неявным образом семейство данных D, даже если оно определено в X. Это означает, что вы можете написать import Y( D(D1,D2) ) без явного списка экспорта, как в этом случае:

         module Y( D(..) ) where ...
    or   module Y( module Y, D ) where ...
    

10.9.5.2. Экземпляры

Экземпляры семейств неявно экспортируются, как и экземпляры классов. Однако это относится только к заголовкам экземпляров, а не к конструкторам данных, определённых экземпляром.

10.9.6. Семейства типов и объявления экземпляров

Для семейств типов необходимо расширить правила для формы заголовков экземпляров, которые приведены в Изменённые правила для заголовка экземпляра. В частности:

  • Семейства типов данных могут появляться в заголовке экземпляра
  • Семейства синонимов типов не могут появляться (вообще) в заголовке экземпляра

Причина последнего ограничения в том, что нет способа проверить соответствие экземпляров. Рассмотрим

type family F a
type instance F Bool = Int

class C a

instance C Int
instance C (F a)

Теперь ограничение (C (F Bool)) будет соответствовать обоим экземплярам. Ситуация особенно плоха, потому что экземпляр типа для F Bool может находиться в другом модуле или даже в модуле, который ещё не написан.

Однако экземпляры класса типов для экземпляров семейств данных можно определять так же, как и любой другой тип данных. Например, мы можем сказать

data instance T Int = T1 Int | T2 Bool
instance Eq (T Int) where
  (T1 i) == (T1 j) = i==j
  (T2 i) == (T2 j) = i==j
  _      == _      = False

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

Объявления экземпляров данных также могут иметь пункты deriving. Например, мы можем написать

data GMap () v = GMapUnit (Maybe v)
               deriving Show

что неявно определяет экземпляр в виде

instance Show v => Show (GMap () v) where ...

10.9.7. Инъективные семейства типов

TypeFamilyDependencies
Подразумевает

TypeFamilies

С

8.0.1

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

Начиная с GHC 8.0 семейства типов могут быть аннотированы информацией об инъективности. Эта информация используется GHC во время проверки типов для разрешения неоднозначностей типов в ситуациях, когда переменная типа встречается только в приложениях семейства типов. Рассмотрим этот искусственный пример:

type family Id a
type instance Id Int = Int
type instance Id Bool = Bool

id :: Id t -> Id t
id x = x

Здесь определение id будет отклонено, потому что переменная типа t встречается только в приложениях семейства типов и, следовательно, неоднозначна. Но этот код будет принят, если мы скажем GHC, что Id является инъективным, что означает, что t будет возможно вывести в местах вызова из типа аргумента:

type family Id a = r | r -> a

Инъективные семейства типов включены с помощью расширения языка -XTypeFamilyDependencies . Это расширение подразумевает -XTypeFamilies.

Для получения подробной информации об инъективных семействах типов см. статью Haskell Symposium 2015 Инъективные семейства типов для Haskell.

10.9.7.1. Синтаксис аннотации инъективности

Аннотация инъективности добавляется после заголовка семейства типов и состоит из двух частей:

  • переменная типа, которая называет результат семейства типов. Синтаксис: = tyvar или = (tyvar :: kind). Переменная типа должна быть новой.
  • аннотация инъективности в виде | A -> B, где A — переменная результата типа (см. предыдущий пункт), а B — список переменных типов и родов аргументов, в которых семейство типов инъективно. Некоторые переменные можно опустить, если семейство типов не инъективно в них.

Примеры:

type family Id a = result | result -> a where
type family F a b c = d | d -> a c b
type family G (a :: k) b c = foo | foo -> k b where

Для открытых и закрытых семейств типов допустимо назвать результат, но пропустить аннотацию инъективности. Это не относится к связанным синонимам типов, где имя результата без аннотации инъективности будет интерпретировано как значение по умолчанию связанного синонима типа.

10.9.7.2. Проверка аннотации инъективности по отношению к уравнениям семейств типов

После того, как пользователь объявляет семейство типов инъективным, GHC должен проверить, что это объявление верно, т. е. что уравнения семейства типов не нарушают аннотацию инъективности. Общая идея заключается в том, что если хотя бы одно уравнение (пункты (1), (2) и (3) ниже) или пара уравнений (пункты (4) и (5) ниже) нарушает аннотацию инъективности, то семейство типов не является инъективным так, как утверждает пользователь, и сообщается об ошибке. В пунктах ниже ПРА относится к правой части уравнения семейства типов, проверяемого на инъективность. ЛЕВ относится к аргументам этого уравнения семейства типов. Ниже приведены правила, используемые при проверке инъективности семейства типов:

  1. Если правая часть уравнения типа семейства представляет собой применение типа семейства, GHC сообщает, что тип семейства не инъективен.
  2. Если правая часть уравнения типа семейства представляет собой голое переменную типа, мы требуем, чтобы все переменные левой части (включая неявные переменные вида) также были голыми. Другими словами, это должно быть единственное уравнение этого типа семейства, и оно должно охватывать все возможные шаблоны. Если шаблоны не охватывают все возможные случаи, GHC сообщает, что тип семейства не инъективен.
  3. Если переменная типа левой части, объявленная как инъективная, не указана в инъективной позиции в правой части, GHC сообщает, что тип семейства не инъективен. Инъективная позиция означает либо аргумент конструктора типа, либо инъективный аргумент типа семейства. Вывод типов потенциально может зацикливаться при поиске под инъективными типами семейства в правой части, поэтому это требует UndecidableInstances; GHC предлагает включить флаг, когда это необходимо.
  4. Открытые типы семейств Открытые типы семейств проверяются инкрементально. Это означает, что при импорте модуля типы семейства, содержащиеся в этом модуле, проверяются относительно экземпляров, присутствующих в уже импортированных модулях.

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

  5. В закрытом типе семейства все уравнения упорядочены и находятся в одном месте. Уравнения также проверяются попарно, но на этот раз уравнение должно быть спарено со всеми предыдущими уравнениями. Конечно, закрытое семейство типа с одним уравнением тривиально инъективно (если не выполняется условие (1), (2) или (3) выше).

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

Обратите внимание, что для целей проверки инъективности в пунктах (4) и (5) GHC использует специальную разновидность алгоритма унификации, которая обрабатывает применения типа семейства как потенциально унифицируемые с любым типом.

10.10. Продвижение типов данных

DataKinds
С момента

7.4.1

Разрешить продвижение типов данных до уровня вида.

В этом разделе описано продвижение типов данных, расширение системы видов, которое дополняет полиморфизм видов. Оно включено с помощью DataKinds и описано более подробно в статье Giving Haskell a Promotion, опубликованной в TLDI 2012.

10.10.1. Мотивация

Стандартный Haskell имеет богатый язык типов. Типы классифицируют термины и служат для предотвращения многих распространенных ошибок программирования. Язык видов, однако, относительно прост, различая только обычные типы (вид Type) и конструкторы типов (например, вид Type -> Type -> Type). В частности, при использовании расширенных функций системы типов, таких как типы семейств (Типы семейств) или GADТ (Обобщенные алгебраические типы данных (GADТ)), эта простая система видов недостаточна и не предотвращает простых ошибок. Рассмотрим пример типов-уровня натуральных чисел и векторов с индексом длины:

data Ze
data Su n

data Vec :: Type -> Type -> Type where
  Nil  :: Vec a Ze
  Cons :: a -> Vec a n -> Vec a (Su n)

Вид Vec — это Type -> Type -> Type. Это означает, что, например, Vec Int Char — это правильно-видовой тип, хотя это не то, что мы имеем в виду при определении векторов с индексом длины.

С DataKinds, пример выше можно переписать следующим образом:

data Nat = Ze | Su Nat

data Vec :: Type -> Nat -> Type where
  Nil  :: Vec a 'Ze
  Cons :: a -> Vec a n -> Vec a ('Su n)

С улучшенным видом Vec, такие вещи, как Vec Int Char теперь имеют неправильный вид, и GHC сообщит об ошибке.

10.10.2. Обзор

С DataKinds, GHC автоматически продвигает каждый тип данных до вида и его (значения) конструкторы до конструкторов типов. Следующие типы

data Nat = Zero | Succ Nat

data List a = Nil | Cons a (List a)

data Pair a b = Pair a b

data Sum a b = L a | R b

порождают следующие виды и конструкторы типов (где продвинутые конструкторы снабжены префиксом тильдой '):

Nat :: Type
'Zero :: Nat
'Succ :: Nat -> Nat

List :: Type -> Type
'Nil  :: forall k. List k
'Cons :: forall k. k -> List k -> List k

Pair  :: Type -> Type -> Type
'Pair :: forall k1 k2. k1 -> k2 -> Pair k1 k2

Sum :: Type -> Type -> Type
'L :: k1 -> Sum k1 k2
'R :: k2 -> Sum k1 k2

Практически все конструкторы данных, даже те, у которых есть сложные виды, могут быть продвинуты. Есть только несколько исключений из этого правила:

  • Конструкторы экземпляров типа семейства в данный момент не могут быть продвинуты. Теория типов GHC просто не готова к продвижению типов семейств, что требует полных зависимых типов.
  • Конструкторы данных с контекстами, содержащими неравенства, не могут быть продвинуты. Например:

    data Foo :: Type -> Type where
      MkFoo1 :: a ~ Int         => Foo a    -- promotable
      MkFoo2 :: a ~~ Int        => Foo a    -- promotable
      MkFoo3 :: Show a          => Foo a    -- not promotable
    

    MkFoo1 и MkFoo2 могут быть продвинуты, поскольку их контексты включают только равенства. Однако, контекст MkFoo3 содержит неравенство Show a, и поэтому не может быть продвинут.

10.10.3. Различие между типами и конструкторами

В примерах выше все продвинутые конструкторы снабжены одинарной апострофом '. Эта отметка сообщает GHC искать имя в пространстве имен конструкторов данных, а не в пространстве имен типа (конструктора). Рассмотрим

data P = MkP    -- 1

data Prom = P   -- 2

Таким образом, мы можем различать тип P (который имеет конструктор MkP) и продвинутый конструктор данных 'P (вида Prom).

Для удобства GHC позволяет опустить апостроф, когда имя однозначно. Однако наш опыт показал, что апостроф помогает сделать код более читаемым и менее подверженным ошибкам. Поэтому GHC поддерживает -Wunticked-promoted-constructors, которая предупредит вас, если вы используете продвинутый конструктор данных без предшествующего апострофа.

Так же, как и в случае Template Haskell (Синтаксис), GHC сбивается с толку, если вы ставите апостроф перед конструктором данных, чья вторая буква — это апостроф. В этом случае просто поставьте пробел между апострофом продвижения и конструктором данных:

data T = A'
type S = 'A'   -- ERROR: looks like a character
type R = ' A'  -- OK: promoted `A'`

10.10.4. Продвинутые типы списков и кортежей

С DataKinds, списки и кортежи Haskell нативно продвинуты до видов и имеют такой же удобный синтаксис на уровне типа, хотя и снабжены префиксом апострофом:

data HList :: [Type] -> Type where
  HNil  :: HList '[]
  HCons :: a -> HList t -> HList (a ': t)

data Tuple :: (Type,Type) -> Type where
  Tuple :: a -> b -> Tuple '(a,b)

foo0 :: HList '[]
foo0 = HNil

foo1 :: HList '[Int]
foo1 = HCons (3::Int) HNil

foo2 :: HList [Int, Bool]
foo2 = ...

Для списков уровня типа, состоящих из двух или более элементов, таких как сигнатура foo2 выше, апостроф можно опустить, потому что смысл однозначен. Но для списков из одного или нуля элементов (как в foo0 и foo1), апостроф необходим, потому что у типов [] и [Int] уже есть значения в Haskell.

Примечание

Объявление для HCons также требует TypeOperators из-за инфиксного оператора типа (':)

10.10.5. Продвижение экзистенциальных конструкторов данных

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

data Ex :: Type where
  MkEx :: forall a. a -> Ex

Тип Ex и конструктор данных MkEx продвинутся с полиморфным видом 'MkEx :: forall k. k -> Ex. Скорее удивительно, что вы можете написать тип семейства, чтобы извлечь члена из экзистенциального типа уровня:

type family UnEx (ex :: Ex) :: k
type instance UnEx (MkEx x) = x

На первый взгляд, UnEx кажется плохо-видовым. Возвращаемый вид k не упоминается в аргументах, и, таким образом, кажется, что экземпляр должен возвращать член k для любого k. Однако это не так. Тип семейства UnEx является типом семейства, индексированного видом. Возвращаемый вид k — это неявный параметр для UnEx. Развернутые определения таковы (где неявные параметры обозначены фигурными скобками):

type family UnEx {k :: Type} (ex :: Ex) :: k
type instance UnEx {k} (MkEx @k x) = x

Таким образом, экземпляр срабатывает только тогда, когда неявный параметр для UnEx соответствует неявном параметру для MkEx. Так как k фактически является параметром для UnEx, вид не выходит за пределы экзистенции, и приведенный выше код является корректным.

См. также #7347.

10.11. Полиморфизм видов

TypeInType
Подразумевает

PolyKinds, DataKinds, KindSignatures

С момента

8.0.1

Расширение TypeInType теперь устарело: его единственное действие — включение PolyKinds (и следовательно KindSignatures) и DataKinds.

PolyKinds
Подразумевает

KindSignatures

С момента

7.4.1

Разрешить полиморфные типы вида.

В этом разделе описывается система типов GHC, которая используется в версии 8.0 и выше. Система типов, описанная здесь, всегда активна, как с расширениями, так и без них, хотя она является расширением стандартного Haskell. Расширения выше просто позволяют использовать синтаксис и настраивают алгоритм вывода, чтобы пользователи могли воспользоваться дополнительной выразительностью системы типов GHC.

10.11.1. Обзор полиморфизма типов

Рассмотрим вычисление типа для

data App f a = MkApp (f a)

В Haskell 98, выведенный тип для App равен (Type -> Type) -> Type -> Type. Но это слишком специфично, поскольку другой подходящий тип Haskell 98 для App — это ((Type -> Type) -> Type) -> (Type -> Type) -> Type, где типу a присваивается тип Type -> Type. Действительно, без типов для типов (KindSignatures) необходимо использовать фиктивный конструктор, чтобы компилятор Haskell вывел второй тип. С полиморфизмом типов (PolyKinds), GHC выводит тип forall k. (k -> Type) -> k -> Type для App, который является его наиболее общим типом.

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

App Maybe Int   -- `k` is instantiated to Type

data T a = MkT (a Int)    -- `a` is inferred to have kind (Type -> Type)
App T Maybe     -- `k` is instantiated to (Type -> Type)

10.11.2. Обзор "Тип в типе"

GHC 8 расширяет идею полиморфизма типов, заявляя, что типы и типы — это одно и то же. Ничто в GHC не различает типы и типы. Другой способ понять это — тип Bool и «продвинутый тип» Bool фактически идентичны. (Обратите внимание, что термин True и тип 'True все еще различны, поскольку первый может использоваться в выражениях, а второй — в типах.) Отсутствие различия между типами и типами является отличительной чертой языков с зависимыми типами. Полностью зависимые языки также убирают различие между выражениями и типами, но это тема для другого дня в GHC.

Одно упрощение, позволяемое сочетанием типов и типов, заключается в том, что тип Type просто Type. Правда, аксиома Type :: Type может привести к зависанию, но это не проблема в GHC, так как у нас уже есть другие способы сделать программы бесконечными как в типах, так и в выражениях. Это решение (среди многих других) *подразумевает*, что, несмотря на выразительность системы типов GHC, «доказательство», которое вы пишете на Haskell, не является неопровержимым математическим доказательством. GHC гарантирует только частичную корректность: если ваши программы компилируются и выполняются до конца, их результаты действительно имеют назначенные типы. Он не делает никаких заявлений о программах, которые не завершаются за конечное время.

Чтобы узнать больше об этом решении и разработке GHC «под капотом», пожалуйста, обратитесь к статье, в которой представлена эта система типов для GHC/Haskell.

10.11.3. Принципы вывода типов

Как правило, когда PolyKinds включен, GHC пытается вывести наиболее общий тип для объявления. Во многих случаях (например, в объявлении типа данных) определение имеет правую часть, которая сообщает о выводе типов. Но это не всегда так. Рассмотрим

type family F a

Объявления семейств типов не имеют правой части, но GHC все равно должен вывести тип для F. Поскольку ограничений нет, он мог бы вывести F :: forall k1 k2. k1 -> k2, но это кажется *слишком* полиморфным. Поэтому GHC по умолчанию устанавливает эти переменные типов без ограничений на Type и получаем F :: Type -> Type. Вы по-прежнему можете объявить F полиморфным по типу с помощью сигнатур типов:

type family F1 a                -- F1 :: Type -> Type
type family F2 (a :: k)         -- F2 :: forall k. k -> Type
type family F3 a :: k           -- F3 :: forall k. Type -> k
type family F4 (a :: k1) :: k2  -- F4 :: forall k1 k2. k1 -> k2

Общий принцип следующий:

  • Когда есть правая часть, GHC выводит наиболее полиморфный тип, согласующийся с правой частью. Примеры: обычные объявления типов данных и объявления GADTs, объявления классов. В случае объявления класса роль «правой части» выполняют сигнатуры методов класса.
  • Когда правой части нет, GHC по умолчанию устанавливает типы аргумента и результата на Type, за исключением случаев, когда это указано в сигнатуре типа. Примеры: объявления данных и открытых семейств типов.

Это правило иногда приводит к неожиданным последствиям (см. #10132).

class C a where    -- Class declarations are generalised
                   -- so C :: forall k. k -> Constraint
  data D1 a        -- No right hand side for these two family
  type F1 a        -- declarations, but the class forces (a :: k)
                   -- so   D1, F1 :: forall k. k -> Type

data D2 a   -- No right-hand side so D2 :: Type -> Type
type F2 a   -- No right-hand side so F2 :: Type -> Type

Полиморфизм типов из объявления класса делает D1 полиморфным по типу, но не D2; аналогично F1, F1.

10.11.4. Вывод порядка переменных в объявлении типа/класса

Возможны сложные зависимости между переменными типа, введенными в объявлении типа или класса. Вот пример:

data T a (b :: k) c = MkT (a c)

После анализа этого объявления GHC обнаружит, что a и c могут быть полиморфными по типу с a :: k2 -> Type и c :: k2. Таким образом, мы выводим следующий тип:

T :: forall {k2 :: Type} (k :: Type). (k2 -> Type) -> k -> k2 -> Type

Обратите внимание, что k2 стоит *перед* k, а k стоит *перед* a. Также обратите внимание, что k2 здесь заключено в фигурные скобки. Как объяснялось с TypeApplications (Переменные типа вывода и указанные), переменные типа и типа, над которыми GHC обобщает, но которые не написаны в исходной программе, недоступны для видимого применения типа. (Они называются *выведенными* переменными.) Такие переменные записываются в фигурных скобках с -fprint-explicit-foralls включенным.

Общий принцип таков:

  • Переменные, недоступные для применения типа, стоят первыми.
  • Затем следуют переменные, написанные пользователем, неявно введенные в область видимости типом переменной.
  • В конце идут обычные переменные типа объявления.
  • Переменные, не получившие явного порядка от пользователя, сортируются по ScopedSort (Порядок указанных переменных).

В примере T выше мы могли бы связать k *после* a; это не нарушало бы зависимостей. Однако это нарушило бы наш общий принцип, и поэтому k идет первой.

Иногда этот порядок не учитывает зависимости. Например:

data T2 k (a :: k) (c :: Proxy '[a, b])

Должно быть, что a и b имеют один и тот же тип. Также обратите внимание, что b неявно объявлено в типе c . Таким образом, согласно нашему общему принципу, b должны быть *перед* k. Однако b *зависит от* k. Поэтому мы отбрасываем T2 с соответствующим сообщением об ошибке.

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

class C (a :: k) b where
  type F (c :: j) (d :: Proxy m) a b

Мы выводим такие типы:

C :: forall {k1 :: Type} (k :: Type). k -> k1 -> Constraint
F :: forall {k1 :: Type} {k2 :: Type} {k3 :: Type} j (m :: k1).
     j -> Proxy m -> k2 -> k3 -> Type

Обратите внимание, что тип a указан в типе C, но выведен в типе F.

«Общий принцип», описанный здесь, призван сделать все это более предсказуемым для пользователей. Несложно было бы расширить GHC, чтобы ослабить этот принцип. Если вам необходимо изменение, подумайте о написании предложения об этом.

10.11.5. Полные пользовательские сигнатуры типов и полиморфная рекурсия

CUSKs
Since

8.10.1

Примечание! Это устаревшая функция, см. StandaloneKindSignatures для современной замены.

Так же, как и в выводе типов, вывод типов для рекурсивных типов может использовать только *мономорфную* рекурсию. Рассмотрим этот (выдуманный) пример:

data T m a = MkT (m a) (T Maybe (m a))
-- GHC infers kind  T :: (Type -> Type) -> Type -> Type

Рекурсивное использование T заставило второй аргумент иметь тип Type. Однако, как и в выводе типов, вы можете добиться полиморфной рекурсии, предоставив *полную пользовательскую сигнатуру типа* (или CUSK) для T. CUSK присутствует, когда все типы аргументов и тип результата известны без необходимости вывода. Например:

data T (m :: k -> Type) :: k -> Type where
  MkT :: m a -> T Maybe (m a) -> T m a

Полная пользовательская сигнатура типа указывает полиморфный тип для T, и эта сигнатура используется для всех вызовов T, включая рекурсивные. В частности, рекурсивное использование T находится в типе Type.

Что именно считается «полной пользовательской сигнатурой типа» для конструктора типа? Вот формы:

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

    data T1 :: (k -> Type) -> k -> Type       where ...
    -- Yes;  T1 :: forall k. (k->Type) -> k -> Type
    
    data T2 (a :: k -> Type) :: k -> Type     where ...
    -- Yes;  T2 :: forall k. (k->Type) -> k -> Type
    
    data T3 (a :: k -> Type) (b :: k) :: Type where ...
    -- Yes;  T3 :: forall k. (k->Type) -> k -> Type
    
    data T4 (a :: k -> Type) (b :: k)      where ...
    -- Yes;  T4 :: forall k. (k->Type) -> k -> Type
    
    data T5 a (b :: k) :: Type             where ...
    -- No;  kind is inferred
    
    data T6 a b                         where ...
    -- No;  kind is inferred
    
  • Для типа данных с верхним уровнем ::: все переменные вида, введённые после :: должны быть явно квантифицированы.

    data T1 :: k -> Type            -- No CUSK: `k` is not explicitly quantified
    data T2 :: forall k. k -> Type  -- CUSK: `k` is bound explicitly
    data T3 :: forall (k :: Type). k -> Type   -- still a CUSK
    
  • Для нового типа правила такие же, как и для типа данных, если UnliftedNewtypes включён. При включённом UnliftedNewtypes, конструктор типа имеет CUSK только если присутствует подпись вида. Как и в случае с типом данных с верхним уровнем ::, все переменные вида, введённые после :: должны быть явно квантифицированы.

    {-# LANGUAGE UnliftedNewtypes #-}
    newtype N1 where                 -- No; missing kind signature
    newtype N2 :: TYPE 'IntRep where -- Yes; kind signature present
    newtype N3 (a :: Type) where     -- No; missing kind signature
    newtype N4 :: k -> Type where    -- No; `k` is not explicitly quantified
    newtype N5 :: forall (k :: Type). k -> Type where -- Yes; good signature
    
  • Для класса каждая переменная типа должна быть аннотирована видом.
  • Для синонима типа каждая переменная типа и тип результата должны быть аннотированы видами:

    type S1 (a :: k) = (a :: k)    -- Yes   S1 :: forall k. k -> k
    type S2 (a :: k) = a           -- No    kind is inferred
    type S3 (a :: k) = Proxy a     -- No    kind is inferred
    

    Обратите внимание, что в S2 и S3 вид правой части довольно очевиден, но он всё ещё не считается имеющим полную подпись — вывод не может быть сделан до обнаружения подписи.

  • Объявление открытого типа или семейства данных всегда имеет CUSK; не аннотированные переменные типа по умолчанию имеют вид Type:

    data family D1 a                  -- D1 :: Type -> Type
    data family D2 (a :: k)           -- D2 :: forall k. k -> Type
    data family D3 (a :: k) :: Type   -- D3 :: forall k. k -> Type
    type family S1 a :: k -> Type     -- S1 :: forall k. Type -> k -> Type
    
  • Объявление ассоциированного типа или семейства данных имеет CUSK тогда и только тогда, когда его включающий класс имеет CUSK.

    class C a where                -- no CUSK
      type AT a b                  -- no CUSK, b is defaulted
    
    class D (a :: k) where         -- yes CUSK
      type AT2 a b                 -- yes CUSK, b is defaulted
    
  • Закрытое семейство типов имеет полную подпись, когда все его переменные типа аннотированы, и предоставлен вид результата (с верхним уровнем ::).

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

data Proxy a           -- Proxy :: forall k. k -> Type
data X (a :: Proxy k)

Согласно вышеприведённым правилам X имеет CUSK. Однако вид k не определён. Поэтому он квантифицируется, что даёт X вид forall k1 (k :: k1). Proxy k -> Type.

Обнаружение CUSK включено флагом CUSKs, который включён по умолчанию. Это расширение запланировано к устареванию, чтобы быть заменено на StandaloneKindSignatures.

10.11.6. Автономные подписи видов и полиморфная рекурсия

StandaloneKindSignatures
Подразумевает

NoCUSKs

С

8.10.1

Так же, как и в выводе типов, вывод видов для рекурсивных типов может использовать только мономорфную рекурсию. Рассмотрим этот (искусственный) пример:

data T m a = MkT (m a) (T Maybe (m a))
-- GHC infers kind  T :: (Type -> Type) -> Type -> Type

Рекурсивное использование T заставило второй аргумент иметь вид Type. Однако, как и в выводе типов, можно добиться полиморфной рекурсии, задав автономную подпись вида для T:

type T :: (k -> Type) -> k -> Type
data T m a = MkT (m a) (T Maybe (m a))

Автономная подпись вида определяет полиморфный вид для T, и эта подпись используется для всех вызовов T, включая рекурсивные. В частности, рекурсивное использование T имеет вид Type.

Хотя автономная подпись вида определяет вид конструктора типа, она не определяет его арность. Это особенно важно для семейств типов и синонимов типов, так как они не могут быть частично применены. См. Объявления семейств типов для получения дополнительной информации об арности.

Арность может быть указана с помощью явных связывателей и встроенных аннотаций вида:

-- arity F0 = 0
type F0 :: forall k. k -> Type
type family F0 :: forall k. k -> Type

-- arity F1 = 1
type F1 :: forall k. k -> Type
type family F1 :: k -> Type

-- arity F2 = 2
type F2 :: forall k. k -> Type
type family F2 a :: Type

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

-- arity FD1 = 1
type FD1 :: forall k. k -> Type
type FD1

-- arity FD2 = 2
type FD2 :: forall k. k -> Type
type FD2 a

Обратите внимание, что F0, F1, F2, FD1, и FD2 все имеют идентичные автономные подписи видов. Арность выводится из заголовка семейства типов.

10.11.7. Автономные подписи видов и заголовки объявлений

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

type Prox1 :: k -> Type
data Prox1 a = MkProx1
  -- OK.

type Prox2 :: k -> Type
data Prox2 = MkProx2
  -- Error:
  --   • Expected a type, but found something with kind ‘k -> Type’
  --   • In the data type declaration for ‘Prox2’

Объявления данных в стиле GADT могут связывать свои входные данные или использовать встроенную подпись в дополнение к автономной подписи вида:

type GProx1 :: k -> Type
data GProx1 a where MkGProx1 :: GProx1 a
  -- OK.

type GProx2 :: k -> Type
data GProx2 where MkGProx2 :: GProx2 a
  -- Error:
  --   • Expected a type, but found something with kind ‘k -> Type’
  --   • In the data type declaration for ‘GProx2’

type GProx3 :: k -> Type
data GProx3 :: k -> Type where MkGProx3 :: GProx3 a
  -- OK.

type GProx4 :: k -> Type
data GProx4 :: w where MkGProx4 :: GProx4 a
  -- OK, w ~ (k -> Type)

Классы подчиняются тем же правилам:

type C1 :: Type -> Constraint
class C1 a
  -- OK.

type C2 :: Type -> Constraint
class C2
  -- Error:
  --   • Couldn't match expected kind ‘Constraint’
  --                 with actual kind ‘Type -> Constraint’
  --   • In the class declaration for ‘C2’

С другой стороны, семейства типов освобождаются от этого правила:

type F :: Type -> Type
type family F
  -- OK.

Семейства данных — сложная область. Их заголовки освобождены от этого правила, но их инстансы — нет:

type T :: k -> Type
data family T
  -- OK.

data instance T Int = MkT1
  -- OK.

data instance T = MkT3
  -- Error:
  --   • Expecting one more argument to ‘T’
  --     Expected a type, but ‘T’ has kind ‘k0 -> Type’
  --   • In the data instance declaration for ‘T’

Это также относится к инстансам данных в стиле GADT:

data instance T (a :: Nat) where MkN4 :: T 4
                                 MKN9 :: T 9
  -- OK.

data instance T :: Symbol -> Type where MkSN :: T "Neptune"
                                        MkSJ :: T "Jupiter"
  -- OK.

data instance T where MkT4 :: T x
  -- Error:
  --   • Expecting one more argument to ‘T’
  --     Expected a type, but ‘T’ has kind ‘k0 -> Type’
  --   • In the data instance declaration for ‘T’

10.11.8. Вывод видов в закрытых семействах типов

Хотя все открытые семейства типов считаются имеющими полную подпись вида, указанную пользователем, мы можем ослабить это условие для закрытых семейств типов, где мы имеем уравнения, для которых можно выполнить вывод видов. GHC выведет виды для аргументов и типов результата закрытого семейства типов.

GHC поддерживает индексированные по виду семейства типов, где семейство совпадает как по виду, так и по типу. GHC не выведет это поведение без полной подписи вида, предоставленной пользователем, так как это иногда выведет неглавный тип. Действительно, мы можем рассматривать индексирование по виду как форму полиморфной рекурсии, где тип используется в виде, отличном от наиболее общего в своём собственном определении.

Например:

type family F1 a where
  F1 True  = False
  F1 False = True
  F1 x     = x
-- F1 fails to compile: kind-indexing is not inferred

type family F2 (a :: k) where
  F2 True  = False
  F2 False = True
  F2 x     = x
-- F2 fails to compile: no complete signature

type family F3 (a :: k) :: k where
  F3 True  = False
  F3 False = True
  F3 x     = x
-- OK

10.11.9. Вывод видов в объявлениях инстансов классов

Рассмотрим следующий пример поли-видового класса и инстанса для него:

class C a where
  type F a

instance C b where
  type F b = b -> b

В объявлении класса ничего не ограничивает вид типа a, поэтому он становится поли-видовой переменной типа (a :: k). Однако в объявлении инстанса правая часть инстанса ассоциированного типа b -> b указывает, что b должен быть вида Type. Теоретически GHC может распространить эту информацию обратно в заголовок инстанса и сделать так, чтобы это объявление инстанса применялось только к типу вида Type, а не к типам любого вида. Однако GHC этого не делает.

Короче говоря: GHC не распространяет информацию о видах из членов объявления инстанса класса в заголовок объявления инстанса.

Это отсутствие вывода видов — просто проблема проектирования GHC, но сделать его работоспособным потребовало бы существенной переработки инфраструктуры вывода, и неясно, стоит ли это того. Если вы хотите ограничить вид b в приведенном выше примере, просто используйте подпись вида в заголовке инстанса.

10.11.10. Вывод видов в сигнатурах типов

При проверке вида тип GHC учитывает только то, что написано в этом типе, при определении того, как обобщить вид типа.

Например, рассмотрим эти определения (с ScopedTypeVariables):

data Proxy a    -- Proxy :: forall k. k -> Type
p :: forall a. Proxy a
p = Proxy :: Proxy (a :: Type)

GHC сообщает об ошибке, сказав, что вид a должен быть переменной вида k, а не Type. Это потому, что, посмотрев на сигнатуру типа forall a. Proxy a, GHC предполагает, что вид a должен быть обобщён, а не ограничен видом Type . Затем определение функции отклоняется как более конкретное, чем его сигнатура типа.

10.11.11. Явная квантификация вида

Включённая с PolyKinds, GHC поддерживает явную квантификацию вида, как в этих примерах:

data Proxy :: forall k. k -> Type
f :: (forall k (a :: k). Proxy a -> ()) -> Int

Обратите внимание, что во втором примере forall связывает как вид k, так и переменную типа a вида k. В общем случае нет ограничений на глубину вложенности такой зависимости. Однако зависимость должна быть корректно оформлена: forall (a :: k) k. ... — это ошибка.

10.11.12. Неявная квантификация в синонимах типов и инстансах семейств типов

Рассмотрим правила области видимости для синонимов типов и инстансов семейств типов, таких как эти:

type          TS a (b :: k) = <rhs>
type instance TF a (b :: k) = <rhs>

Основной принцип заключается в том, что все переменные, упомянутые в правой части <rhs> должны быть связаны в левой части:

type TS a (b :: k) = (k, a, Proxy b)    -- accepted
type TS a (b :: k) = (k, a, Proxy b, z) -- rejected: z not in scope

Но есть одно исключение: свободные переменные, упомянутые во внешней подписи вида в правой части, неявно квантифицируются. Таким образом, в следующем примере переменные a, b, и k находятся в области видимости в правой части S:

type S a b = <rhs> :: k -> k

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

S :: forall k. Type -> Type -> k -> k

В этом случае мы не можем просто связать k в левой части, так как k станет видимым параметром:

type S k a b = <rhs> :: k -> k
S :: forall k -> Type -> Type -> k -> k

Обратите внимание, что мы рассматриваем только внешнюю подпись вида, чтобы определить, какие переменные неявно квантифицировать. В качестве контр-примера рассмотрим M1:

type M1 = 'Just ('Nothing :: Maybe k)    -- rejected: k not in scope

Здесь подпись вида скрыта внутри 'Just, и нет внешней подписи вида. Мы можем исправить этот пример, предоставив внешнюю подпись вида:

type M2 = 'Just ('Nothing :: Maybe k) :: Maybe (Maybe k)

Здесь k попадает в область видимости благодаря :: Maybe (Maybe k).

Подпись вида считается внешней независимо от избыточных скобок:

type P =    'Nothing :: Maybe a    -- accepted
type P = ((('Nothing :: Maybe a))) -- accepted

Закрытые экземпляры семейств типов подчиняются тем же правилам:

type family F where
  F = 'Nothing :: Maybe k            -- accepted

type family F where
  F = 'Just ('Nothing :: Maybe k)    -- rejected: k not in scope

type family F where
  F = 'Just ('Nothing :: Maybe k) :: Maybe (Maybe k)  -- accepted

type family F :: Maybe (Maybe k) where
  F = 'Just ('Nothing :: Maybe k)    -- rejected: k not in scope

type family F :: Maybe (Maybe k) where
  F @k = 'Just ('Nothing :: Maybe k) -- accepted

Переменные типов также могут быть квантифицированы в видимых позициях. Рассмотрим два следующих примера:

data ProxyKInvis (a :: k)
data ProxyKVis k (a :: k)

В первом примере, переменная типа k является невидимым аргументом для ProxyKInvis. Другими словами, пользователю не нужно явно инициализировать k, так как вывод типов автоматически определяет, чем должен быть k. Например, в ProxyKInvis True, k выводится как Bool. Это отражается в типе ProxyKInvis:

ProxyKInvis :: forall k. k -> Type

Во втором примере, k является видимым аргументом для ProxyKVis. То есть, k является аргументом, который пользователи должны явно предоставить при применении ProxyKVis. Например, ProxyKVis Bool True — это корректно сформированный тип.

Каков тип ProxyKVis? Можно сказать forall k. Type -> k -> Type, но это не совсем верно, так как это позволило бы допускать неверные вещи, такие как ProxyKVis Bool Int, которые должны быть отклонены из-за того, что Int не имеет тип Bool. Ключевое наблюдение состоит в том, что тип второго аргумента зависит от первого аргумента. GHC указывает эту зависимость в синтаксисе типа ProxyKVis:

ProxyKVis :: forall k -> k -> Type

Этот тип похож на тип ProxyKInvis, но с ключевым отличием: переменные типов, квантифицированные forall связываются со стрелкой (->), а не с точкой (.). Это видимый, зависимый квантификатор. Он видим, потому что пользователь должен явно указать тип для k, и он зависимый в том смысле, что k появляется позже в типе ProxyKVis. В свою очередь, k связка в forall k. k -> Type можно рассматривать как невидимый, зависимый квантификатор.

GHC допускает запись типов с этим синтаксисом при включенных расширениях ExplicitForAll и PolyKinds языка. Так же, как и с невидимыми forall, можно поместить явные типы для видимых переменных типов, поэтому следующее синтаксически корректно:

ProxyKVis :: forall (k :: Type) -> k -> Type

В настоящее время возможность записи видимых зависимых квантификаторов ограничена типами. Следовательно, видимые зависимые квантификаторы отклоняются в любом контексте, который однозначно является типом терма. Они также отклоняются в типах конструкторов данных.

10.11.13. Индексированные GADTs

Рассмотрим тип

data G (a :: k) where
  GInt    :: G Int
  GMaybe  :: G Maybe

Этот тип данных G похож на GADT как по типу, так и по виду. Предположим, что у вас есть g :: G a, где a :: k. Тогда сопоставление с образцом, показывающее, что g фактически GMaybe говорит нам, что k ~ (Type -> Type) и a ~ Maybe. Определение G требует, чтобы PolyKinds было включено, но сопоставление с образцом G не требует расширения, помимо GADTs. То, что это работает, является на самом деле простым расширением обычных GADTs и следствием того, что типы и виды являются одним и тем же.

Обратите внимание, что тип данных G используется в различных видах в его теле, и поэтому индексированные GADTs используют форму полиморфной рекурсии. Поэтому этот функционал можно использовать только в том случае, если вы предоставили полный пользовательский тип для типа данных (Полные пользовательские подписи типов и полиморфная рекурсия).

10.11.14. Многоранговые типы

Вместе с RankNTypes, GHC поддерживает многоранговые типы. Вот пример:

-- Heterogeneous propositional equality
data (a :: k1) :~~: (b :: k2) where
  HRefl :: a :~~: a

class HTestEquality (t :: forall k. k -> Type) where
  hTestEquality :: forall k1 k2 (a :: k1) (b :: k2). t a -> t b -> Maybe (a :~~: b)

Обратите внимание, что hTestEquality принимает два аргумента, где переменная типа t применяется к типам разных видов. Эта переменная типа должна быть поликиндной. Соответственно, тип HTestEquality (класс) имеет тип (forall k. k -> Type) -> Constraint, многоранговый тип.

Большое различие между многоранговыми типами и многоранговыми видами заключается в том, что forall в видах нельзя перемещать. Лучше всего это продемонстрировать на примере. Предположим, что мы хотим получить экземпляр HTestEquality для (:~~:).

instance HTestEquality ((:~~:) a) where
  hTestEquality HRefl HRefl = Just HRefl

С объявлением (:~~:) выше, ему присваивается вид forall k1 k2. k1 -> k2 -> Type. Таким образом, типу (:~~:) a присваивается вид k2 -> Type для некоторого k2. GHC не может переобобщить этот вид, чтобы получить forall k2. k2 -> Type, как ожидается. Таким образом, экземпляр отклоняется как некорректный по виду.

Чтобы разрешить такой экземпляр, нам нужно определить (:~~:) следующим образом:

data (:~~:) :: forall k1. k1 -> forall k2. k2 -> Type where
  HRefl :: a :~~: a

В этом переопределении мы указываем явный вид для (:~~:), откладывая выбор k2 до обработки первого аргумента (a). С этим объявлением для (:~~:), экземпляр для HTestEquality принимается.

Еще одно различие между многоранговыми видами и типами заключается в обработке выведенных и указанных пользователем переменных типов. Рассмотрим следующую программу:

newtype Foo (f :: forall k. k -> Type) = MkFoo (f Int)
data Proxy a = Proxy

foo :: Foo Proxy
foo = MkFoo Proxy

Тип параметра Foo имеет тип forall k. k -> Type, но тип Proxy имеет тип forall {k}. k -> Type, где {k} обозначает, что переменная вида k должна выводиться, а не задаваться пользователем. (См. Видимое применение типа для получения дополнительной информации о различии выводимых и задаваемых пользователем). GHC не считает forall k. k -> Type и forall {k}. k -> Type равными на уровне вида, и поэтому отклоняет Foo Proxy как некорректный по виду.

10.11.15. Ограничения в видах

Так как виды и типы идентичны, виды могут (с TypeInType) содержать ограничения типов. Однако поддерживаются только ограничения равенства.

Вот пример ограниченного вида:

type family IsTypeLit a where
  IsTypeLit Nat    = 'True
  IsTypeLit Symbol = 'True
  IsTypeLit a      = 'False

data T :: forall a. (IsTypeLit a ~ 'True) => a -> Type where
  MkNat    :: T 42
  MkSymbol :: T "Don't panic!"

Вышеприведенные объявления принимаются. Однако, если мы добавим MkOther :: T Int, мы получим ошибку, что ограничение равенства не выполняется; Int не является литералом типа. Обратите внимание, что явное квантование с forall a необходимо для проверки типа T (см. Полные пользовательские подписи типов и полиморфная рекурсия).

10.11.16. Вид Type

StarIsType
Since

8.6.1

Обрабатывать неквалифицированные использования оператора типа * как нулевые и перевести их в Data.Kind.Type.

Вид Type (импортированный из Data.Kind ) классифицирует обычные типы. С StarIsType (в настоящее время включено по умолчанию), * преобразуется в Type, но использование этого устаревшего синтаксиса не рекомендуется из-за конфликтов с TypeOperators. Это также относится к ★, варианту Unicode *.

10.11.17. Вывод зависимости в объявлениях типов данных

Если переменная типа a в объявлении типа данных, класса или семейства типов зависит от другой переменной типа k в том же объявлении, должны выполняться два условия:

  • a должна следовать за k в объявлении, и
  • k должна быть явно указана в типе некоторых переменных типа в этом объявлении.

Первый пункт просто означает, что зависимость должна быть корректно оформлена. Второй пункт касается способности GHC выводить зависимость. Вывод этой зависимости сложен, и GHC в настоящее время требует явного указания зависимости, то есть k должна быть указана в типе переменной типа, чтобы GHC явно понял, что зависимость намеревается.

Например:

data Proxy k (a :: k)            -- OK: dependency is "obvious"
data Proxy2 k a = P (Proxy k a)  -- ERROR: dependency is unclear

Во втором объявлении GHC не может сразу определить, что k должна быть зависимой переменной, и поэтому объявление отклоняется.

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

10.11.18. Вывод зависимости в пользовательских forall

Программист может использовать forall в типе, чтобы ввести новые квантифицированные переменные типа. Эти переменные могут зависеть друг от друга, даже в том же forall. Однако GHC требует, чтобы зависимость была выводима из тела forall. Вот несколько примеров:

data Proxy k (a :: k) = MkProxy   -- just to use below

f :: forall k a. Proxy k a        -- This is just fine. We see that (a :: k).
f = undefined

g :: Proxy k a -> ()              -- This is to use below.
g = undefined

data Sing a
h :: forall k a. Sing k -> Sing a -> ()  -- No obvious relationship between k and a
h _ _ = g (MkProxy :: Proxy k a)  -- This fails. We didn't know that a should have kind k.

Обратите внимание, что в последнем примере невозможно понять, что a зависит от k в теле forall (то есть в Sing k -> Sing a -> ()). Поэтому GHC отклоняет программу.

10.11.19. Значение по умолчанию для видов без PolyKinds

Без PolyKinds, GHC отказывается обобщать переменные видов. Поэтому он устанавливает значение по умолчанию для переменных вида в Type, когда это возможно; в противном случае выдаётся ошибка.

Вот пример этого в действии:

{-# LANGUAGE PolyKinds #-}
import Data.Kind (Type)
data Proxy a = P   -- inferred kind: Proxy :: k -> Type
data Compose f g x = MkCompose (f (g x))
  -- inferred kind: Compose :: (b -> Type) -> (a -> b) -> a -> Type

-- separate module having imported the first
{-# LANGUAGE NoPolyKinds, DataKinds #-}
z = Proxy :: Proxy 'MkCompose

В последней строке мы используем продвинутый конструктор 'MkCompose, который имеет вид

forall (a :: Type) (b :: Type) (f :: b -> Type) (g :: a -> b) (x :: a).
  f (g x) -> Compose f g x

Теперь мы должны вывести тип для z. Для этого, не обобщая переменные вида, мы должны установить значения по умолчанию для переменных вида 'MkCompose. Мы можем легко установить значения по умолчанию a и b в Type, но f и g будут иметь неправильный вид, если установить значения по умолчанию. Таким образом, определение для z является ошибкой.

10.11.20. Вывод типов при наличии полиморфизма видов

При полиморфизме видов происходит довольно много действий за кулисами, которые могут быть не видны программисту Haskell. GHC поддерживает несколько флагов, которые контролируют вывод типов в сообщениях об ошибках и в приглашении GHCi. Подробнее см. обсуждение опций вывода типов. Если вы используете полиморфизм видов и не понимаете, почему GHC отклоняет (или принимает) вашу программу, мы рекомендуем включить эти флаги, особенно -fprint-explicit-kinds.

10.12. Полиморфизм лёгкости

Для обеспечения полной гибкости в использовании видов необходимо использовать систему видов для различения упакованных, поднятых типов (обычных типов, таких как Int и [Bool]) и неупакованных, примитивных типов (Неупакованные типы и примитивные операции), таких как Int#. Таким образом, у нас есть так называемый полиморфизм лёгкости.

Вот ключевые определения, все доступные из GHC.Exts:

TYPE :: RuntimeRep -> Type   -- highly magical, built into GHC

data RuntimeRep = LiftedRep     -- for things like `Int`
                | UnliftedRep   -- for things like `Array#`
                | IntRep        -- for `Int#`
                | TupleRep [RuntimeRep]  -- unboxed tuples, indexed by the representations of the elements
                | SumRep [RuntimeRep]    -- unboxed sums, indexed by the representations of the disjuncts
                | ...

type Type = TYPE LiftedRep    -- Type is just an ordinary type synonym

Идея заключается в том, что у нас есть новая фундаментальная константа типа TYPE, параметризованная RuntimeRep. Таким образом, мы получаем Int# :: TYPE 'IntRep и Bool :: TYPE 'LiftedRep. Всё, что имеет тип в форме TYPE x может появляться по обе стороны стрелки функции ->. Таким образом, мы можем сказать, что -> имеет тип TYPE r1 -> TYPE r2 -> TYPE 'LiftedRep.

10.12.1. Переменные и аргументы с полиморфизмом лёгкости

Если бы GHC не приходилось компилировать программы, выполняющиеся в реальном мире, на этом всё закончилось бы. Но полиморфизм представлений может создавать немало проблем для генератора кода GHC. Рассмотрим

bad :: forall (r1 :: RuntimeRep) (r2 :: RuntimeRep)
              (a :: TYPE r1) (b :: TYPE r2).
       (a -> b) -> a -> b
bad f x = f x

Это кажется обобщением стандартного оператора $. Однако, если подумать о компиляции этого в исполняемый код, возникают проблемы. В частности, когда мы вызываем bad, мы каким-то образом должны передать x в bad. Какова ширина (то есть, количество бит) x? Это указатель? Какой регистр (плавающей запятой или целого числа) должен содержать x? Всё это невозможно сказать, потому что тип x, a :: TYPE r1 является полиморфным по лёгкости. Поэтому мы запрещаем такие конструкции с помощью следующего простого правила:

Переменная не может иметь полиморфный тип по лёгкости.

Это исключает bad , потому что переменная x имела бы полиморфный тип по представлению.

Однако, всё не потеряно. Мы всё ещё можем это сделать:

($) :: forall r (a :: Type) (b :: TYPE r).
       (a -> b) -> a -> b
f $ x = f x

Здесь только b является полиморфным по лёгкости. Нет переменных с полиморфным типом по лёгкости. И генератор кода не сталкивается с проблемами. На самом деле, это настоящий тип оператора $ GHC, немного более общий, чем версия Haskell 98.

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

10.12.2. Полиморфные по лёгкости дна

Мы можем использовать полиморфизм по лёгкости с error и undefined, типы которых указаны здесь:

undefined :: forall (r :: RuntimeRep) (a :: TYPE r).
             HasCallStack => a
error :: forall (r :: RuntimeRep) (a :: TYPE r).
         HasCallStack => String -> a

Эти функции не связывают переменную, полиморфную по лёгкости, и поэтому принимаются. Их полиморфизм позволяет пользователям удобно заглушать функции, которые возвращают неупакованные типы.

10.12.3. Вывод полиморфных по лёгкости типов

-fprint-explicit-runtime-reps

Выводить параметры RuntimeRep так, как они появляются; в противном случае они устанавливаются по умолчанию в 'LiftedRep.

Большинству пользователей GHC не нужно беспокоиться о полиморфизме по лёгкости или неупакованных типах. Для таких пользователей, вид полиморфизма по лёгкости в типе $ бесполезен. И поэтому, по умолчанию, он подавляется, предполагая, что все переменные типа RuntimeRep являются 'LiftedRep при выводе и вывод TYPE 'LiftedRep как Type (или * при включенном StarIsType).

Если вы хотите увидеть полиморфизм по лёгкости в своих типах, включите флаг -fprint-explicit-runtime-reps. Например,

ghci> :t ($)
($) :: (a -> b) -> a -> b
ghci> :set -fprint-explicit-runtime-reps
ghci> :t ($)
($)
  :: forall (r :: GHC.Types.RuntimeRep) a (b :: TYPE r).
     (a -> b) -> a -> b

10.13. Литералы на уровне типов

GHC поддерживает числовые и строковые литералы на уровне типов, предоставляя удобный доступ к большому количеству предопределённых констант на уровне типов. Числовые литералы имеют вид Nat, а строковые литералы — Symbol. Эта функция включается с помощью языкового расширения DataKinds.

Виды литералов и все другие низкоуровневые операции для этой функции определены в модуле GHC.TypeLits. Обратите внимание, что модуль определяет некоторые операторы на уровне типов, которые конфликтуют со своими операторами на уровне значений (например, (+)). Объявления импорта и экспорта, относящиеся к этим операторам, требуют явного аннотирования пространства имён (см. Явные пространства имён в импорте/экспорте).

Вот пример использования числовых литералов на уровне типов для предоставления безопасного интерфейса к низкоуровневой функции:

import GHC.TypeLits
import Data.Word
import Foreign

newtype ArrPtr (n :: Nat) a = ArrPtr (Ptr a)

clearPage :: ArrPtr 4096 Word8 -> IO ()
clearPage (ArrPtr p) = ...

Вот пример использования строковых литералов на уровне типов для имитации простых операций с записями:

data Label (l :: Symbol) = Get

class Has a l b | a l -> b where
  from :: a -> Label l -> b

data Point = Point Int Int deriving Show

instance Has Point "x" Int where from (Point x _) _ = x
instance Has Point "y" Int where from (Point _ y) _ = y

example = from (Point 1 2) (Get :: Label "x")

10.13.1. Значения на уровне выполнения для литералов на уровне типов

Иногда полезно получить значение на уровне выполнения литерала, связанного с литералом на уровне типа. Это делается с помощью функций natVal и symbolVal. Например:

GHC.TypeLits> natVal (Proxy :: Proxy 2)
2

Эти функции перегружены, потому что им нужно возвращать разные результаты в зависимости от типа, для которого они инстанцированы.

natVal :: KnownNat n => proxy n -> Integer

-- instance KnownNat 0
-- instance KnownNat 1
-- instance KnownNat 2
-- ...

GHC разряжает ограничение, как только он знает, какой конкретный литерал на уровне типа используется в программе. Обратите внимание, что это работает только для литералов, а не для произвольных выражений типов. Например, ограничение в форме KnownNat (a + b) не будет упрощено до (KnownNat a, KnownNat b); вместо этого GHC сохранит ограничение в том виде, в котором оно есть, до тех пор, пока не сможет упростить a + b до константного значения.

Также можно преобразовать значение целочисленного или строкового значения на уровне выполнения в соответствующий литерал на уровне типа. Конечно, результирующий литерал на уровне типа будет неизвестен на этапе компиляции, поэтому он скрыт в экзистенциальном типе. Преобразование может быть выполнено с помощью someNatVal для целых чисел и someSymbolVal для строк:

someNatVal :: Integer -> Maybe SomeNat
SomeNat    :: KnownNat n => Proxy n -> SomeNat

Операции со строками аналогичны.

10.13.2. Вычисления с натуральными числами на уровне типов

GHC 7.8 может вычислять арифметические выражения, включающие натуральные числа на уровне типов. Такие выражения могут быть построены с помощью семейств типов (+), (*), (^) для сложения, умножения и возведения в степень. Числа можно сравнивать с помощью (<=?), которое возвращает продвинутое булево значение, или (<=), которое сравнивает числа как ограничение. Например:

GHC.TypeLits> natVal (Proxy :: Proxy (2 + 3))
5

В настоящее время GHC имеет довольно ограниченные возможности рассуждений об арифметике: он будет вычислять только арифметические функции типов и сравнивать результаты — так же, как и для любой другой функции типов. В частности, он не знает более общих фактов об арифметике, таких как коммутативность и ассоциативность (+) , например.

Однако, возможно выполнить вычисление «назад». Например, вот как мы можем заставить GHC вычислять произвольные логарифмы на уровне типа:

lg :: Proxy base -> Proxy (base ^ pow) -> Proxy pow
lg _ _ = Proxy

GHC.TypeLits> natVal (lg (Proxy :: Proxy 2) (Proxy :: Proxy 8))
3

10.14. Ограничения равенства, Coercible и ограничение вида

10.14.1. Ограничения равенства

Контекст типа может содержать ограничения равенства вида t1 ~ t2, которые указывают на то, что типы t1 и t2 должны быть одинаковыми. При наличии семейств типов, нельзя обычно локально определить, равны ли два типа. Поэтому контексты сигнатур функций могут содержать ограничения равенства, как в следующем примере:

sumCollects :: (Collects c1, Collects c2, Elem c1 ~ Elem c2) => c1 -> c2 -> c2

где требуется, чтобы тип элемента c1 и c2 были одинаковыми. В общем случае, типы t1 и t2 ограничения равенства могут быть произвольными монотипами; то есть, они могут не содержать квантификаторов, независимо от того, включены ли иначе типы высшего порядка.

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

class C a b | a -> b

в

class (F a ~ b) => C a b where
  type F a

То есть, мы представляем каждую функциональную зависимость (FD) a1 .. an -> b семейством типов FD F a1 .. an и ограничением равенства суперкласса F a1 .. an ~ b, по существу давая имя функциональной зависимости. В инстансах классов мы определяем типы инстансов семейств FD в соответствии с заголовком класса. Сигнатуры методов не затрагиваются этим процессом.

10.14.2. Гетерогенное равенство

GHC также поддерживает разнородное равенство, которое связывает два типа потенциально разных видов. Разнородное равенство записывается как ~~. Вот виды ~ и ~~ для лучшего понимания их различий:

(~)  :: forall k. k -> k -> Constraint
(~~) :: forall k1 k2. k1 -> k2 -> Constraint

Пользователи, скорее всего, захотят ~, но ~~ доступно, если GHC не может a priori узнать, что два типа имеют одинаковый вид. Доказательство, что (a :: k1) ~~ (b :: k2) сообщает GHC о том, что k1 и k2 одинаковы, и что a и b одинаковы.

Поскольку ~ является более распространённым отношением равенства, GHC выводит ~~ как ~, если не установлен -fprint-equality-relations.

10.14.3. Неподъёмное разнородное равенство

Внутри GHC есть ещё одно отношение равенства (~#). Оно разнородное (как ~~) и используется только внутри. Оно может отображаться в сообщениях об ошибках и других результатах только при включении -fprint-equality-relations.

10.14.4. Ограничение Coercible

Ограничение Coercible t1 t2 аналогично t1 ~ t2, но обозначает репрезентативное равенство между t1 и t2 в смысле ролей (Роли). Оно экспортируется из Data.Coerce, в котором также содержится документация. Более подробные сведения и обсуждение можно найти в статье “Safe Coercions”.

10.14.5. Вид Constraint

ConstraintKinds
Since

7.4.1

Разрешает использовать типы вида Constraint в контекстах.

Обычно ограничения (которые появляются в типах слева от стрелки =>) имеют очень ограниченный синтаксис. Они могут быть только:

  • Ограничения классов, например, Show a
  • Implicit parameter ограничения, например, ?x::Int (с расширением ImplicitParams)
  • Ограничения равенства, например, a ~ Int (с расширениями TypeFamilies или GADTs)

С расширением ConstraintKinds GHC становится более либеральным в том, что он принимает в качестве ограничений в вашей программе. Точнее, с этим флагом любой тип нового вида Constraint может использоваться как ограничение. Следующие вещи имеют вид Constraint:

  • Всё, что уже является допустимым ограничением без флага: насыщенные применения к типам классов, ограничения неявных параметров и равенства.
  • Кортежи, все компоненты типов которых имеют вид Constraint. Например, тип (Show a, Ord a) имеет вид Constraint.
  • Всё, чья форма ещё не известна, но пользователь объявил, что имеет вид Constraint (для чего он должен импортировать его из Data.Kind). Например, type Foo (f :: Type -> Constraint) = forall b. f b => b -> b разрешено, а также примеры, связанные с типами семейств:

    type family Typ a b :: Constraint
    type instance Typ Int  b = Show b
    type instance Typ Bool b = Num b
    
    func :: Typ a b => a -> b -> b
    func = ...
    

Обратите внимание, что поскольку ограничения обрабатываются как типы определённого вида, это расширение позволяет использовать синонимы ограничений типов:

type Stringy a = (Read a, Show a)
foo :: Stringy a => a -> (String, String -> a)
foo x = (show x, read)

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

type family Clsish u a
type instance Clsish () a = Cls a
class Clsish () a => Cls a where
class OkCls a where

type family OkClsish u a
type instance OkClsish () a = OkCls a
instance OkClsish () a => OkCls a where

Вы можете писать программы, которые используют экзотические виды ограничений в контекстах экземпляров и суперклассов, но для этого вам нужно использовать UndecidableInstances для указания того, что вы не возражаете, если проверка типа не завершится.

10.15. Ограничения с кванторами

QuantifiedConstraints
Since

8.6.1

Разрешает квантование типов по ограничениям.

Расширение QuantifiedConstraints вводит ограничения с кванторами, которые предоставляют новый уровень выразительности в ограничениях. Например, рассмотрим

data Rose f a = Branch a (f (Rose f a))

instance (Eq a, ???) => Eq (Rose f a)
  where
    (Branch x1 c1) == (Branch x2 c2)
       = x1==x1 && c1==c2

Из x1==x2 нам нужно Eq a, что нормально. Из c1==c2 нам нужно Eq (f (Rose f a)), что не нормально в Haskell на сегодняшний день; у нас нет способа решить такое ограничение.

QuantifiedConstraints позволяет записать это

instance (Eq a, forall b. (Eq b) => Eq (f b))
       => Eq (Rose f a)
  where
    (Branch x1 c1) == (Branch x2 c2)
       = x1==x1 && c1==c2

Здесь ограничение с квантором forall b. (Eq b) => Eq (f b) ведет себя как локальная декларация экземпляра и делает экземпляр типизируемым.

В статье Quantified class constraints (Bottu, Karachalias, Schrijvers, Oliveira, Wadler, Haskell Symposium 2017) подробно описывается эта функция с примерами, поэтому она является основным источником информации по этой функции.

10.15.1. Мотивация

Введение ограничений с кванторами предлагает две основные выгоды:

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

    data Rose f x = Rose x (f (Rose f x))
    
    instance (Eq a, forall b. Eq b => Eq (f b)) => Eq (Rose f a) where
      (Rose x1 rs1) == (Rose x2 rs2) = x1 == x2 && rs1 == rs2
    

    Это расширение позволяет нам записать ограничения в виде forall b. Eq b => Eq (f b), которые необходимы для решения ограничения Eq (f (Rose f x)), возникающего при втором использовании метода (==).

  • Во-вторых, ограничения с кванторами позволяют создавать более краткие и точные спецификации. В качестве примера рассмотрим тип класса MTL для трансформаторов монады:

    class Trans t where
      lift :: Monad m => m a -> (t m) a
    

    Разработчик знает, что трансформатор монады преобразует монаду m в новую монаду t m. Но это свойство не формально определено в приведенном выше объявлении. Это упущение становится проблемой при определении композиции трансформаторов монады:

    newtype (t1 * t2) m a = C { runC :: t1 (t2 m) a }
    
    instance (Trans t1, Trans t2) => Trans (t1 * t2) where
      lift = C . lift . lift
    

    Здесь цель состоит в lift из монады m в t2 m и затем lift это снова в t1 (t2 m). Однако это второе lift может быть принято только тогда, когда (t2 m) является монадой, и нет способа установить этот факт повсеместно.

    Ограничения с кванторами позволяют сделать это свойство явным в объявлении класса Trans:

    class (forall m. Monad m => Monad (t m)) => Trans t where
      lift :: Monad m => m a -> (t m) a
    

Эта идея очень старая; см. раздел 7 Derivable type classes.

10.15.2. Изменения в синтаксисе

Haskell 2010 определяет context (часть слева от => в типе) так

context ::= class
        |   ( class1, ..., classn )

class ::= qtycls tyvar
        |  qtycls (tyvar atype1 ... atypen)

Мы расширяем class (предупреждение: это довольно запутанное нетерминальное обозначение) двумя дополнительными формами, а именно тем, что может быть в объявлении экземпляра

class ::= ...
      | [context =>] qtycls inst
      | [context =>] tyvar inst

Определение inst не изменено с момента отчета Haskell (примерно, просто тип). Часть context => необязательна. Это единственное изменение синтаксиса языка.

Примечания:

  • В тех местах, где GHC допускает объявления экземпляров, мы допускаем те же расширения и для этого нового вида class. В частности, с ExplicitForAll и MultiParamTypeClasses синтаксис становится

    class ::= ...
           | [forall tyavrs .] [context =>] qtycls inst1 ... instn
           | [forall tyavrs .] [context =>] tyvar inst1 ... instn
    

    Обратите внимание, что явный forall часто абсолютно необходим. Рассмотрим пример дерева роз

    instance (Eq a, forall b. Eq b => Eq (f b)) => Eq (Rose f a) where ...
    

    Без forall b переменная типа b будет квантована по всему объявлению экземпляра, что не соответствует цели.

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

    f :: (Eq a, forall b. Eq b => Eq (f b)) => Rose f a -> Rose f a -> Bool
    f t1 t2 = not (t1 == t2)
    
  • Форма с переменной типа в начале позволяет это:

    instance (forall xx. c (Free c xx)) => Monad (Free c) where
        Free f >>= g = f g
    

    См. резюме Айсленда Джека. Ключевой момент заключается в том, что часть справа от => может быть заголовком переменной типа (c в данном случае), а не класса. Она не должна быть одной из переменных, по которым идёт forall.

    (Примечание: это выходит за рамки описанного в статье, но не кажется, что это вводит какие-либо новые технические трудности.)

10.15.3. Изменения в типизации

См. статью.

10.15.4. Суперклассы

Предположим, у нас есть:

f :: forall m. (forall a. Ord a => Ord (m a)) => m Int -> Bool
f x = x == x

Из x==x нам нужна ограничение Eq (m Int), но контекст дает только способ выяснить ограничения Ord (m a). Однако из заданного ограничения forall a. Ord a => Ord (m a) мы выводим второе заданное ограничение forall a. Ord a => Eq (m a), и из этого мы можем легко решить Eq (m Int). Этот процесс очень похож на работу суперклассов: при заданном ограничении Ord a мы выводим второе заданное ограничение Eq a.

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

10.15.5. Перекрытие

Квантифицированные ограничения потенциально могут привести к перекрывающимся локальным аксиомам. Рассмотрим, например, следующий пример:

class A a where {}
class B a where {}
class C a where {}
class (A a => C a) => D a where {}
class (B a => C a) => E a where {}

class C a => F a where {}
instance (B a, D a, E a) => F a where {}

При проверке типа объявления экземпляра для F a, необходимо проверить, что суперкласс C для F выполняется. Таким образом, мы пытаемся вывести ограничение C a в теории, содержащей:

  • Аксиомы экземпляра: (B a, D a, E a) => F a
  • Локальные аксиомы из контекста экземпляра: B a, D a и E a
  • Закрытие отношения суперкласса по этим локальным аксиомам: A a => C a и B a => C a

Однако, аксиомы A a => C a и B a => C a обе соответствуют нужному ограничению C a. Существует несколько возможных подходов к обработке этих перекрывающихся локальных аксиом:

  • Выбор первой. Мы можем просто выбрать первую соответствующую аксиому, которую мы встречаем. В приведенном выше примере это будет A a => C a. Затем нам нужно вывести A a, для которого у нас нет соответствующих аксиом, что приведет к отклонению вышеуказанной программы.

    Но предположим, что мы внесли небольшое изменение в порядок контекста экземпляра, поместив E a перед D a:

    instance (B a, E a, D a) => F a where {}
    

    Первая соответствующая аксиома, которую мы встречаем при выводе C a, — это B a => C a. У нас есть локальная аксиома B a доступна, и теперь программа неожиданно принимается. Такое поведение, когда порядок контекста экземпляра определяет, принимается ли программа или нет, кажется довольно запутанным для разработчика.

  • Отклонить при сомнениях. Альтернативный подход заключался бы в проверке перекрывающихся аксиом при решении ограничения. При обнаружении нескольких соответствующих аксиом мы отклоняем программу. Этот подход несколько консервативен, так как он может отклонить рабочие программы. Но он кажется более прозрачным для разработчика, которому может быть представлено четкое сообщение, объясняющее, почему программа отклоняется.
  • Обратная отработка. Наконец, можно ввести простую форму обратной отработки. Мы просто выбираем первую соответствующую аксиому, которую мы встречаем, и когда вычисление терпит неудачу, мы отступаем и ищем другие аксиомы, которые могут соответствовать нужному ограничению.

    Этот подход кажется наиболее интуитивным и прозрачным для разработчика, которому больше не нужно беспокоиться о том, что его код может содержать перекрывающиеся аксиомы или о порядке его контекстов экземпляров. Но обратная отработка будет применяться и к обычному выбору экземпляров (при наличии перекрывающихся экземпляров), поэтому это гораздо более существенное изменение со значительными последствиями для движка вывода типов.

GHC принимает отклонение при сомнениях пока. Мы можем увидеть, насколько это сложно на практике, и попробовать что-то более амбициозное при необходимости.

10.15.6. Поиск экземпляра

В свете решения по перекрытию поиск экземпляра работает следующим образом при попытке решить ограничение класса C t

  1. Сначала проверьте, есть ли заданное неквантифицированное ограничение C t. Если да, используйте его для решения ограничения.
  2. Если нет, посмотрите на все доступные заданные квантифицированные ограничения; если ровно одно соответствует C t, выберите его; если соответствует более одного, сообщите об ошибке.
  3. Если ни одно квантифицированное ограничение не соответствует, выполните поиск в глобальных экземплярах, как описано в Разрешение экземпляров и Перекрывающиеся экземпляры.

10.15.7. Завершение

GHC использует Условия Патерсона для обеспечения завершения разрешения экземпляров. Как эти правила модифицируются для квантифицированных ограничений? Двумя способами.

  • Каждое квантифицированное ограничение само по себе должно удовлетворять правилам завершения для объявления экземпляра.
  • После «для каждого ограничения класса (C t1 ... tn)» добавьте «или каждого квантифицированного ограничения (forall as. context => C t1 .. tn)»

Обратите внимание, что второй пункт только в начале квантифицированного ограничения, а не в его контексте. Причина: заголовок — это новая цель, которую нужно решить, если мы используем объявление экземпляра.

Конечно, UndecidableInstances поднимает условия Патерсона, как и сейчас.

10.15.8. Согласованность

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

f :: (forall a. Eq a => Eq (f a)) => f b -> f Bool
f x = ...rhs...

В ...rhs... фактически существует локальный экземпляр для Eq (f a) для любого a. Но в месте вызова f сам компилятор генерирует доказательство для передачи f. Например, если мы вызовем f Nothing, то f равно Maybe, и компилятор должен доказать (в месте вызова), что forall a. Eq a => Eq (Maybe a) выполняется. Он может сделать это легко, обратившись к существующему объявлению экземпляра для Eq (Maybe a).

Короче говоря, квантифицированные ограничения не вводят несогласованность.

10.16. Расширения сигнатур типов

10.16.1. Явное универсальное квантование (forall)

ExplicitForAll
С

6.12.1

Разрешить использование ключевого слова forall в местах, где универсальное квантование подразумевается.

Сигнатуры типов Haskell неявно квантифицированы. Когда используется параметр языка ExplicitForAll, ключевое слово forall позволяет точно сказать, что это значит. Например:

g :: b -> b

означает следующее:

g :: forall b. (b -> b)

Оба варианта обрабатываются одинаково, за исключением того, что последний может вводить типы переменных в область видимости (см. Переменные типов с лексической областью видимости).

Это расширение также позволяет явно квантифицировать переменные типов и видов в Объявлениях экземпляров данных, Объявлениях экземпляров типов, Закрытых семействах типов, Связанных экземплярах и Правилах переписывания.

Примечания:

  • Также в сигнатурах типов вы можете использовать явное forall в объявлении экземпляра:

    instance forall a. Eq a => Eq [a] where ...
    
  • Если включён флаг -Wunused-foralls, будет выведено предупреждение при написании переменной типа в явном forall операторе, которая в противном случае не используется. Например:

    g :: forall a b. (b -> b)
    

    вызовет предупреждение о неиспользуемой переменной типа a.

10.16.2. Контекст сигнатуры типа

Расширение FlexibleContexts снимает ограничение Haskell 98, что ограничения типа класса в сигнатуре типа должны иметь вид (class type-variable) или (class (type-variable type1 type2 … typen)). С FlexibleContexts эти сигнатуры типа допустимы

g :: Eq [a] => ...
g :: Ord (T a ()) => ...

Флаг FlexibleContexts также снимает соответствующее ограничение на объявления классов (Суперклассы объявления класса) и объявления экземпляров (Измененные правила для контекстов экземпляров).

10.16.3. Неоднозначные типы и проверка неоднозначности

AllowAmbiguousTypes
С

7.8.1

Разрешить сигнатуры типов, которые кажутся такими, что приведут к непригодной связи.

Каждая введённая пользователем сигнатура типа подвергается проверке неоднозначности. Проверка неоднозначности отклоняет функции, которые никогда не могут быть вызваны; например:

f :: C a => Int

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

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

class D a b | a -> b where ..
h :: D Int b => Int

Int , вероятно, определит b в месте вызова, поэтому сигнатура не должна отклоняться. Кроме того, зависимости могут быть скрытыми. Рассмотрим

class X a b where ...
class D a b | a -> b where ...
instance D a b => X [a] b where...
h :: X a b => a -> a

Здесь тип h выглядит неоднозначным в b, но вот законное вызов:

...(h [True])...

Это приводит к ограничению (X [Bool] beta), и использование экземпляра означает, что нам нужны (D Bool beta) и это исправляет beta через fundep D!

За всеми этими особыми случаями стоит простое руководящее начало. Рассмотрим

f :: type
f = ...blah...

g :: type
g = f

Можно было бы подумать, что определение g обязательно пройдет проверку типов! В конце концов f имеет точно такой же тип, и g=f. Но на самом деле тип f инстанцируется, и инстанцированные ограничения решаются относительно ограничений, связанных с сигнатурой g. Таким образом, в случае неоднозначного типа решение завершится неудачей. Например, рассмотрите ранее определённое f :: C a => Int:

f :: C a => Int
f = ...blah...

g :: C a => Int
g = f

В определении g мы инстанцируем (C alpha) и попытаемся вывести (C alpha) из (C a), и потерпим неудачу.

Таким образом, фактически мы используем это как определение неоднозначности: тип ty неоднозначен, если и только если ((undefined :: ty) :: ty) не пройдёт проверку типов. Мы используем очень похожий тест для выведенных типов, чтобы убедиться, что они также не являются неоднозначными.

Отключение проверки неоднозначности. Даже если функция имеет неоднозначный тип согласно «руководящему принципу», возможно, что функция вызываема. Например:

class D a b where ...
instance D Bool b where ...

strange :: D a b => a -> a
strange = ...blah...

foo = strange True

Здесь тип strange неоднозначен, но вызов в foo допустим, потому что он приводит к ограничению (D Bool beta), которое разрешается экземпляром (D Bool b).

Другой способ избавиться от неоднозначности в месте вызова — использовать расширение TypeApplications для указания типов. Например:

class D a b where
  h :: b
instance D Int Int where ...

main = print (h @Int @Int)

Здесь a неоднозначно в определении D, но позже указывается как Int с использованием приложений типов.

AllowAmbiguousTypes позволяет отключить проверку неоднозначности. Однако даже с отключённой проверкой неоднозначности GHC будет жаловаться на функцию, которую никогда нельзя вызвать, например, на эту:

f :: (Int ~ Bool) => a -> a

Иногда AllowAmbiguousTypes не сочетается с RankNTypes. Например:

foo :: forall r. (forall i. (KnownNat i) => r) -> r
foo f = f @1

boo :: forall j. (KnownNat j) => Int
boo = ....

h :: Int
h = foo boo

Эта программа будет отклонена как неоднозначная, потому что GHC не сведёт типы переменных j и i.

В отличие от предыдущих примеров, в настоящее время невозможно вручную разрешить неоднозначность с помощью TypeApplications.

Примечание

Историческая справка. GHC раньше накладывал некоторые более жёсткие и менее принципиальные условия на сигнатуры типов. Для типа forall tv1..tvn (c1, ...,cn) => type GHC раньше требовал

  1. что каждая универсально квантифицируемая переменная типа tvi должна быть «достижима» из type, и
  2. что каждое ограничение ci упоминает по крайней мере одну из универсально квантифицируемых переменных типа tvi. Эти произвольные ограничения полностью подразумеваются новой проверкой неоднозначности.

10.16.4. Явно-видовое квантование

KindSignatures
С момента

6.8.1

Разрешить явные сигнатуры видов для переменных типа.

Haskell выводит вид каждой переменной типа. Иногда полезно указать вид явно как (проверяемое машиной) документацию, так же, как полезно указать сигнатуру типа для функции. В некоторых случаях это необходимо. Например, в своей статье «Ограниченные типы данных в Haskell» (Haskell Workshop 1999) Джон Хьюз должен был определить тип данных:

data Set cxt a = Set [a]
               | Unused (cxt a -> ())

Единственное использование конструктора Unused заключалось в том, чтобы принудительно установить правильный вид для переменной типа cxt.

Теперь GHC вместо этого позволяет указать вид переменной типа напрямую, где бы ни была явным образом связана переменная типа, с расширением KindSignatures.

Это расширение позволяет использовать сигнатуры видов в следующих местах:

  • объявления data:

    data Set (cxt :: Type -> Type) a = Set [a]
    
  • объявления type:

    type T (f :: Type -> Type) = f Int
    
  • объявления class:

    class (Eq a) => C (f :: Type -> Type) a where ...
    
  • типы в сигнатурах forall:

    f :: forall (cxt :: Type -> Type). Set cxt Int
    

Скобки обязательны.

В рамках того же расширения вы можете поместить аннотации вида в типы. Таким образом:

f :: (Int :: Type) -> Int
g :: forall a. a -> (a :: Type)

Синтаксис

atype ::= '(' ctype '::' kind ')

Скобки обязательны.

10.17. Переменные типа с лексическим охватом

ScopedTypeVariables
Подразумевает

ExplicitForAll

С момента

6.8.1

Включить лексический охват переменных типа, явно введённых с forall.

Подсказка

ScopedTypeVariables нарушает обычное правило GHC, что явные forall необязательны и не влияют на семантику. Для примеров Сигнатуры типов декларации (или Сигнатуры типов выражений) в этом разделе явные forall обязательны. (Если их опустить, обычно программа не будет компилироваться; в некоторых случаях она будет компилироваться, но функции получат другую сигнатуру.) Для запуска этих форм ScopedTypeVariables, forall должен появиться в отношении основной сигнатуры (или внешнего выражения), но не в отношении вложенных сигнатур, ссылающихся на те же переменные типа.

Явные forall не всегда требуются — см. эквивалентную форму шаблона для примера в этом разделе или Сигнатуры типов шаблонов.

GHC поддерживает переменные типа с лексическим охватом, без которых некоторые сигнатуры типов просто невозможно записать. Например:

f :: forall a. [a] -> [a]
f xs = ys ++ ys
     where
       ys :: [a]
       ys = reverse xs

Сигнатура типа для f вводит переменную типа a в область видимости из-за явного forall (Сигнатуры типов декларации). Переменные типа, связанные с областью видимости forall охватывают всё определение сопутствующего объявления значения. В этом примере переменная типа a охватывает всё определение f, включая сигнатуру типа для ys. В Haskell 98 невозможно объявить тип для ys; основное преимущество переменных типа с охватом заключается в том, что это становится возможным.

Эквивалентная форма для этого примера, избегающая явных forall использует Сигнатуры типов шаблонов:

f :: [a] -> [a]
f (xs :: [aa]) = xs ++ ys
  where
    ys :: [aa]
    ys = reverse xs

В отличие от формы forall переменная типа a из сигнатуры f не охватывает уравнение(я) f. Переменная типа aa связана сигнатурой типа шаблона и охватывает правую часть уравнения f. (Поэтому нет необходимости использовать отдельную переменную типа; использование a было бы эквивалентно.)

10.17.1. Обзор

Проект основан на следующих принципах

  • Переменная типа с охватом обозначает переменную типа, а не тип. (Это изменение по сравнению с предыдущим дизайном GHC.)
  • Кроме того, различные лексические переменные типа обозначают различные переменные типа. Это означает, что каждая написанная программистом сигнатура типа (включая ту, которая содержит свободные переменные типа с охватом) обозначает жёсткий тип; то есть тип полностью известен проверяющему типа, и никаких вычислений не требуется.
  • Лексические переменные типа могут быть переименованы свободно без изменения программы.

Переменная типа с лексическим охватом может быть связана:

  • Сигнатурой типа объявления (Сигнатуры типов декларации)
  • Сигнатурой типа выражения (Сигнатуры типов выражений)
  • Сигнатурой типа шаблона (Сигнатуры типов шаблонов)
  • Объявления класса и экземпляра (Объявления класса и экземпляра)

В Haskell сигнатура типа, написанная программистом, неявно квантифицируется по своим свободным переменным типа (Раздел 4.1.2 отчета по Haskell). Переменные типа с лексическим охватом влияют на эти правила неявного квантования следующим образом: любая переменная типа, которая находится в области видимости, не универсально квантифицируется. Например, если переменная типа a находится в области видимости, то

(e :: a -> a)     means     (e :: a -> a)
(e :: b -> b)     means     (e :: forall b. b->b)
(e :: a -> b)     means     (e :: forall b. a->b)

10.17.2. Сигнатуры типов декларации

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

f :: forall a. [a] -> [a]
f (x:xs) = xs ++ [ x :: a ]

«forall a» вводит «a» в область видимости в определении «f».

Это происходит только если:

  • Квантификация в типе f явная. Например:

    g :: [a] -> [a]
    g (x:xs) = xs ++ [ x :: a ]
    

    Эта программа будет отклонена, потому что «a» не охватывает определение «g», поэтому «x::a» означает «x::forall a. a» по обычным правилам неявной квантификации Haskell.

  • Переменная типа квантифицируется единственной, синтаксически видимой, внешней forall сигнатуры типа. Например, GHC отклонит все следующие примеры:

    f1 :: forall a. forall b. a -> [b] -> [b]
    f1 _ (x:xs) = xs ++ [ x :: b ]
    
    f2 :: forall a. a -> forall b. [b] -> [b]
    f2 _ (x:xs) = xs ++ [ x :: b ]
    
    type Foo = forall b. [b] -> [b]
    
    f3 :: Foo
    f3 (x:xs) = xs ++ [ x :: b ]
    

    В f1 и f2, переменная типа b не квантифицируется внешней forall, поэтому она не находится в области видимости тела функций. Также b не находится в области видимости тела f3, так как forall спрятана под синонимом типа Foo.

  • Сигнатура задаёт тип для привязки функции или привязки голой переменной, а не для привязки с шаблоном. Например:

    f1 :: forall a. [a] -> [a]
    f1 (x:xs) = xs ++ [ x :: a ]   -- OK
    
    f2 :: forall a. [a] -> [a]
    f2 = \(x:xs) -> xs ++ [ x :: a ]   -- OK
    
    f3 :: forall a. [a] -> [a]
    Just f3 = Just (\(x:xs) -> xs ++ [ x :: a ])   -- Not OK!
    

    f1 — это привязка функции, а f2 — привязка голой переменной; в обоих случаях сигнатура типа вводит a в область видимости. Однако привязка для f3 — это привязка с шаблоном, и поэтому f3 — это новая переменная, введённая в область видимости шаблоном, а не связанная с глобальной f3. Тогда переменная типа a не находится в области видимости правой части Just f3 = ....

10.17.3. Сигнатуры типов выражений

Сигнатура типа выражения с явной квантификацией (используя forall) вводит в область видимости явно квантифицированные переменные типа в аннотированном выражении. Например:

f = runST ( (op >>= \(x :: STRef s Int) -> g x) :: forall s. ST s Bool )

Здесь сигнатура типа forall s. ST s Bool вводит переменную типа s в область видимости в аннотированном выражении (op >>= \(x :: STRef s Int) -> g x).

10.17.4. Сигнатуры типов шаблонов

Сигнатура типа может встречаться в любом шаблоне; это сигнатура типа шаблона. Например:

-- f and g assume that 'a' is already in scope
f = \(x::Int, y::a) -> x

g (x::a) = x

h ((x,y) :: (Int,Bool)) = (y,x)

В случае, когда все переменные типа в сигнатуре типа шаблона уже находятся в области видимости (т.е. связаны окружающим контекстом), дела просты: сигнатура просто ограничивает тип шаблона очевидным образом.

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

f :: forall a. [a] -> (Int, [a])
f xs = (n, zs)
  where
    (ys::[a], n) = (reverse xs, length xs) -- OK
    (zs::[a])    = xs ++ ys                     -- OK

    Just (v::b)  = ...  -- Not OK; b is not in scope

Здесь сигнатуры типов для ys и zs приемлемы, но сигнатура для v не приемлема, потому что b не находится в области видимости.

Однако во всех шаблонах, кроме привязок с шаблонами, сигнатура типа шаблона может содержать переменную типа, которая не находится в области видимости; в этом случае сигнатура вводит эту переменную типа в область видимости. Например:

-- same f and g as above, now assuming that 'a' is not already in scope
f = \(x::Int, y::a) -> x           -- 'a' is in scope on RHS of ->

g (x::a) = x :: a

hh (Just (v :: b)) = v :: b

Сигнатура типа шаблона делает переменную типа доступной в правой части уравнения.

Введение переменных типа в область видимости особенно важно для конструкторов данных с экзистенциальными типами. Например:

data T = forall a. MkT [a]

k :: T -> T
k (MkT [t::a]) =
    MkT t3
  where
    (t3::[a]) = [t,t,t]

Здесь сигнатура типа шаблона [t::a] упоминает лексическую переменную типа, которая ещё не находится в области видимости. Действительно, она не должна уже находиться в области видимости, потому что она связана соответствием шаблонов. Эффект заключается в том, чтобы ввести её в область видимости, представляя экзистенциально связанную переменную типа.

Кажется странным, что экзистенциально связанная переменная типа не должна уже находиться в области видимости. В отличие от этого, обычно привязки имён просто затемняют (делают «дыру») в области видимости внешней переменной с тем же именем. Но у нас должен быть какой-то способ введения таких переменных типа в область видимости, иначе мы не могли бы давать имена экзистенциально связанным переменным типа в последующих сигнатурах типов.

Сравните два (одинаковых) определения для примеров f, g; они оба допустимы, независимо от того, находится ли a в области видимости. Они отличаются тем, что если a уже находится в области видимости, сигнатура ограничивает шаблон, а не привязка шаблона связывает переменную.

10.17.5. Объявления классов и экземпляров

Переменные типа в заголовке объявления class или instance охватывают методы, определённые в части where. Вам даже не нужна явная forall (хотя вам разрешается явная forall в объявлении instance; см. Явная универсальная квантификация (forall)). Например:

class C a where
  op :: [a] -> a

  op xs = let ys::[a]
              ys = reverse xs
          in
          head ys

instance C b => C [b] where
  op xs = reverse (head (xs :: [[b]]))

10.18. Привязки и обобщение

10.18.1. Отключение неприятного ограничения мономорфизма

NoMonomorphismRestriction
Значение по умолчанию

включено

С версии

6.8.1

Препятствует компилятору применять ограничение мономорфизма к привязкам без явных сигнатур типов.

Ограничение мономорфизма Haskell (см. Раздел 4.5.5 отчёта Haskell) можно полностью отключить с помощью NoMonomorphismRestriction. С GHC 7.8.1 ограничение мономорфизма по умолчанию выключено в интерактивных опциях GHCi (см. Настройка параметров только для интерактивного оценивания).

10.18.2. Обобщение let

MonoLocalBinds
С версии

6.12.1

По умолчанию выводит менее полиморфные типы для локальных привязок.

Язык в стиле ML обычно обобщает тип любой переменной, связанной с let или где, так что он является максимально полиморфным. С расширением MonoLocalBinds GHC использует немного более консервативную политику, используя следующие правила:

  • Переменная является закрытой тогда и только тогда, когда

    • переменная связана let
    • выполняется одно из следующих условий:

      • переменная имеет явную сигнатуру типа без свободных переменных типа, или
      • группа её привязки полностью обобщена (см. следующий пункт)
  • Группа привязки является полностью обобщённой тогда и только тогда, когда

    • каждая из её свободных переменных импортирована или закрыта, и
    • привязка не затронута ограничением мономорфизма (Отчёт Haskell, раздел 4.5.5)

Например, рассмотрим

f x = x + 1
g x = let h y = f y * 2
          k z = z+x
      in  h x + k x

Здесь f обобщается, потому что у неё нет свободных переменных; и группа её привязки не затронута ограничением мономорфизма; и поэтому f закрыта. Такой же аргумент относится к g, за исключением того, что у неё одна закрытая свободная переменная, а именно f. Аналогично h закрыта, даже если она не связана на верхнем уровне, потому что её единственная свободная переменная f закрыта. Но k не закрыта, потому что она упоминает x, которая не закрыта (потому что она не связана let).

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

Обоснование этой более консервативной стратегии приведено в статьях «Let should not be generalised» и «Modular type inference with local assumptions», а также в соответствующей записи в блоге.

Расширение MonoLocalBinds подразумевается TypeFamilies и GADTs. Вы можете его отключить с помощью NoMonoLocalBinds, но вывод типов становится менее предсказуемым, если вы это сделаете. (Прочитайте статьи!)

10.19. Видимое применение типа

TypeApplications
С версии

8.0.1

Разрешает использование синтаксиса применения типа.

Расширение TypeApplications позволяет использовать видимое применение типа в выражениях. Вот пример: show (read @Int "5"). @Int — это видимое применение типа; оно указывает значение переменной типа в типе read.

Видимое применение типа предваряется знаком @. (Чтобы разобрать синтаксис, @ должно предваряться неидентификатором, обычно пробелом. Например, read@Int 5 не будет разборчиво.) Оно может использоваться всякий раз, когда известен полный полиморфный тип функции. Если функция является идентификатором (обычный случай), её тип считается известным только тогда, когда идентификатору задана сигнатура типа. Если идентификатору не задана сигнатура типа, видимое применение типа использовать нельзя.

GHC также допускает видимое применение вида, где пользователи могут объявлять аргументы вида, подлежащие установке в случаях вида полиморфизма. Его использование аналогично видимому применению типа на уровне термина, как указано выше.

10.19.1. Выводные и указанные переменные типа

GHC отслеживает различие между тем, что мы называем выведенными и указанными переменными типов. Только указанные переменные типов доступны для инициализации с видимым применением типа. Это хорошо иллюстрирует пример:

f :: (Eq b, Eq a) => a -> b -> Bool
f x y = (x == x) && (y == y)

g x y = (x == x) && (y == y)

Функции f и g имеют одинаковый код, но только f имеет подпись типа. Когда GHC определяет, как обработать видимое применение типа, он должен знать, какую переменную инициализировать. Таким образом, он должен иметь возможность задать порядок переменных типа в типе функции.

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

В отличие от этого, нет надежного способа сделать это для g; мы не будем знать, будет ли Eq a или Eq b перечислено первой в ограничении в типе g. Для того, чтобы видимое применение типа было устойчивым между выпусками GHC, мы запрещаем его использование с g.

Мы говорим, что переменные типа в f являются указанными, а переменные в g — выведенными. Общее правило таково: если пользователь написал переменную типа в исходной программе, она указана; если нет, она выведена.

Это правило применяется и к объявлениям типов данных. Например, если у нас есть data Proxy a = Proxy (и PolyKinds включен), то a получит вид k, где k — новая переменная вида. Поскольку k не было написано пользователем, оно не будет доступно для применения типа в типе конструктора Proxy; только a будет доступно.

Когда -fprint-explicit-foralls включено, выведенные переменные выводятся в фигурных скобках. Таким образом, тип конструктора данных Proxy из предыдущего примера будет forall {k} (a :: k). Proxy a. Мы можем наблюдать это поведение в сеансе GHCi:

> :set -XTypeApplications -fprint-explicit-foralls
> let myLength1 :: Foldable f => f a -> Int; myLength1 = length
> :type +v myLength1
myLength1 :: forall (f :: * -> *) a. Foldable f => f a -> Int
> let myLength2 = length
> :type +v myLength2
myLength2 :: forall {a} {t :: * -> *}. Foldable t => t a -> Int
> :type +v myLength2 @[]

<interactive>:1:1: error:
    • Cannot apply expression of type ‘t0 a0 -> Int’
      to a visible type argument ‘[]’
    • In the expression: myLength2 @[]

Обратите внимание, что так как myLength1 был определен с явной подписью типа, :type +v сообщает, что все его переменные типа доступны для применения типа. С другой стороны, myLength2 не было дано подписи типа. В результате все его переменные типа окружены фигурными скобками, и попытка использовать видимое применение типа с myLength2 терпит неудачу.

Также обратите внимание на использование :type +v в сеансе GHCi выше вместо :type. Это потому, что :type дает вам тип, который был бы выведен для переменной, присвоенной предоставленному выражению (то есть, тип x в let x = <expr>). Как мы видели выше с myLength2, этот тип не будет иметь переменных, доступных для видимого применения типа. С другой стороны, :type +v дает вам фактический тип предоставленного выражения. Чтобы проиллюстрировать это:

> :type myLength1
myLength1 :: forall {a} {f :: * -> *}. Foldable f => f a -> Int
> :type myLength2
myLength2 :: forall {a} {t :: * -> *}. Foldable t => t a -> Int

Использование :type может привести к выводу, что ни одна из переменных типа в подписи типа myLength1 не доступна для применения типа. Однако это не так! Используйте :type +v, если вы хотите получить наиболее точную информацию относительно свойств видимого применения типа.

10.19.2. Порядок указанных переменных

В простом случае предыдущего раздела мы можем сказать, что указанные переменные появляются в порядке слева направо. Однако не все случаи настолько просты. Вот правила в более сложных случаях:

  • Если тип идентификатора имеет forall, то порядок переменных типа, как записано в forall, сохраняется.
  • Если какие-либо из переменных зависят от других переменных (то есть, если некоторые из переменных являются переменными вида), переменные переупорядочиваются так, что переменные вида появляются перед переменными типа, сохраняя порядок слева направо по возможности. То есть, GHC выполняет устойчивую топологическую сортировку переменных. Пример:

    h :: Proxy (a :: (j, k)) -> Proxy (b :: Proxy a) -> ()
      -- as if h :: forall j k a b. ...
    

    В этом примере a зависит от j и k, а b зависит от a. Несмотря на то, что a появляется лексически перед j и k, j и k квантуются первыми, потому что a зависит от j и k. Обратите также внимание, что j и k не переупорядочиваются относительно друг друга, даже если бы это не нарушило условия зависимости.

    «Устойчивая топологическая сортировка» здесь означает, что мы выполняем этот алгоритм (который мы называем ScopedSort):

    • Работаем слева направо по списку входных переменных типа, с курсором.
    • Если переменная v в курсоре зависит от любой предыдущей переменной w, перемещаем v непосредственно перед левым таким w.
  • Аргументы типа методов класса включают переменные типа класса, а затем любые переменные, в которых отдельный метод является полиморфным. Таким образом, class Monad m where return :: a -> m a означает, что аргументы типа return — это m, a.
  • С расширением RankNTypes (Лексически сгруппированные переменные типа) можно объявить аргументы типа где-то кроме начала типа. Например, мы можем иметь pair :: forall a. a -> forall b. b -> (a, b) и затем сказать pair @Bool True @Char, который будет иметь тип Char -> (Bool, Char).
  • Частичные подписи типов (Частичные подписи типов) хорошо взаимодействуют с видимым применением типа. Если вы хотите указать только второй аргумент типа для wurble, вы можете сказать wurble @_ @Int. Первый аргумент является подстановкой, как и в частичной подписи типа. Однако, если используется при видимом применении типа/видимом применении вида, не обязательно указывать PartialTypeSignatures, и ваш код не сгенерирует предупреждение об опущенном типе.

В разделе этой справки по полиморфизму видов описано, как упорядочиваются переменные в объявлениях типов и классов (Вывод порядка переменных в объявлении типа/класса).

10.20. Неявные параметры

ImplicitParams
Since

6.8.1

Разрешить определение функций, ожидающих неявные параметры.

Неявные параметры реализованы, как описано в [Lewis2000], и активируются опцией ImplicitParams. (Большая часть следующего, пока еще довольно неполного, документации принадлежит Джеффу Льюису.)

Lewis2000

“Неявные параметры: динамическое связывание со статическими типами”, Дж. Льюис, М.Б. Шильдс, Э. Майер, Дж. Лончбери, 27-й симпозиум ACM по принципам программирования (POPL’00), Бостон, январь 2000 г.

Переменная называется динамически связанной, когда она связана контекстом вызова функции, и статически связанной, когда она связана контекстом вызываемой функции. В Haskell все переменные статически связаны. Динамическое связывание переменных восходит к Lisp, но было позже отброшено в более современных воплощениях, таких как Scheme. Динамическое связывание может быть очень запутанным в нетипизированном языке, и, к сожалению, типизированные языки, особенно языки с типами Хиндли-Милнера, такие как Haskell, поддерживают только статическое связывание переменных.

Однако, путем простого расширения системы типов классов Haskell, мы можем поддерживать динамическое связывание. В основном, мы выражаем использование динамически связанной переменной как ограничение по типу. Эти ограничения приводят к типам вида (?x::t') => t, который говорит «эта функция использует динамически связанную переменную ?x типа t'». Например, следующее выражает тип функции сортировки, неявным образом параметризованной функцией сравнения с именем cmp.

sort :: (?cmp :: a -> a -> Bool) => [a] -> [a]

Ограничения динамического связывания — это просто новый вид предиката в системе типов классов.

Неявный параметр появляется в выражении, используя специальную форму ?x, где x — любой допустимый идентификатор (например, ord ?x — допустимое выражение). Использование этого конструктора также вводит новое ограничение динамического связывания в типе выражения. Например, следующее определение показывает, как мы можем определить функцию сортировки с неявным параметром в терминах явно параметризованной функции sortBy:

sortBy :: (a -> a -> Bool) -> [a] -> [a]

sort   :: (?cmp :: a -> a -> Bool) => [a] -> [a]
sort    = sortBy ?cmp

10.20.1. Ограничения типов неявных параметров

Ограничения динамического связывания ведут себя как и другие ограничения типов классов, т. е. они автоматически распространяются. Таким образом, когда используется функция, ее неявные параметры наследуются функцией, которая ее вызвала. Например, наша функция sort может быть использована для выбора наименьшего значения в списке:

least   :: (?cmp :: a -> a -> Bool) => [a] -> a
least xs = head (sort xs)

Без усилий параметр ?cmp распространяется, чтобы стать параметром least также. С явными параметрами, по умолчанию, параметры должны всегда явно распространяться. С неявными параметрами, по умолчанию, они всегда распространяются.

Ограничение типа с неявным параметром отличается от других ограничений типа класса следующим образом: все использования конкретного неявного параметра должны иметь одинаковый тип. Это означает, что тип (?x, ?x) равен (?x::a) => (a,a), а не (?x::a, ?x::b) => (a, b), как это было бы в случае с ограничениями типа класса.

Вы не можете использовать неявный параметр в контексте объявления класса или экземпляра. Например, оба эти объявления некорректны:

class (?x::Int) => C a where ...
instance (?x::a) => Foo [a] where ...

Причина: точный неявный параметр, который вы используете, зависит от того, где вы вызываете функцию. Но «вызов» объявлений экземпляров выполняется компилятором в фоновом режиме, поэтому сложно определить, где именно это происходит. Самый простой способ — запретить использование этих типов.

Ограничения неявных параметров не приводят к неоднозначности. Например, рассмотрим:

f :: (?x :: [a]) => Int -> Int
f n = n + length ?x

g :: (Read a, Show a) => String -> String
g s = show (read s)

Здесь g имеет неоднозначный тип и будет отклонен, но f допустим. Связывание для ?x в месте вызова f является вполне однозначным и определяет тип a.

10.20.2. Связывания неявных параметров

Неявный параметр связывается с использованием стандартных форм связывания let или where. Например, мы определяем функцию min связывая cmp.

min :: Ord a => [a] -> a
min  = let ?cmp = (<=) in least

Группа связываний неявных параметров может находиться в любом месте, где может находиться обычная группа связываний Haskell, за исключением верхнего уровня. То есть, они могут находиться в let (включая список с пониманием, или обозначение do, или условия шаблонов), или в where фрагменте. Обратите внимание на следующие моменты:

  • Группа связываний неявных параметров должна представлять собой набор простых связываний с переменными в стиле неявных параметров (без связываний в стиле функций и без сигнатур типов); эти связывания не являются полиморфными или рекурсивными.
  • Вы не можете смешивать связывания неявных параметров с обычными связываниями в одном выражении let; вместо этого используйте два вложенных let фрагмента. (В случае where вы заблокированы, поскольку вы не можете вкладывать where фрагменты.)
  • Вы можете поместить несколько связываний неявных параметров в одну группу связываний; но они не рассматриваются как взаиморекурсивная группа (как обычные let связывания). Вместо этого они обрабатываются как нерекурсивная группа, одновременно связывающая все неявные параметры. Связывания не вложены и могут быть переупорядочены без изменения смысла программы. Например, рассмотрим:

    f t = let { ?x = t; ?y = ?x+(1::Int) } in ?x + ?y
    

    Использование ?x в связывании для ?y не «видит» связывание для ?x, поэтому тип f равен

    f :: (?x::Int) => Int -> Int
    

10.20.3. Неявные параметры и полиморфная рекурсия

Рассмотрим эти два определения:

len1 :: [a] -> Int
len1 xs = let ?acc = 0 in len_acc1 xs

len_acc1 [] = ?acc
len_acc1 (x:xs) = let ?acc = ?acc + (1::Int) in len_acc1 xs

------------

len2 :: [a] -> Int
len2 xs = let ?acc = 0 in len_acc2 xs

len_acc2 :: (?acc :: Int) => [a] -> Int
len_acc2 [] = ?acc
len_acc2 (x:xs) = let ?acc = ?acc + (1::Int) in len_acc2 xs

Единственное различие между двумя группами заключается в том, что во второй группе len_acc задана сигнатура типа. В первом случае len_acc1 является мономорфным по своему правому члену, поэтому неявный параметр ?acc не передаётся рекурсивному вызову. Во втором случае, поскольку len_acc2 имеет сигнатуру типа, рекурсивный вызов делается к полиморфной версии, которая принимает ?acc в качестве неявного параметра. Поэтому мы получаем следующие результаты в GHCi:

Prog> len1 "hello"
0
Prog> len2 "hello"
5

Добавление сигнатуры типа радикально изменяет результат! Это довольно неинтуитивное явление, на которое стоит обратить внимание.

10.20.4. Неявные параметры и мономорфизм

GHC применяет ненавистное ограничение мономорфизма (раздел 4.5.5 отчета Haskell) к неявным параметрам. Например, рассмотрим:

f :: Int -> Int
f v = let ?x = 0     in
      let y = ?x + v in
      let ?x = 5     in
      y

Поскольку связывание для y попадает под ограничение мономорфизма, оно не обобщается, поэтому тип y просто Int, а не (?x::Int) => Int. Следовательно, (f 9) возвращает результат 9. Если вы добавите сигнатуру типа для y, тогда y получит тип (?x::Int) => Int, поэтому появление y в теле let увидит внутреннее связывание ?x, поэтому (f 9) вернет 14.

10.21. Полиморфизм произвольного ранга

RankNTypes
Подразумевает

ExplicitForAll

С

6.8.1

Разрешает типы произвольного ранга.

Rank2Types
С

6.8.1

Устаревшее псевдоним для RankNTypes.

Система типов GHC поддерживает полиморфизм произвольного ранга явное универсальное квантификацию в типах. Например, все следующие типы допустимы:

f1 :: forall a b. a -> b -> a
g1 :: forall a b. (Ord a, Eq  b) => a -> b -> a

f2 :: (forall a. a->a) -> Int -> Int
g2 :: (forall a. Eq a => [a] -> a -> Bool) -> Int -> Int

f3 :: ((forall a. a->a) -> Int) -> Bool -> Bool

f4 :: Int -> (forall a. a -> a)

Здесь, f1 и g1 — типы 1-го ранга, и их можно записать на стандартном Haskell (например f1 :: a->b->a). forall делает явным универсальное квантификацию, которое неявно добавляется Haskell.

Функции f2 и g2 имеют типы 2-го ранга; forall находится слева от стрелки функции. Как показывает g2, полиморфный тип слева от стрелки функции может быть перегружен.

Функция f3 имеет тип 3-го ранга; она имеет типы 2-го ранга слева от стрелки функции.

Флаг языка RankNTypes (который подразумевает ExplicitForAll) разрешает типы высшего ранга. То есть, вы можете вкладывать forall сколь угодно глубоко в стрелки функций. Например, тип forall (также называемый «схемой типа»), включая контекст типа класса, допустим:

  • Слева или справа (см. f4, например) от стрелки функции
  • В качестве аргумента конструктора или типа поля в объявлении типа данных. Например, любой из f1, f2, f3, g1, g2 выше был бы допустимыми сигнатурами типов поля.
  • В качестве типа неявного параметра
  • В сигнатуре типа шаблона (см. Локально видимые переменные типа)

Флаг RankNTypes также необходим для любого типа с forall или контекстом справа от стрелки (например, f :: Int -> forall a. a->a, или g :: Int -> Ord a => a -> a). Такие типы технически являются типом 1-го ранга, но явно не Haskell-98, и дополнительный флаг не показался оправданным.

В частности, в объявлениях data и newtype аргументы конструктора могут быть полиморфными типами любого ранга; см. примеры в Примеры. Обратите внимание, что объявленные типы всё же всегда мономорфны. Это важно, потому что по умолчанию GHC не будет подставлять переменные типа в полиморфный тип (Импредикативный полиморфизм).

Устаревший флаг языка Rank2Types является синонимом для RankNTypes. Раньше они определяли более тонкие различия, которые GHC больше не делает.

10.21.1. Примеры

Это примеры объявлений data и newtype, где конструкторы данных имеют полиморфные аргументы типа:

data T a = T1 (forall b. b -> b -> b) a

data MonadT m = MkMonad { return :: forall a. a -> m a,
                          bind   :: forall a b. m a -> (a -> m b) -> m b
                        }

newtype Swizzle = MkSwizzle (forall a. Ord a => [a] -> [a])

Конструкторы имеют типы 2-го ранга:

T1 :: forall a. (forall b. b -> b -> b) -> a -> T a

MkMonad :: forall m. (forall a. a -> m a)
                  -> (forall a b. m a -> (a -> m b) -> m b)
                  -> MonadT m

MkSwizzle :: (forall a. Ord a => [a] -> [a]) -> Swizzle

В более ранних версиях GHC, было возможно опустить forall в типе конструктора, если существовал явный контекст. Например:

newtype Swizzle' = MkSwizzle' (Ord a => [a] -> [a])

Начиная с GHC 8.0, объявления, такие как MkSwizzle' будут вызывать ошибку области видимости.

Что касается сигнатур типов, неявное квантификация происходит и для неоперегруженных типов. Итак, если вы напишете это:

f :: (a -> a) -> a

будто вы написали это:

f :: forall a. (a -> a) -> a

То есть, поскольку переменная типа a не входит в область видимости, она неявно универсально квантифицируется.

Вы создаёте значения типов T1, MonadT, Swizzle путём применения конструктора к подходящим значениям, как обычно. Например,

a1 :: T Int
a1 = T1 (\xy->x) 3

a2, a3 :: Swizzle
a2 = MkSwizzle sort
a3 = MkSwizzle reverse

a4 :: MonadT Maybe
a4 = let r x = Just x
     b m k = case m of
           Just y -> k y
           Nothing -> Nothing
     in
     MkMonad r b

mkTs :: (forall b. b -> b -> b) -> a -> [T a]
mkTs f x y = [T1 f x, T1 f y]

Тип аргумента, как обычно, может быть более общим, чем требуемый тип, как показано в (MkSwizzle reverse). (reverse не нуждается в ограничении Ord.)

При использовании сопоставления с образцом, связанные переменные могут теперь иметь полиморфные типы. Например:

f :: T a -> a -> (a, Char)
f (T1 w k) x = (w k x, w 'c' 'd')

g :: (Ord a, Ord b) => Swizzle -> [a] -> (a -> b) -> [b]
g (MkSwizzle s) xs f = s (map f (s xs))

h :: MonadT m -> [m a] -> m [a]
h m [] = return m []
h m (x:xs) = bind m x          $ \y ->
             bind m (h m xs)   $ \ys ->
             return m (y:ys)

В функции h мы используем селекторы записей return и bind для извлечения полиморфных связывающих и возвращающих функций из структуры данных MonadT вместо использования сопоставления с образцом.

10.21.2. Вывод типа

В общем случае, вывод типа для типов произвольного ранга неразрешим. GHC использует алгоритм, предложенный Odersky и Laufer («Putting type annotations to work», POPL’96), чтобы получить разрешимый алгоритм, потребовав некоторой помощи от программиста. У нас пока нет формального описания «некоторой помощи», но правило таково:

Для переменной x, связанной лямбдой или case, либо программист предоставляет явный полиморфный тип для x, либо GHC предположит, что тип x не содержит forall.

Что означает «предоставить» явный тип для x? Вы можете сделать это, предоставив сигнатуру типа для x напрямую, используя сигнатуру типа шаблона (Локально видимые переменные типа), таким образом:

\ f :: (forall a. a->a) -> (f True, f 'c')

В качестве альтернативы, вы можете дать сигнатуру типа окружающему контексту, который GHC может «протолкнуть вниз», чтобы найти тип переменной:

(\ f -> (f True, f 'c')) :: (forall a. a->a) -> (Bool,Char)

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

h :: (forall a. a->a) -> (Bool,Char)
h f = (f True, f 'c')

Вам не нужно указывать тип подписи, если переменная лямбда-связи является аргументом конструктора. Вот пример, который мы видели ранее:

f :: T a -> a -> (a, Char)
f (T1 w k) x = (w k x, w 'c' 'd')

Здесь нам не нужно указывать тип подписи для w, потому что это аргумент конструктора T1 , и этого достаточно для GHC.

10.21.3. Неявное квантификация

GHC выполняет неявное квантификацию следующим образом. На внешнем уровне (только) типов, написанных пользователем, если и только если нет явного forall, GHC находит все переменные типов, упомянутые в типе, которые еще не в области видимости, и универсально их квантифицирует. Например, следующие пары эквивалентны:

f :: a -> a
f :: forall a. a -> a

g (x::a) = let
              h :: a -> b -> b
              h x y = y
           in ...
g (x::a) = let
              h :: forall b. a -> b -> b
              h x y = y
           in ...

Обратите внимание, что GHC всегда добавляет неявные квантификаторы на внешнем уровне типа, написанного пользователем; он не находит наименее возможную точку квантификации. Например:

f :: (a -> a) -> Int
         -- MEANS
f :: forall a. (a -> a) -> Int
         -- NOT
f :: (forall a. a -> a) -> Int


g :: (Ord a => a -> a) -> Int
         -- MEANS
g :: forall a. (Ord a => a -> a) -> Int
         -- NOT
g :: (forall a. Ord a => a -> a) -> Int

Если вам нужен последний тип, вы можете написать свои forall явно. Действительно, это настоятельно рекомендуется для типов ранга 2.

Иногда нет «внешнего уровня», в этом случае не происходит неявной квантификации:

data PackMap a b s t = PackMap (Monad f => (a -> f b) -> s -> f t)

Это отклоняется, потому что нет «внешнего уровня» для типов в правой части (было бы очевидно ужасно добавить дополнительные параметры к PackMap), поэтому не происходит неявной квантификации, и объявление отклоняется (с «f находится вне области видимости»). Решение: используйте явное forall:

data PackMap a b s t = PackMap (forall f. Monad f => (a -> f b) -> s -> f t)

10.22. Импредикативный полиморфизм

ImpredicativeTypes
Подразумевает

RankNTypes

С

6.10.1

Разрешить импредикативные полиморфные типы.

В общем случае GHC будет инстанцировать полиморфную функцию только в мономорфном типе (среди тех, у которых нет foralls). Например,

runST :: (forall s. ST s a) -> a
id :: forall b. b -> b

foo = id runST   -- Rejected

Определение foo отклоняется, так как пришлось бы инстанцировать тип id с b := (forall s. ST s a) -> a, и это не разрешено. Инстанцирование полиморфных переменных типов полиморфными типами называется импредикативным полиморфизмом.

GHC имеет чрезвычайно нестабильную поддержку импредикативного полиморфизма, включенного с помощью ImpredicativeTypes. Если бы это работало, это означало бы, что вы могли вызывать полиморфную функцию в полиморфном типе и параметризовать структуры данных над полиморфными типами. Например:

f :: Maybe (forall a. [a] -> [a]) -> Maybe ([Int], [Char])
f (Just g) = Just (g [3], g "hello")
f Nothing  = Nothing

Обратите внимание, что тип Maybe параметризован полиморфным типом (forall a. [a] -> [a]). Однако расширение следует рассматривать как высокоэкспериментальное и определенно не поддерживаемое. Вы можете его попробовать, но не полагайтесь на его стабильную работу или одинаковый результат в последующих версиях. Для получения более подробной информации см. эту страницу вики.

Если вам нужен импредикативный полиморфизм, основным способом решения является использование обертки newtype. Пример id runST можно записать с помощью этого обходного пути следующим образом:

runST :: (forall s. ST s a) -> a
id :: forall b. b -> b

newtype Wrap a = Wrap { unWrap :: (forall s. ST s a) -> a }

foo :: (forall s. ST s a) -> a
foo = unWrap (id (Wrap runST))
      -- Here id is called at monomorphic type (Wrap a)

10.23. Типизированные дыры

Типизированные дыры — это функция GHC, которая позволяет использовать специальные маркеры, записанные с ведущим символом подчеркивания (например, «_», «_foo», «_bar»), как выражения. Во время компиляции эти дыры сгенерируют сообщение об ошибке, которое опишет ожидаемый тип в месте дыры, информацию о происхождении любых свободных переменных типов и список локальных связей, которые могут помочь заполнить дыру, а также связи в области видимости, которые подходят к типу дыры, которые могут помочь заполнить дыру фактическим кодом. Типизированные дыры всегда включены в GHC.

Цель типизированных дыр — помочь в написании кода Haskell, а не изменить систему типов. Типизированные дыры могут использоваться для получения дополнительной информации от проверяющего типов, которую иначе трудно получить. Обычно с помощью GHCi пользователи могут просмотреть (выведенные) типы подписей всех связей верхнего уровня. Однако этот метод менее удобен с терминами, которые не определены на верхнем уровне или внутри сложных выражений. Дыры позволяют пользователю проверять тип термина, который они собираются написать.

Например, компиляция следующего модуля с помощью GHC:

f :: a -> a
f x = _

вызовет следующую ошибку:

hole.hs:2:7:
    Found hole `_' with type: a
    Where: `a' is a rigid type variable bound by
               the type signature for f :: a -> a at hole.hs:1:6
    In the expression: _
    In an equation for `f': f x = _
    Relevant bindings include
      x :: a (bound at hole.hs:2:3)
      f :: a -> a (bound at hole.hs:2:1)
    Valid hole fits include x :: a (bound at hole.hs:2:3)

Вот некоторые дополнительные сведения:

  • Ошибка «Found hole» обычно завершает компиляцию, как и любая другая ошибка типа. В конце концов, вы опустили часть кода из своей программы. Тем не менее, вы можете запустить и протестировать часть кода, содержащую дыры, с помощью флага -fdefer-typed-holes. Этот флаг откладывает ошибки, создаваемые типизированными дырами, до времени выполнения и преобразует их в предупреждения во время компиляции. Эти предупреждения в свою очередь можно полностью подавить с помощью -Wno-typed-holes.

    Такое же поведение для ошибок «Variable out of scope» — по умолчанию оно завершает компиляцию. Вы можете отложить такие ошибки с помощью флага -fdefer-out-of-scope-variables. Этот флаг откладывает ошибки, создаваемые переменными, не находящимися в области видимости, до времени выполнения и преобразует их в предупреждения во время компиляции. Эти предупреждения в свою очередь можно полностью подавить с помощью -Wno-deferred-out-of-scope-variables.

    В результате дыра или переменная будет вести себя как undefined, но с дополнительными преимуществами: она показывает предупреждение во время компиляции и покажет то же сообщение, если она будет вычислена во время выполнения. Это поведение аналогично варианту -fdefer-type-errors, который подразумевает -fdefer-typed-holes и -fdefer-out-of-scope-variables. См. Откладывание ошибок типов до времени выполнения.

  • Все не связанные идентификаторы обрабатываются как типизированные дыры, независимо от того, начинаются ли они с подчеркивания. Единственное различие — в сообщении об ошибке:

    cons z = z : True : _x : y
    

    выдает ошибки

    Foo.hs:3:21: error:
       Found hole: _x :: Bool
       Or perhaps ‘_x’ is mis-spelled, or not in scope
       In the first argument of ‘(:)’, namely ‘_x’
       In the second argument of ‘(:)’, namely ‘_x : y’
       In the second argument of ‘(:)’, namely ‘True : _x : y’
       Relevant bindings include
         z :: Bool (bound at Foo.hs:3:6)
       cons :: Bool -> [Bool] (bound at Foo.hs:3:1)
       Valid hole fits include
         z :: Bool (bound at mpt.hs:2:6)
         otherwise :: Bool
           (imported from ‘Prelude’ at mpt.hs:1:8-10
           (and originally defined in ‘GHC.Base’))
         False :: Bool
           (imported from ‘Prelude’ at mpt.hs:1:8-10
           (and originally defined in ‘GHC.Types’))
         True :: Bool
           (imported from ‘Prelude’ at mpt.hs:1:8-10
           (and originally defined in ‘GHC.Types’))
         maxBound :: forall a. Bounded a => a
           with maxBound @Bool
           (imported from ‘Prelude’ at mpt.hs:1:8-10
           (and originally defined in ‘GHC.Enum’))
         minBound :: forall a. Bounded a => a
           with minBound @Bool
           (imported from ‘Prelude’ at mpt.hs:1:8-10
           (and originally defined in ‘GHC.Enum’))
    
    Foo.hs:3:26: error:
        Variable not in scope: y :: [Bool]
    

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

  • Не связанные идентификаторы с одинаковым именем никогда не объединяются, даже в пределах одной функции, но отображаются индивидуально. Например:

    cons = _x : _x
    

    приводит к следующим ошибкам:

    unbound.hs:1:8:
        Found hole '_x' with type: a
        Where: `a' is a rigid type variable bound by
                   the inferred type of cons :: [a] at unbound.hs:1:1
        In the first argument of `(:)', namely `_x'
        In the expression: _x : _x
        In an equation for `cons': cons = _x : _x
        Relevant bindings include cons :: [a] (bound at unbound.hs:1:1)
    
    unbound.hs:1:13:
        Found hole: _x :: [a]
        Where: ‘a’ is a rigid type variable bound by
                the inferred type of cons :: [a]
                at unbound.hs:3:1-12
        Or perhaps ‘_x’ is mis-spelled, or not in scope
        In the second argument of ‘(:)’, namely ‘_x’
        In the expression: _x : _x
        In an equation for ‘cons’: cons = _x : _x
        Relevant bindings include cons :: [a] (bound at unbound.hs:3:1)
        Valid hole fits include
          cons :: forall a. [a]
            with cons @a
            (defined at mpt.hs:3:1)
          mempty :: forall a. Monoid a => a
            with mempty @[a]
            (imported from ‘Prelude’ at mpt.hs:1:8-10
            (and originally defined in ‘GHC.Base’))
    

    Обратите внимание на два разных типа, которые сообщаются для двух разных случаев _x.

  • Для использования типизированных дыр не требуется никаких языковых расширений. Лексема «_» ранее была недействительной в Haskell, но теперь имеет более информативное сообщение об ошибке. Лексема «_x» — это совершенно законная переменная, и ее поведение не изменяется, когда она находится в области видимости. Например,

    f _x = _x + 1
    

    не вызывает никаких ошибок. Только переменная, которая не находится в области видимости (вне зависимости от того, начинается ли она с подчеркивания), обрабатывается как ошибка (так было всегда), хотя теперь с более информативным сообщением об ошибке.

  • Не связанные конструкторы данных, используемые в выражениях, ведут себя точно так же, как и выше. Однако не связанные конструкторы данных, используемые в шаблонах, не могут быть отложены и вместо этого останавливают компиляцию. (С точки зрения реализации они сообщаются обработчиком переименований, а не проверяющим типов.)
  • Список допустимых совпадений дыр определяется путем проверки, какие связи в области видимости подходят в дыру. Например, при компиляции следующего модуля с помощью GHC:

    import Data.List (inits)
    
    g :: [String]
    g = _ "hello, world"
    

    выдаются следующие ошибки:

    • Found hole: _ :: [Char] -> [String]
    • In the expression: _
      In the expression: _ "hello, world"
      In an equation for ‘g’: g = _ "hello, world"
    • Relevant bindings include g :: [String] (bound at mpt.hs:6:1)
      Valid hole fits include
        lines :: String -> [String]
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘base-4.11.0.0:Data.OldList’))
        words :: String -> [String]
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘base-4.11.0.0:Data.OldList’))
        inits :: forall a. [a] -> [[a]]
          with inits @Char
          (imported from ‘Data.List’ at mpt.hs:4:19-23
           (and originally defined in ‘base-4.11.0.0:Data.OldList’))
        repeat :: forall a. a -> [a]
          with repeat @String
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘GHC.List’))
        fail :: forall (m :: * -> *). Monad m => forall a. String -> m a
          with fail @[] @String
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘GHC.Base’))
        return :: forall (m :: * -> *). Monad m => forall a. a -> m a
          with return @[] @String
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘GHC.Base’))
        pure :: forall (f :: * -> *). Applicative f => forall a. a -> f a
          with pure @[] @String
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘GHC.Base’))
        read :: forall a. Read a => String -> a
          with read @[String]
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘Text.Read’))
        mempty :: forall a. Monoid a => a
          with mempty @([Char] -> [String])
          (imported from ‘Prelude’ at mpt.hs:3:8-9
           (and originally defined in ‘GHC.Base’))
    

Существует несколько флагов для управления количеством контекстной информации, отображаемой для типизированных дыр:

-fshow-hole-constraints

При сообщении о типизированных дырах также печатаются ограничения, которые находятся в области видимости. Пример:

f :: Eq a => a -> Bool
f x = _

приводит к следующему сообщению:

show_constraints.hs:4:7: error:
    • Found hole: _ :: Bool
    • In the expression: _
      In an equation for ‘f’: f x = _
    • Relevant bindings include
        x :: a (bound at show_constraints.hs:4:3)
        f :: a -> Bool (bound at show_constraints.hs:4:1)
      Constraints include Eq a (from show_constraints.hs:3:1-22)
      Valid hole fits include
        otherwise :: Bool
        False :: Bool
        True :: Bool
        maxBound :: forall a. Bounded a => a
          with maxBound @Bool
        minBound :: forall a. Bounded a => a
          with minBound @Bool

10.23.1. Допустимые совпадения дыр

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

-fno-show-valid-hole-fits
По умолчанию

выключено

Этот флаг можно переключить, чтобы полностью отключить отображение допустимых совпадений дыр.

-fmax-valid-hole-fits=⟨n⟩
По умолчанию

6

Список допустимых совпадений дыр ограничен отображением до 6 совпадений на дыру. Количество отображаемых совпадений можно установить с помощью этого флага. Отключение ограничения с помощью -fno-max-valid-hole-fits отображает все найденные совпадения.

-fshow-type-of-hole-fits
По умолчанию

включено

По умолчанию совпадения дыр показывают тип совпадения дыры. Это можно отключить с помощью обратного этого флага.

-fshow-type-app-of-hole-fits
По умолчанию

включено

По умолчанию совпадения дыр показывают тип применения, необходимого для соответствия этой дыры типу дыры, например, для дыры (_ :: Int -> [Int]), mempty — это совпадение дыры с mempty @(Int -> [Int]). Это можно отключить, использовав обратный этому флагу.

-fshow-docs-of-hole-fits
По умолчанию

выкл.

Иногда название и тип допустимого соответствия отверстия недостаточны для понимания его смысла. Этот флаг добавляет документацию к сообщению, если она доступна (и модуль, из которого происходит функция, был скомпилирован с флагом -haddock).

-fshow-type-app-vars-of-hole-fits
По умолчанию

вкл.

По умолчанию, соответствия отверстий показывают приложение типа, необходимое для соответствия этого отверстия типу отверстия, например, для отверстия (_ :: Int -> [Int]), mempty :: Monoid a => a является соответствием отверстия с mempty @(Int -> [Int]). Этот флаг переключает отображение a ~ (Int -> [Int]) вместо mempty @(Int -> [Int]) в части where сообщения о допустимом соответствии отверстия.

-fshow-provenance-of-hole-fits
По умолчанию

вкл.

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

-funclutter-valid-hole-fits
По умолчанию

выкл.

Этот флаг можно переключить, чтобы уменьшить подробность предложений для допустимых соответствий отверстий, не отображая происхождения и применения типов предложений.

10.23.1.1. Уточняющие соответствия отверстий

Когда флаг -frefinement-level-hole-fits=⟨n⟩ установлен на значение, n большее, чем 0, GHC предложит список допустимых уточняющих соответствий отверстий, которые являются допустимыми соответствиями отверстий, которым требуется до n уровней дополнительного уточнения для завершения, где каждый уровень представляет дополнительное отверстие в соответствии отверстия, которое необходимо заполнить. Например, рассмотрим отверстие в

f :: [Integer] -> Integer
f = _

Когда уровень уточнения не установлен, будут предложены только допустимые соответствия отверстий:

Valid hole fits include
  f :: [Integer] -> Integer
  head :: forall a. [a] -> a
    with head @Integer
  last :: forall a. [a] -> a
    with last @Integer
  maximum :: forall (t :: * -> *).
              Foldable t =>
              forall a. Ord a => t a -> a
    with maximum @[] @Integer
  minimum :: forall (t :: * -> *).
              Foldable t =>
              forall a. Ord a => t a -> a
    with minimum @[] @Integer
  product :: forall (t :: * -> *).
              Foldable t =>
              forall a. Num a => t a -> a
    with product @[] @Integer
  sum :: forall (t :: * -> *).
          Foldable t =>
          forall a. Num a => t a -> a
    with sum @[] @Integer

Однако, при установке -frefinement-level-hole-fits=⟨n⟩, например, на 1, также будет предложен список уточняющих соответствий отверстий, в данном случае:

Valid refinement hole fits include
  foldl1 (_ :: Integer -> Integer -> Integer)
    with foldl1 @[] @Integer
    where foldl1 :: forall (t :: * -> *).
                    Foldable t =>
                    forall a. (a -> a -> a) -> t a -> a
  foldr1 (_ :: Integer -> Integer -> Integer)
    with foldr1 @[] @Integer
    where foldr1 :: forall (t :: * -> *).
                    Foldable t =>
                    forall a. (a -> a -> a) -> t a -> a
  const (_ :: Integer)
    with const @Integer @[Integer]
    where const :: forall a b. a -> b -> a
  ($) (_ :: [Integer] -> Integer)
    with ($) @'GHC.Types.LiftedRep @[Integer] @Integer
    where ($) :: forall a b. (a -> b) -> a -> b
  fail (_ :: String)
    with fail @((->) [Integer]) @Integer
    where fail :: forall (m :: * -> *).
                  Monad m =>
                  forall a. String -> m a
  return (_ :: Integer)
    with return @((->) [Integer]) @Integer
    where return :: forall (m :: * -> *). Monad m => forall a. a -> m a
  (Some refinement hole fits suppressed;
    use -fmax-refinement-hole-fits=N or -fno-max-refinement-hole-fits)

Это показывает, что отверстие можно заменить, например, foldl1 _. Несмотря на то, что это не исправляет отверстие, это может помочь пользователям понять, какие у них есть варианты.

-frefinement-level-hole-fits=⟨n⟩
По умолчанию

выкл.

Список допустимых уточняющих соответствий отверстий генерируется путём рассмотрения соответствий отверстий с различным количеством дополнительных отверстий. Количество отверстий в уточнении можно установить с помощью этого флага. Если флаг установлен на 0 или вообще не установлен, допустимые уточняющие соответствия отверстий не будут предложены.

-fabstract-refinement-hole-fits
По умолчанию

выкл.

Допустимый список допустимых уточняющих соответствий отверстий может часто увеличиваться, когда уровень уточнения >= 2, с отверстиями, такими как head _ _ или fst _ _, которые являются допустимыми уточнениями, но которые вряд ли будут актуальны, так как одно или несколько отверстий по-прежнему полностью открыты, в том смысле, что ни тип, ни вид этих отверстий не ограничены предлагаемым идентификатором вообще. По умолчанию такие отверстия не сообщаются. Включив этот флаг, такие отверстия включаются в список допустимых уточняющих соответствий отверстий.

-fmax-refinement-hole-fits=⟨n⟩
По умолчанию

6

Список допустимых уточняющих соответствий отверстий ограничен отображением до 6 соответствий отверстий на каждое отверстие. Количество отображаемых соответствий отверстий можно задать с помощью этого флага. Отключение ограничения с помощью -fno-max-refinement-hole-fits отображает все найденные соответствия отверстий.

-fshow-hole-matches-of-hole-fits
По умолчанию

вкл.

Типы дополнительных отверстий в уточняющих соответствиях отверстий отображаются в выводе, например, foldl1 (_ :: a -> a -> a) является уточнением для отверстия _ :: [a] -> a. Если этот флаг выключен, вывод будет отображать только foldl1 _, который может быть использован как непосредственная замена отверстию без необходимости -XScopedTypeVariables.

10.23.1.2. Сортировка допустимых соответствий отверстий

В настоящее время существует два способа сортировки допустимых соответствий отверстий. Сортировка может быть переключена с помощью -fsort-valid-hole-fits

-fno-sort-valid-hole-fits
По умолчанию

выкл.

По умолчанию допустимые соответствия отверстий сортируются для отображения наиболее релевантных соответствий отверстий в верхней части списка допустимых соответствий отверстий. Это можно отключить с помощью этого флага.

-fsort-by-size-hole-fits
По умолчанию

вкл.

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

-fsort-by-subsumption-hole-fits
По умолчанию

выкл.

Альтернативная сортировка. Сортирует, проверяя, какие соответствия отверстий подразумевают другие соответствия отверстий, таким образом, если соответствие отверстия a может быть использовано как соответствие отверстия для соответствия отверстия b, то b появляется перед a в выводе. Это точнее, чем сортировка по умолчанию, но также намного медленнее, так как проверка подстановки должна выполняться для каждой пары допустимых соответствий отверстий.

10.24. Частичные подписи типов

PartialTypeSignatures
С

7.10.1

Проверяющий типов позволит выведенным типам для отверстий.

Частичная подпись типа — это подпись типа, содержащая специальные плейсхолдеры, называемые заполнительными символами. Заполнительный символ записывается как нижнее подчеркивание (например, «_») или, если NamedWildCards включен, любой идентификатор с ведущим нижним подчеркиванием (например, «_foo», «_bar»). Частичные подписи типов — это к подписям типов то же, что Типовые отверстия к выражениям. Во время компиляции эти заполнительные символы или отверстия будут генерировать сообщение об ошибке, которое описывает, какой тип был выведен в расположении отверстия, и информацию о происхождении любых свободных переменных типа. GHC сообщает такие сообщения об ошибках по умолчанию.

В отличие от Типовых отверстий, которые делают программу неполной и приведут к ошибкам при их вычислении, это не обязательно должно быть так для отверстий в подписях типов. Проверяющий типов способен (в большинстве случаев) проверить связывание с подписью типа или без неё. Частичная подпись типа заполняет разрыв между двумя крайностями, программист может выбрать, какие части типа аннотировать, а какие оставить на проверку типа для вывода.

По умолчанию проверяющий типов будет сообщать сообщение об ошибке для каждого отверстия в частичной подписи типа, сообщая программисту о выведенном типе. Когда расширение PartialTypeSignatures включено, проверяющий типов будет принимать выведенный тип для каждого отверстия, генерируя предупреждения вместо ошибок. Кроме того, эти предупреждения могут быть отключены с помощью флага -Wno-partial-type-signatures.

Однако, поскольку GHC должен вывести тип, когда часть типа оставлена, он не может использовать полиморфную рекурсию. То же ограничение действует, когда подпись типа полностью опущена.

10.24.1. Синтаксис

Частичная подпись типа имеет следующий вид: forall a b .. . (C1, C2, ..) => tau. Она состоит из трёх частей:

  • Переменные типа: a b ..
  • Ограничения: (C1, C2, ..)
  • (Моно)тип: tau

Мы различаем три вида заполнительных символов.

10.24.1.1. Заполнительные символы типов

Заполнительные символы, встречающиеся в монотипной (тау) части подписи типа, являются заполнительными символами типов («тип» часто опускается, так как это значение по умолчанию для вида заполнительного символа). Заполнительные символы типов могут быть приведены к любому монотипу, например, Bool или Maybe [Bool], включая функции и типы высшего порядка, такие как (Int -> Bool) или Maybe.

not' :: Bool -> _
not' x = not x
-- Inferred: Bool -> Bool

maybools :: _
maybools = Just [True]
-- Inferred: Maybe [Bool]

just1 :: _ Int
just1 = Just 1
-- Inferred: Maybe Int

filterInt :: _ -> _ -> [Int]
filterInt = filter -- has type forall a. (a -> Bool) -> [a] -> [a]
-- Inferred: (Int -> Bool) -> [Int] -> [Int]

Например, первый заполнительный символ в подписи типа not' выведет следующее сообщение об ошибке:

Test.hs:4:17: error:
    • Found type wildcard ‘_’ standing for ‘Bool’
      To use the inferred type, enable PartialTypeSignatures
    • In the type signature:
        not' :: Bool -> _
    • Relevant bindings include
        not' :: Bool -> Bool (bound at Test.hs:5:1)

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

foo :: _ -> _
foo x = x
-- Inferred: forall t. t -> t

filter' :: _
filter' = filter -- has type forall a. (a -> Bool) -> [a] -> [a]
-- Inferred: (a -> Bool) -> [a] -> [a]

10.24.1.2. Именованные заполнительные символы

NamedWildCards
С

7.10.1

Позволяют именовать заполнительные символы (например, _x) в подписях типов.

Дикие карты типов также могут быть именованы, присваивая постфикс подчёркиванию, т. е. _a. Они называются именованными дикими картами. Все вхождения одной и той же именованной дикой карты в одном типе сигнатуры будут объединены в один и тот же тип. Например:

f :: _x -> _x
f ('c', y) = ('d', error "Urk")
-- Inferred: forall t. (Char, t) -> (Char, t)

Именованная дикая карта заставляет аргументы и типы результатов быть одинаковыми. При отсутствии сигнатуры GHC бы сделал вывод forall a b. (Char, a) -> (Char, b). Именованная дикая карта может упоминаться в ограничениях, при условии, что она также встречается в монотипной части типа сигнатуры, чтобы гарантировать, что она объединится с чем-то:

somethingShowable :: Show _x => _x -> _
somethingShowable x = show x
-- Inferred type: Show a => a -> String

somethingShowable' :: Show _x => _x -> _
somethingShowable' x = show (not x)
-- Inferred type: Bool -> String

Помимо дикой карты дополнительных ограничений (см. Дикую карту дополнительных ограничений), только именованные дикие карты могут встречаться в ограничениях, например, _x в Show _x.

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

В первом примере выше, _x обобщается (и фактически заменяется новой переменной типа a). Во втором примере, _x объединяется с типом Bool, и поскольку Bool реализует класс типа Show, ограничение Show Bool может быть упрощено.

По умолчанию GHC (как предписывает стандарт Haskell 2010) анализирует идентификаторы, начинающиеся с подчёркивания в типе, как переменные типа. Чтобы обработать их как именованные дикие карты, необходимо включить расширение NamedWildCards. Пример ниже демонстрирует эффект.

foo :: _a -> _a
foo _ = False

Компиляция этой программы без включения NamedWildCards даёт следующее сообщение об ошибке, жалуясь на переменную типа _a, которая не соответствует фактическому типу Bool.

Test.hs:5:9: error:
    • Couldn't match expected type ‘_a’ with actual type ‘Bool’
      ‘_a’ is a rigid type variable bound by
        the type signature for:
          foo :: forall _a. _a -> _a
        at Test.hs:4:8
    • In the expression: False
      In an equation for ‘foo’: foo _ = False
    • Relevant bindings include foo :: _a -> _a (bound at Test.hs:5:1)

Компиляция этой программы с включенным NamedWildCards (а также PartialTypeSignatures) выводит следующее сообщение об ошибке, в котором сообщается об выведенном типе именованной дикой карты _a.

Test.hs:4:8: warning: [-Wpartial-type-signatures]
    • Found type wildcard ‘_a’ standing for ‘Bool’
    • In the type signature:
        foo :: _a -> _a
    • Relevant bindings include
        foo :: Bool -> Bool (bound at Test.hs:5:1)

10.24.1.3. Дикая карта дополнительных ограничений

Третий вид дикой карты — дикая карта дополнительных ограничений. Наличие дикой карты дополнительных ограничений указывает на то, что во время проверки типов может быть выведено любое количество дополнительных ограничений, которые будут добавлены в тип сигнатуры. В примере ниже используется дикая карта дополнительных ограничений для вывода трёх дополнительных ограничений.

arbitCs :: _ => a -> String
arbitCs x = show (succ x) ++ show (x == x)
-- Inferred:
--   forall a. (Enum a, Eq a, Show a) => a -> String
-- Error:
Test.hs:5:12: error:
    Found constraint wildcard ‘_’ standing for ‘(Show a, Eq a, Enum a)’
    To use the inferred type, enable PartialTypeSignatures
    In the type signature:
      arbitCs :: _ => a -> String

Дикая карта дополнительных ограничений не должна мешать программисту указывать известные или желаемые ограничения, например:

-- Also a correct partial type signature:
arbitCs' :: (Enum a, _) => a -> String
arbitCs' x = arbitCs x
-- Inferred:
--   forall a. (Enum a, Show a, Eq a) => a -> String
-- Error:
Test.hs:9:22: error:
    Found constraint wildcard ‘_’ standing for ‘()’
    To use the inferred type, enable PartialTypeSignatures
    In the type signature:
      arbitCs' :: (Enum a, _) => a -> String

Дикая карта дополнительных ограничений также может привести к выводу нуля дополнительных ограничений, например:

noCs :: _ => String
noCs = "noCs"
-- Inferred: String
-- Error:
Test.hs:13:9: error:
    Found constraint wildcard ‘_’ standing for ‘()’
    To use the inferred type, enable PartialTypeSignatures
    In the type signature:
      noCs :: _ => String

Так как одна дикая карта дополнительных ограничений достаточно для вывода любого количества ограничений, только одна допускается в сигнатуре типа, и она должна стоять в конце списка ограничений.

Именованные дикие карты дополнительных ограничений не допускаются.

10.24.2. Где они могут встречаться?

Частичные типы сигнатуры допускаются для связываний, паттернов и сигнатур выражений, за исключением того, что дикие карты дополнительных ограничений не поддерживаются в паттернах или сигнатурах выражений. В следующем примере дикая карта используется в каждом из трёх возможных контекстов.

{-# LANGUAGE ScopedTypeVariables #-}
foo :: _
foo (x :: _) = (x :: _)
-- Inferred: forall w_. w_ -> w_

Анонимные и именованные дикие карты могут встречаться в левой части объявления типа или экземпляра данных; см. Дикие карты в левой части данных и экземпляров типов семейств.

Анонимные дикие карты также допускаются во видимых применениях типов/видимых применениях типов (Видимое применение типа). Если вы хотите указать только второй аргумент типа wurble, вы можете сказать wurble @_ @Int, где первый аргумент является дикой картой.

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

deriving instance _ => Eq (Foo a)

Это обозначает производный экземпляр Eq (Foo a), где контекст выводится аналогично обычным правилам deriving.

Любое другое использование диких карт в самостоятельном объявлении deriving запрещено.

Во всех остальных контекстах дикие карты типа не допускаются, и именованные дикие карты обрабатываются как обычные переменные типа. Например:

class C _ where ...          -- Illegal
instance Eq (T _)            -- Illegal (currently; would actually make sense)
instance Eq _a => Eq (T _a)  -- Perfectly fine, same as  Eq a => Eq (T a)

Частичные сигнатуры типов также могут использоваться в Template Haskell вставках.

  • Объявления вставки: частичные сигнатуры типов полностью поддерживаются.

    {-# LANGUAGE TemplateHaskell, NamedWildCards #-}
    $( [d| foo :: _ => _a -> _a -> _
           foo x y = x == y|] )
    
  • Вставки выражений: анонимные и именованные дикие карты могут использоваться в сигнатурах выражений. Дикие карты дополнительных ограничений не поддерживаются, как и в обычных сигнатурах выражений.

    {-# LANGUAGE TemplateHaskell, NamedWildCards #-}
    $( [e| foo = (Just True :: _m _) |] )
    
  • Вставки выражений с типом: поддерживаются те же дикие карты, что и в (нетипизированных) вставках выражений.
  • Вставки паттернов: анонимные и именованные дикие карты могут использоваться в сигнатурах паттернов. Обратите внимание, что ScopedTypeVariables должен быть включён, чтобы разрешить сигнатуры паттернов. Дикие карты дополнительных ограничений не поддерживаются, как и в обычных сигнатурах паттернов.

    {-# LANGUAGE TemplateHaskell, ScopedTypeVariables #-}
    foo $( [p| (x :: _) |] ) = x
    
  • Вставки типов: только анонимные дикие карты поддерживаются в вставках типов. Именованные и дикие карты дополнительных ограничений не поддерживаются.

    {-# LANGUAGE TemplateHaskell #-}
    foo :: $( [t| _ |] ) -> a
    foo x = x
    

10.25. Пользовательские ошибки во время компиляции

При разработке встроенных языков предметной области в Haskell полезно иметь что-то вроде error на уровне типов. Таким образом, разработчик EDSL может показать ошибку типа, специфичную для EDSL, а не стандартную ошибку типа GHC.

Например, рассмотрим класс типа, который не предназначен для использования с функциями, но пользователь случайно использовал его в типе функции, возможно, пропустив аргумент какой-либо функции. Вместо стандартного сообщения GHC о недостающем экземпляре, будет лучше вывести более дружелюбное сообщение, специфичное для EDSL. Аналогично, вычисление функции на уровне типов может заблокироваться из-за ошибки, в этом случае было бы неплохо сообщить об ошибке, специфичной для EDSL, а не об общей ошибке о неоднозначном типе.

Для решения этой проблемы GHC предоставляет единственную функцию на уровне типов,

type family TypeError (msg :: ErrorMessage) :: k

а также небольшой язык на уровне типов (через DataKinds) для построения красивых сообщений об ошибках,

-- ErrorMessage is intended to be used as a kind
data ErrorMessage =
      Text Symbol                        -- Show this text as is
    | forall t. ShowType t               -- Pretty print a type
    | ErrorMessage :<>: ErrorMessage     -- Put two chunks of error message next to each other
    | ErrorMessage :$$: ErrorMessage     -- Put two chunks of error message above each other

в модуле GHC.TypeLits.

Например, мы можем использовать этот интерфейс для предоставления более полезного сообщения об ошибке для приложений show к неполным функциям, как в этом примере:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

import GHC.TypeLits

instance TypeError (Text "Cannot 'Show' functions." :$$:
                    Text "Perhaps there is a missing argument?")
         => Show (a -> b) where
   showsPrec = error "unreachable"

main = print negate

Что выведет следующую ошибку во время компиляции:

Test.hs:12:8: error:
    • Cannot 'Show' functions.
      Perhaps there is a missing argument?
    • In the expression: print negate
      In an equation for ‘main’: main = print negate

10.26. Откладывание ошибок типов до выполнения

При разработке иногда желательно разрешить успешную компиляцию, даже если в коде есть ошибки типов. Рассмотрим следующий случай:

module Main where

a :: Int
a = 'a'

main = print "b"

Несмотря на то, что a имеет ошибку типа, она не используется в конце, поэтому, если всё, что нас интересует, это main, может быть полезно игнорировать проблемы в a.

Для получения дополнительной мотивации и подробностей см. страницу вики Wiki или оригинальную статью оригинальную статью.

10.26.1. Включение откладывания ошибок типов до выполнения

Флаг -fdefer-type-errors управляет тем, откладываются ли ошибки типов до выполнения. Ошибки типов всё ещё будут отображаться как предупреждения, но не будут препятствовать компиляции. Вы можете использовать -Wno-deferred-type-errors для подавления этих предупреждений.

Этот флаг подразумевает флаги -fdefer-typed-holes и -fdefer-out-of-scope-variables, которые включают это поведение для типизированных дыр и переменных. Если вы хотите, вы можете включить -fdefer-type-errors без включения -fdefer-typed-holes или -fdefer-out-of-scope-variables, явно указав -fno-defer-typed-holes или -fno-defer-out-of-scope-variables в командной строке после флага -fdefer-type-errors.

При выполнении, всякий раз, когда терм, содержащий ошибку типа, нужно будет вычислить, ошибка преобразуется в исключение времени выполнения типа TypeError. Обратите внимание, что ошибки типов откладываются по возможности во время выполнения, но недействительные преобразования никогда не выполняются, даже если они в конечном итоге приведут к значению правильного типа. Например, приведённый код:

x :: Int
x = 0

y :: Char
y = x

z :: Int
z = y

вычисление z приведёт к ошибке времени выполнения TypeError.

10.26.2. Отложенные ошибки типов в GHCi

Флаг -fdefer-type-errors работает и в GHCi, с одним исключением: для «голых» выражений, типизированных в командной строке, ошибки типов не откладываются, поэтому, например:

Prelude> fst (True, 1 == 'a')

<interactive>:2:12:
    No instance for (Num Char) arising from the literal `1'
    Possible fix: add an instance declaration for (Num Char)
    In the first argument of `(==)', namely `1'
    In the expression: 1 == 'a'
    In the first argument of `fst', namely `(True, 1 == 'a')'

В противном случае, в обычном случае простой ошибки типа, такой как ввод reverse True в командной строке, вы получите предупреждение, а затем немедленную ошибку типа при вычислении выражения.

Это исключение не относится к операторам, как показывает следующий пример:

Prelude> let x = (True, 1 == 'a')

<interactive>:3:16: Warning:
    No instance for (Num Char) arising from the literal `1'
    Possible fix: add an instance declaration for (Num Char)
    In the first argument of `(==)', namely `1'
    In the expression: 1 == 'a'
    In the expression: (True, 1 == 'a')
Prelude> fst x
True

10.26.3. Ограничения отложенных ошибок типов

Отложенными могут быть следующие ошибки:

  • Переменные терминов, выходящие за область видимости
  • Ограничения равенства; например, ord True приводит к неразрешимому ограничению равенства Char ~ Bool, которое может быть отложено.
  • Ограничения классов типов и неявных параметров

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

В некоторых случаях даже ограничения равенства не могут быть отложены. В частности:

  • Ограничения равенства видов не могут быть отложены, например

    f :: Int Bool -> Char
    

    Эта сигнатура типа содержит ошибку вида, которая не может быть отложена.

  • Равенства типов под forall не могут быть отложены (см. #14605).

10.27. Шаблонный Haskell

Шаблонный Haskell позволяет выполнять метапрограммирование на этапе компиляции в Haskell. Обзор основных технических новшеств приведен в статье «Template Meta-programming for Haskell» (Proc Haskell Workshop 2002).

На странице Template Haskell вики-сайта GHC содержится много информации. Вы также можете обратиться к справочной документации Haddock <Language.Haskell.TH.>. Многие изменения по сравнению с исходным дизайном описаны в Заметках о версии Template Haskell 2. Однако не все эти изменения реализованы в GHC.

Первый пример из этой статьи приведен ниже (Пример использования Template Haskell) в качестве практического примера, чтобы помочь вам начать работу.

В данном документе описывается реализация Template Haskell в GHC. Информации недостаточно для понимания Template Haskell; см. страницу вики .

10.27.1. Синтаксис

TemplateHaskell
Подразумевает

TemplateHaskellQuotes

С

6.0. Типизированные вставки, введенные в GHC 7.8.1.

Включить синтаксис вставки и цитирования Template Haskell.

TemplateHaskellQuotes
С

8.0.1

Включить только синтаксис цитирования Template Haskell.

Шаблонный Haskell имеет следующие новые синтаксические конструкции. Для включения этих синтаксических расширений необходимо использовать расширение TemplateHaskell. В качестве альтернативы можно использовать расширение TemplateHaskellQuotes для включения подмножества цитирования Template Haskell (т. е. без синтаксиса вставки). Расширение TemplateHaskellQuotes считается безопасным в Безопасном Haskell, в то время как TemplateHaskell — нет.

  • Вставка записывается $x, где x — идентификатор, или $(...), где «…» — произвольное выражение. Между «$» и идентификатором или скобками не должно быть пробелов. Это использование «$» переопределяет его значение как инфиксного оператора, так же как «M.x» переопределяет значение «.» как инфиксного оператора. Если вам нужен инфиксный оператор, поставьте пробелы вокруг него.

    Вставка может произойти вместо

    • выражения; вставленное выражение должно иметь тип Q Exp
    • шаблона; вставленный шаблон должен иметь тип Q Pat
    • типа; вставленное выражение должно иметь тип Q Type
    • списка объявлений на верхнем уровне; вставленное выражение должно иметь тип Q [Dec]

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

  • Выражение в кавычках записывается в оксфордские скобки, таким образом:

    • [| ... |], или [e| ... |], где «…» — выражение; кавычки имеют тип Q Exp.
    • [d| ... |], где «…» — список объявлений верхнего уровня; кавычки имеют тип Q [Dec].
    • [t| ... |], где «…» — тип; кавычки имеют тип Q Type.
    • [p| ... |], где «…» — шаблон; кавычки имеют тип Q Pat.

    См. Где они могут возникнуть? для использования частичных сигнатур типов в кавычках.

  • Вставка типизированного выражения записывается как $$x, где x — идентификатор, или $$(...), где «…» — произвольное выражение.

    Вставка типизированного выражения может произойти вместо выражения; вставленное выражение должно иметь тип Q (TExp a)

  • Типизированное выражение в кавычках записывается как [|| ... ||], или [e|| ... ||], где «…» — выражение; если выражение «…» имеет тип a, то кавычки имеют тип Q (TExp a).

    Значения типа TExp a могут быть преобразованы в значения типа Exp с помощью функции unType :: TExp a -> Exp.

  • Квазицитата может появляться в контексте шаблона, типа, выражения или объявления и также записывается в оксфордские скобки:

    • [varid| ... |], где «…» — произвольная строка; полное описание функции квазицитации приведено в Квазицитата Template Haskell.
  • Имя может быть помещено в кавычки с одним или двумя префиксными одинарными кавычками:

    • 'f имеет тип Name, и задаёт имя функции f. Аналогично 'C имеет тип Name и задаёт имя конструктора данных C. В общем случае '⟨thing⟩ интерпретирует ⟨thing⟩ в контексте выражения.

      Имя, вторая буква которого является одинарной кавычкой (к сожалению), не может быть помещено в кавычки таким образом, потому что оно будет интерпретировано как символ в кавычках. Например, если функция называется f'7 (что является допустимым идентификатором Haskell), попытка поместить её в кавычки как 'f'7 будет интерпретирована как литерал символа 'f' за которым следует числовая литерал 7. В настоящее время нет механизма экранирования в этой (необычной) ситуации.

    • ''T имеет тип Name, и задаёт имя конструктора типов T. То есть, ''⟨thing⟩ интерпретирует ⟨thing⟩ в контексте типа.

    Эти Names могут быть использованы для построения выражений Template Haskell, шаблонов, объявлений и т. д. Их также можно передать в качестве аргумента функции reify.

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

    module Bar where
    
    import Language.Haskell.TH
    
    add1 :: Int -> Q Exp
    add1 x = [| x + 1 |]
    

    Теперь рассмотрим вставку, использующую add1 в отдельном модуле:

    module Foo where
    
    import Bar
    
    two :: Int
    two = $(add1 1)
    

    Template Haskell не может знать, каким будет аргумент функции add1 в месте определения функции, поэтому используется механизм подъёма для повышения x до значения типа Q Exp. Эта функциональность предоставляется пользователю в виде класса типов Lift в модуле Language.Haskell.TH.Syntax.

    Если тип имеет экземпляр Lift , то любое его значение может быть поднято до выражения Template Haskell:

    class Lift t where
        lift :: t -> Q Exp
    

    В общем случае, если GHC видит выражение внутри оксфордских скобок (например, [| foo bar |], то GHC ищет каждое имя внутри скобок.

    Если имя является глобальным (например, предположим, что foo поступает из импорта или объявления верхнего уровня), то полное квалифицированное имя используется непосредственно в кавычках.

    Если имя является локальным (например, предположим, что bar связано локально в определении функции mkFoo bar = [| foo bar |]), то GHC использует lift на нём (так GHC делает вид, что [| foo bar |] фактически содержит [| foo $(lift bar) |]). Локальные имена, которые не находятся в области видимости в местах вставки, фактически вычисляются во время обработки кавычек.

    Библиотека template-haskell предоставляет экземпляры Lift для многих распространённых типов данных.

    Кроме того, можно автоматически выводить экземпляры Lift с помощью языкового расширения DeriveLift. Дополнительную информацию см. в разделе Вывод экземпляров Lift.

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

    module Foo where
    import Bar
    
    f x = x
    
    $(deriveStuff 'f)   -- Uses the $(...) notation
    
    g y = y+1
    
    deriveStuff 'g      -- Omits the $(...)
    
    h z = z-1
    

    Это сокращение делает вставки объявлений верхнего уровня более лаконичными и менее устрашающими.

  • Вставки шаблонов вводят связующие переменные, но область видимости переменных в выражениях внутри области видимости шаблона проверяется только при выполнении вставки. Обратите внимание, что вставки шаблонов, которые появляются вне скобок кавычек, выполняются во время компиляции.

    Вставки шаблонов, которые появляются внутри скобок кавычек, не выполняются во время компиляции; они выполняются при вставке скобки, в некоторое время позже. Например,

    mkPat :: Q Pat
    mkPat = [p| (x, y) |]
    
    -- in another module:
    foo :: (Char, String) -> String
    foo $(mkPat) = x : z
    
    bar :: Q Exp
    bar = [| \ $(mkPat) -> x : w |]
    

    ошибётся из-за z , не находящегося в области видимости при определении foo , но это не ошибётся из-за w , не находящегося в области видимости при определении bar . Это произойдёт только когда bar будет вставлено.

  • Квазицитата шаблона может генерировать связующие переменные, которые охватывают правую часть определения, поскольку эти связующие переменные находятся в области видимости лексически. Например, задана квазицитата haskell , которая анализирует Haskell, в следующем коде y в правой части f относится к y , связанному шаблоном квазицитаты haskell , а не к глобальному y = 7.

    y :: Int
    y = 7
    
    f :: Int -> Int -> Int
    f n = \ [haskell|y|] -> y+n
    
  • Объявления верхнего уровня разделяют исходный файл на группы объявлений. Группа объявлений — это группа объявлений, созданная вставкой объявления верхнего уровня, плюс следующие за ней объявления, вплоть до, но не включая, следующую вставку объявления верхнего уровня. Примечание: только вставки верхнего уровня разделяют группы объявлений, а не вставки выражений. Первая группа объявлений в модуле включает все определения верхнего уровня до, но не включая, первую вставку объявления верхнего уровня.

    Каждая группа объявлений рекурсивно связана только в пределах группы. Группы объявлений могут ссылаться на определения в предыдущих группах, но не в последующих.

    Соответственно, среда типов, видимая для reify , включает все объявления верхнего уровня до конца непосредственно предшествующей группы объявлений, но не более того.

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

    Рассмотрим следующий код

    module M where
    
    import ...
    
    f x = x
    
    $(th1 4)
    
    h y = k y y $(blah1)
    
    [qq|blah|]
    
    k x y z = x + y + z
    
    $(th2 10)
    
    w z = $(blah2)
    

    В этом примере, reify внутри…

    1. Вставка $(th1 ...) увидит определение f — вставка находится на верхнем уровне, и поэтому все определения в предыдущей группе объявлений являются видимыми (то есть все определения в модуле до, но не включая, саму вставку).
    2. Вставка $(blah1) не может ссылаться на функцию w — w является частью последующей группы объявлений и, следовательно, невидимой, аналогично, $(blah1) не может видеть определение h (поскольку оно является частью той же группы объявлений, что и $(blah1). Однако, вставка $(blah1) может видеть определение f (поскольку оно находится в непосредственно предшествующей группе объявлений).
    3. Вставка $(th2 ...) увидит определение f, все связывания, созданные $(th1 ...), определение h и все связывания, созданные [qq|blah|] (все они находятся в предыдущих группах объявлений).
    4. Тело h может ссылаться на функцию k, появляющуюся по другую сторону вставки квазицитаций объявлений, поскольку квазицитаторы не вызывают разбиения группы объявлений.
    5. qq квазицитатор сможет увидеть определение f из предшествующей группы объявлений, но не определения h или k, или любые определения из последующих групп объявлений.
    6. Вставка $(blah2) увидит те же определения, что и вставка $(th2 ...) (но не любые связывания, которые она создаёт).

    Обратите внимание, что так как вставка выражения не может ссылаться на объявления в той же группе объявлений, мы можем ввести вставку верхнего уровня (пустую), чтобы разбить группу объявлений

    module M where
    
    data D = C1 | C2
    
    f1 = $(th1 ...)
    
    $(return [])
    
    f2 = $(th2 ...)
    

    Здесь

    1. Вставка $(th1 ...) не может ссылаться на D — она находится в той же группе объявлений.
    2. Группа объявлений, содержащая D , завершается пустой вставкой объявления верхнего уровня $(return []) (вспомните, Q — это монада, поэтому мы можем просто return пустой список объявлений).
    3. Поскольку группа объявлений, содержащая D , находится в предыдущей группе объявлений, вставка $(th2 ...) может ссылаться на D.
  • Цитаты выражений принимают большинство конструкций языка Haskell. Однако существуют некоторые расширения, специфичные для GHC, которые вставки выражений в настоящее время не поддерживают, включая

    • Рекурсивные do-выражения (см. #1262)
    • Свободные места типов в типизированных вставках (см. #10945 и #10946)

(По сравнению с оригинальной статьей, существуют многочисленные различия в деталях. Синтаксис для вставки объявления использует «$», а не «splice». Тип заключённого выражения должен быть Q [Dec], а не [Q Dec]. Типизированные вставки и цитаты выражений поддерживаются.)

-fenable-th-splice-warnings

Вставки Template Haskell не будут проверяться на наличие предупреждений, потому что код, вызывающий предупреждение, может исходить из сторонней библиотеки и, возможно, не был написан пользователем. Если вы всё-таки хотите получать предупреждения для вставок, передайте -fenable-th-splice-warnings.

10.27.2. Использование Template Haskell

  • Типы данных и моноидные конструкторы функций Template Haskell находятся в библиотеке Language.Haskell.TH.Syntax.
  • Вы можете запустить функцию во время компиляции только если она импортирована из другого модуля. То есть, вы не можете определить функцию в модуле и вызвать её из вставки в том же модуле. (Это было бы логично, но сложно реализовать.)
  • Вы можете запустить функцию во время компиляции только если она импортирована из другого модуля, который не является частью взаимно рекурсивной группы модулей, включающей модуль, который сейчас компилируется. Кроме того, все модули взаимно рекурсивной группы должны быть доступны по не-SOURCE импортам из модуля, в котором должна выполняться вставка.

    Например, при компиляции модуля A вы можете запустить функции Template Haskell, импортированные из B, только если B не импортирует A (прямо или косвенно). Причина должна быть понятна: чтобы запустить B, нам необходимо скомпилировать и запустить A, но мы сейчас проверяем тип A.

  • Если вы собираете GHC из исходного кода, вам нужен, по крайней мере, компилятор bootstrap 2-го этапа, чтобы запустить вставки Template Haskell и квазицитаты. Компилятор 1-го этапа будет принимать только обычные цитаты Haskell. Причина: вставки TH и квазицитаты компилируют и запускают программу, а затем проверяют результат. Поэтому важно, чтобы компилируемая программа давала результаты, представления которых идентичны представлениям самого компилятора.

Template Haskell работает в любом режиме (--make, --interactive или пофайлово). Раньше существовало ограничение для первых двух, но это ограничение было снято.

10.27.3. Просмотр кода, сгенерированного Template Haskell

Флаг -ddump-splices показывает расширение всех вставок объявлений верхнего уровня, как типизированных, так и нетипизированных, по мере их выполнения. Как и со всеми флагами вывода, по умолчанию этот вывод отправляется в стандартный вывод. Для нетривиальной программы вас может заинтересовать объединение этого с флагом -ddump-to-file (см. Вывод промежуточных структур компилятора. Для каждого файла, использующего Template Haskell, этот вывод будет показан в файле .dump-splices.

Флаг -dth-dec-file выводит расширения всех вставок объявлений верхнего уровня TH, как типизированных, так и нетипизированных, в файл M.th.hs для каждого компилируемого модуля M. Обратите внимание, что другие типы вставок (выражения, типы и шаблоны) не отображаются. Разработчики приложений могут включить этот вывод в свой репозиторий, чтобы можно было искать идентификаторы, которые были определены в Template Haskell. Это похоже на использование -ddump-to-file с -ddump-splices, но оно всегда генерирует файл, а не связано с -ddump-to-file. Формат также отличается: он не отображает код из исходного файла, а только сгенерированный код и имеет комментарий для расположения вставки в исходном файле.

Ниже приведён пример вывода -ddump-splices

TH_pragma.hs:(6,4)-(8,26): Splicing declarations
  [d| foo :: Int -> Int
      foo x = x + 1 |]
======>
  foo :: Int -> Int
  foo x = (x + 1)

Ниже приведён вывод того же примера с использованием -dth-dec-file

-- TH_pragma.hs:(6,4)-(8,26): Splicing declarations
foo :: Int -> Int
foo x = (x + 1)

10.27.4. Пример использования Template Haskell

Чтобы помочь вам преодолеть барьер, попробуйте этот скелетный рабочий пример. Сначала скопируйте и вставьте два модуля ниже в Main.hs и Printf.hs:

{- Main.hs -}
module Main where

-- Import our template "pr"
import Printf ( pr )

-- The splice operator $ takes the Haskell source code
-- generated at compile time by "pr" and splices it into
-- the argument of "putStrLn".
main = putStrLn ( $(pr "Hello") )


{- Printf.hs -}
module Printf where

-- Skeletal printf from the paper.
-- It needs to be in a separate module to the one where
-- you intend to use it.

-- Import some Template Haskell syntax
import Language.Haskell.TH

-- Describe a format string
data Format = D | S | L String

-- Parse a format string.  This is left largely to you
-- as we are here interested in building our first ever
-- Template Haskell program and not in building printf.
parse :: String -> [Format]
parse s   = [ L s ]

-- Generate Haskell source code from a parsed representation
-- of the format string.  This code will be spliced into
-- the module which calls "pr", at compile time.
gen :: [Format] -> Q Exp
gen [D]   = [| \n -> show n |]
gen [S]   = [| \s -> s |]
gen [L s] = stringE s

-- Here we generate the Haskell code for the splice
-- from an input format string.
pr :: String -> Q Exp
pr s = gen (parse s)

Теперь запустите компилятор,

$ ghc --make -XTemplateHaskell main.hs -o main

Запустите main и вот ваш вывод:

$ ./main
Hello

10.27.5. Использование Template Haskell с профилированием

Template Haskell полагается на встроенный байткодовый компилятор и интерпретатор GHC для выполнения выражений вставок. Интерпретатор байткода выполняет скомпилированное выражение в той же среде выполнения, в которой работает сам GHC; это означает, что скомпилированный код, на который ссылается интерпретируемое выражение, должен быть совместим с этой средой выполнения, и, в частности, это означает, что объектный код, который компилируется для профилирования, не может загружаться и использоваться выражением вставки, потому что профилированный объектный код совместим только с профилируемой версией среды выполнения.

Это вызывает трудности, если у вас есть многомодульная программа, содержащая код Template Haskell, и вам нужно скомпилировать её для профилирования, потому что GHC не может загрузить профилированный объектный код и использовать его при выполнении вставок.

К счастью, GHC предоставляет два обходных пути.

Первый вариант — дважды скомпилировать программу:

  1. Сначала скомпилируйте программу или библиотеку обычным способом, без -prof.
  2. Затем скомпилируйте её снова с -prof, и дополнительно используйте -osuf p_o для присвоения объектным файлам разных имён (вы можете выбрать любой суффикс, который не является стандартным суффиксом объектных файлов). GHC автоматически загрузит объектные файлы, созданные на первом шаге, при выполнении выражений splice. Если вы опустите флаг -osuf ⟨suffix⟩ при компиляции с -prof и используется Template Haskell, GHC выведет сообщение об ошибке.

Второй вариант — добавить флаг -fexternal-interpreter (см. Запуск интерпретатора в отдельном процессе), который запустит интерпретатор в отдельном процессе, где он сможет загрузить и выполнить профилируемый код напрямую. Нет необходимости компилировать код дважды, просто добавьте -fexternal-interpreter и всё должно работать. (Этот вариант экспериментальный в GHC 8.0.x, но он может стать стандартным в будущих релизах).

10.27.6. Квазицитирование Template Haskell

QuasiQuotes
С тех пор как

6.10.1

Включить синтаксис квазицитирования Template Haskell.

Квазицитирование позволяет записывать шаблоны и выражения с использованием определённого программистом конкретного синтаксиса; мотивация расширения и несколько примеров документированы в статье «Why It’s Nice to be Quoted: Quasiquoting for Haskell» (Proc Haskell Workshop 2007). Пример ниже демонстрирует, как написать квазицитирующее значение для простого языка выражений.

Вот основные особенности

  • Квазицитата имеет вид [quoter| string |].

    • ⟨quoter⟩ должно быть именем импортированного квазицитирующего значения, квалифицированным или без квалификатора; это не может быть произвольным выражением.
    • ⟨quoter⟩ не может быть «e», «t», «d» или «p», так как они перекрываются с цитируемыми значениями Template Haskell.
    • В токенах [quoter| не должно быть пробелов.
    • Цитируемая ⟨строка⟩ может быть произвольной и может содержать новые строки.
    • Цитируемая ⟨строка⟩ заканчивается на первой встреченной последовательностью из двух символов "|]". Абсолютно никакой экранизации не выполняется. Если вы хотите встроить эту последовательность символов в строку, вам нужно придумать свою собственную систему экранирования (например, использовать строку "|~]" вместо), и заставить функцию вашего квазицитирующего значения интерпретировать "|~]" как "|]". Один из способов реализации этого — составить своё квазицитирующее значение с предварительной обработкой для выполнения преобразования экранирования. Подробности см. в обсуждении в #5348.
  • Квазицитата может появляться вместо

    • Выражения
    • Шаблона
    • Типа
    • Декларации на верхнем уровне

    (Только первые два описаны в статье.)

  • Квазицитирующее значение — это значение типа Language.Haskell.TH.Quote.QuasiQuoter, которое определяется следующим образом:

    data QuasiQuoter = QuasiQuoter { quoteExp  :: String -> Q Exp,
                                     quotePat  :: String -> Q Pat,
                                     quoteType :: String -> Q Type,
                                     quoteDec  :: String -> Q [Dec] }
    

    То есть, квазицитирующее значение — это кортеж из четырёх парсеров, по одному для каждого контекста, в котором может появиться квазицитата.

  • Квазицитата расширяется путём применения соответствующего парсера к строке, заключённой в квадратные скобки. Контекст квазицитаты (выражение, шаблон, тип, декларация) определяет, какой из парсеров вызывается.
  • В отличие от обычных декларативных splice выражений вида $(...), квазицитаты деклараций не вызывают разрыв группы деклараций. Подробнее см. в разделе Синтаксис.

Предупреждение

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

let x = [v| v <- [0..10]]

Без QuasiQuotes это разбирается как списковое включение. С QuasiQuotes это разбирается как квазицитата; однако это разбор завершится неудачей из-за отсутствия закрывающей |]. См. #11679.

Пример ниже демонстрирует квазицитирование в действии. Квазицитирующее значение expr привязано к значению типа QuasiQuoter, определённому в модуле Expr. Пример использует антицитированную переменную n, указанную синтаксисом 'int:n (этот синтаксис для антицитирования был определён автором парсера, а не GHC). Это связывает n со значением целочисленного аргумента конструктора IntExpr при сопоставлении с образцом. Более подробные сведения об антицитировании, а также описание техники, использующей SYB для использования одного парсера типа String -> a для генерации парсера выражений, возвращающего значение типа Q Exp, и парсера шаблонов, возвращающего значение типа Q Pat, см. в указанной статье.

Квазицитирующие значения должны подчиняться тем же ограничениям этапов, что и Template Haskell, например, в примере expr не может быть определено в Main.hs там, где оно используется, но должно быть импортировано.

{- ------------- file Main.hs --------------- -}
module Main where

import Expr

main :: IO ()
main = do { print $ eval [expr|1 + 2|]
          ; case IntExpr 1 of
              { [expr|'int:n|] -> print n
              ;  _              -> return ()
              }
          }


{- ------------- file Expr.hs --------------- -}
module Expr where

import qualified Language.Haskell.TH as TH
import Language.Haskell.TH.Quote

data Expr  =  IntExpr Integer
           |  AntiIntExpr String
           |  BinopExpr BinOp Expr Expr
           |  AntiExpr String
    deriving(Show, Typeable, Data)

data BinOp  =  AddOp
            |  SubOp
            |  MulOp
            |  DivOp
    deriving(Show, Typeable, Data)

eval :: Expr -> Integer
eval (IntExpr n)        = n
eval (BinopExpr op x y) = (opToFun op) (eval x) (eval y)
  where
    opToFun AddOp = (+)
    opToFun SubOp = (-)
    opToFun MulOp = (*)
    opToFun DivOp = div

expr = QuasiQuoter { quoteExp = parseExprExp, quotePat =  parseExprPat }

-- Parse an Expr, returning its representation as
-- either a Q Exp or a Q Pat. See the referenced paper
-- for how to use SYB to do this by writing a single
-- parser of type String -> Expr instead of two
-- separate parsers.

parseExprExp :: String -> Q Exp
parseExprExp ...

parseExprPat :: String -> Q Pat
parseExprPat ...

Теперь запустите компилятор:

$ ghc --make -XQuasiQuotes Main.hs -o main

Запустите «main», и вот ваш вывод:

$ ./main
3
1

10.28. Стрелочная нотация

Arrows
С тех пор как

6.8.1

Включить стрелочную нотацию.

Стрелки обобщают монады, введённые Джоном Хьюзом. Для получения более подробной информации, см.

  • «Обобщение монад на стрелки», Джон Хьюз, в Science of Computer Programming 37, стр. 67–111, май 2000. Статья, которая ввела стрелки: дружественное введение, мотивированное примерами программирования.
  • «Новая нотация для стрелок», Росс Патерсон, в ICFP, сентябрь 2001. Представлена нотация, описанная здесь.
  • «Стрелки и вычисления», Росс Патерсон, в The Fun of Programming, Palgrave, 2003.
  • «Программирование со стрелками», Джон Хьюз, в 5-й Международной летней школе по продвинутому функциональному программированию, Lecture Notes in Computer Science, том 3622, Springer, 2004. Эта статья содержит ещё одно введение в нотацию с практическим применением.
  • «Типовые и правила трансляции для стрелочной нотации в GHC», Росс Патерсон и Саймон Пейтон Джонс, 16 сентября 2004. Краткое изложение формальных правил, используемых (извлеченных из комментариев в исходном коде).
  • Страница стрелок в http://www.haskell.org/arrows/ <http://www.haskell.org/arrows/>`__.

С расширением Arrows, GHC поддерживает стрелочную нотацию, описанную во второй из этих статей, переводя её с помощью комбинаторов из модуля Control.Arrow. Далее приводится краткое введение в нотацию; оно не будет иметь смысла, если вы не читали статью Хьюза.

Расширение добавляет новый тип выражения для определения стрелок:

exp10 ::= ...
       |  proc apat -> cmd

где proc — новое ключевое слово. Переменные шаблона привязаны в теле proc-выражения, которое представляет собой новый тип, называемый командой. Синтаксис команд следующий:

cmd   ::= exp10 -<  exp
       |  exp10 -<< exp
       |  cmd0

с ⟨cmd⟩0 до ⟨cmd⟩9, определёнными с использованием инфиксных операторов, как и для выражений, и

cmd10 ::= \ apat ... apat -> cmd
       |  let decls in cmd
       |  if exp then cmd else cmd
       |  case exp of { calts }
       |  do { cstmt ; ... cstmt ; cmd }
       |  fcmd

fcmd  ::= fcmd aexp
       |  ( cmd )
       |  (| aexp cmd ... cmd |)

cstmt ::= let decls
       |  pat <- cmd
       |  rec { cstmt ; ... cstmt [;] }
       |  cmd

где ⟨calts⟩ похожи на ⟨alts⟩, за исключением того, что тела являются командами, а не выражениями.

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

Простой пример новой нотации — выражение

proc x -> f -< x+1

Мы называем это процедурой или абстракцией стрелки. Как и с лямбда-выражением, переменная x — новая переменная, связанная в proc-выражении. Она относится к входным данным стрелки. В приведённом выше примере -< — не идентификатор, а новое зарезервированное слово, используемое для построения команд из выражения стрелочного типа и выражения, которое должно быть подано на вход этой стрелки. (Необычный вид станет понятнее позже.) Его можно прочитать как аналог применения для стрелок. Приведённый выше пример эквивалентен выражению Haskell

arr (\ x -> x+1) >>> f

Это не имело бы смысла, если выражение слева от -< включает связанную переменную x. В более общем случае, выражение слева от -< может не включать никакие локальные переменные, т. е. переменные, связанные в текущей абстракции стрелки. Для таких ситуаций существует вариант -<<, как в

proc x -> f x -<< x+1

что эквивалентно

arr (\ x -> (f x, x+1)) >>> app

Таким образом, в этом случае стрелка должна принадлежать классу ArrowApply. Такая стрелка эквивалентна монаде, поэтому, если вы используете эту форму, вы можете найти монадное представление более удобным.

10.28.1. do-нотация для команд

Другой формой команды является форма do-нотации. Например, вы можете написать

proc x -> do
        y <- f -< x+1
        g -< 2*y
        let z = x+y
        t <- h -< x*z
        returnA -< t+z

Вы можете читать это так же, как обычные do-нотации, но с командами вместо монодических выражений. Первая строка отправляет значение x+1 в качестве входных данных стрелке f, и сопоставляет её выходные данные с y. В следующей строке выходные данные отбрасываются. Стрелка returnA определена в модуле Control.Arrow как arr id. Приведённый выше пример рассматривается как сокращение для

arr (\ x -> (x, x)) >>>
        first (arr (\ x -> x+1) >>> f) >>>
        arr (\ (y, x) -> (y, (x, y))) >>>
        first (arr (\ y -> 2*y) >>> g) >>>
        arr snd >>>
        arr (\ (x, y) -> let z = x+y in ((x, z), z)) >>>
        first (arr (\ (x, z) -> x*z) >>> h) >>>
        arr (\ (t, z) -> t+z) >>>
        returnA

Обратите внимание, что переменные, не используемые позже в композиции, исключаются. После упрощения с помощью правил переписывания (см. Правила переписывания), определённых в модуле Control.Arrow, это сводится к

arr (\ x -> (x+1, x)) >>>
        first f >>>
        arr (\ (y, x) -> (2*y, (x, y))) >>>
        first g >>>
        arr (\ (_, (x, y)) -> let z = x+y in (x*z, z)) >>>
        first h >>>
        arr (\ (t, z) -> t+z)

что вы, возможно, написали бы вручную. С помощью нотации со стрелками GHC отслеживает все эти кортежи переменных за вас.

Обратите внимание, что хотя вышеупомянутый перевод предполагает, что let-связанные переменные, такие как z должны быть мономорфными, фактический перевод создаёт Core, поэтому полиморфные переменные разрешены.

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

counter :: ArrowCircuit a => a Bool Int
counter = proc reset -> do
        rec     output <- returnA -< if reset then 0 else next
                next <- delay 0 -< output+1
        returnA -< output

Перевод таких форм использует комбинатор loop, поэтому соответствующая стрелка должна принадлежать классу ArrowLoop.

10.28.2. Условные команды

В предыдущем примере мы использовали условное выражение для построения входных данных для стрелки. Иногда нам нужно условно выполнить разные команды, как в

proc (x,y) ->
        if f x y
        then g -< x+1
        else h -< y+2

что переводится в

arr (\ (x,y) -> if f x y then Left x else Right y) >>>
        (arr (\x -> x+1) >>> g) ||| (arr (\y -> y+2) >>> h)

Поскольку перевод использует |||, соответствующая стрелка должна принадлежать классу ArrowChoice.

Также существуют case команды, такие как

case input of
    [] -> f -< ()
    [x] -> g -< x+1
    x1:x2:xs -> do
        y <- h -< (x1, x2)
        ys <- k -< xs
        returnA -< y:ys

Синтаксис такой же, как и для case выражений, за исключением того, что тела альтернатив являются командами, а не выражениями. Перевод аналогичен переводу if команд.

10.28.3. Определение собственных управляющих структур

Как мы видели, нотация со стрелками предоставляет конструкции, основанные на тех, которые предназначены для выражений, для последовательности, рекурсии значений и условных выражений. Но подходящие комбинаторы, которые вы можете определить в обычном Haskell, также могут использоваться для построения новых команд из существующих. Основная идея в том, что команда определяет стрелку от окружений к значениям. Эти окружения присваивают значения свободным локальным переменным команды. Таким образом, комбинаторы, которые производят стрелки из стрелок, также могут использоваться для построения команд из команд. Например, класс ArrowPlus включает комбинатор

ArrowPlus a => (<+>) :: a b c -> a b c -> a b c

так что мы можем использовать его для построения команд:

expr' = proc x -> do
                returnA -< x
        <+> do
                symbol Plus -< ()
                y <- term -< ()
                expr' -< x + y
        <+> do
                symbol Minus -< ()
                y <- term -< ()
                expr' -< x - y

(Символ do в первой строке необходим для предотвращения интерпретации первого <+> ... как части выражения на предыдущей строке.) Это эквивалентно

expr' = (proc x -> returnA -< x)
        <+> (proc x -> do
                symbol Plus -< ()
                y <- term -< ()
                expr' -< x + y)
        <+> (proc x -> do
                symbol Minus -< ()
                y <- term -< ()
                expr' -< x - y)

На самом деле мы здесь используем <+> с более конкретным типом

ArrowPlus a => (<+>) :: a (e,()) c -> a (e,()) c -> a (e,()) c

Важно, чтобы этот оператор был полиморфным по e (представляющему входное окружение команды, а затем её подкоманд) и удовлетворял соответствующему свойству естественности

arr (first k) >>> (f <+> g) = (arr (first k) >>> f) <+> (arr (first k) >>> g)

по крайней мере для строгих k. (Это должно быть автоматическим, если вы не используете seq.) Это гарантирует, что окружения, увиденные подкомандами, являются окружениями всей команды, а также позволяет переводу безопасно обрезать эти окружения. (Вторая компонента пар входных данных может содержать неопределённые входные значения, как описано в следующей секции.) Оператор также не должен использовать какие-либо переменные, определённые внутри текущей абстракции стрелки.

Мы могли бы определить собственный оператор

untilA :: ArrowChoice a => a (e,s) () -> a (e,s) Bool -> a (e,s) ()
untilA body cond = proc x -> do
        b <- cond -< x
        if b then returnA -< ()
        else do
                body -< x
                untilA body cond -< x

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

proc x -> do
        y <- f -< x+1
        (|untilA (increment -< x+y) (within 0.5 -< x)|)

10.28.4. Примитивные конструкции

Некоторым операторам необходимо передавать дополнительные входные данные своим подкомандам. Например, в типе стрелки, поддерживающем исключения, оператор, который прикрепляет обработчик исключений, будет желать передать исключение, которое произошло, обработчику. Такой оператор может иметь тип

handleA :: ... => a (e,s) c -> a (e,(Ex,s)) c -> a (e,s) c

где Ex — это тип обрабатываемых исключений. Затем вы можете использовать это с нотацией со стрелками, написав команду

body `handleA` \ ex -> handler

так что если в команде body возникает исключение, переменная ex связана со значением исключения, и выполняется команда handler, которая, как правило, относится к ex. Хотя синтаксис здесь похож на функциональный лямбда-выражение, мы говорим о командах, и происходит нечто другое. Входные данные стрелки, представленной командой, состоят из значений свободных локальных переменных в команде, плюс стек анонимных значений. Во всех предыдущих примерах мы не делали предположений об этом стеке. Во втором аргументе handleA, значение исключения было добавлено к входному стеку обработчика. Командная форма лямбда-выражения просто даёт этому значению имя.

Более конкретно, входными данными для команды является пара окружения и стека. Каждое значение в стеке спарено с остальной частью стека, при этом пустой стек является Strict. Таким образом, операторы, такие как handleA , которые передают дополнительные входные данные своим подкомандам, могут быть спроектированы для использования с обозначением путём помещения значений в стек, спаренный с окружением таким образом. Более точно, тип каждого аргумента оператора (и его результат) должен иметь вид

a (e, (t1, ... (tn, ())...)) t

где ⟨e⟩ — полиморфная переменная (представляющая окружение), а ⟨ti⟩ — типы значений в стеке, причём ⟨t1⟩ — «верхний». Полиморфная переменная ⟨e⟩ не должна встречаться в ⟨a⟩, ⟨ti⟩ или ⟨t⟩. Однако применимые стрелки не обязательно должны быть одинаковыми. Вот ещё несколько примеров подходящих операторов:

bracketA :: ... => a (e,s) b -> a (e,(b,s)) c -> a (e,(c,s)) d -> a (e,s) d
runReader :: ... => a (e,s) c -> a' (e,(State,s)) c
runState :: ... => a (e,s) c -> a' (e,(State,s)) (c,State)

Мы можем предоставить дополнительные входные данные, необходимые командам, построенным с двумя последними, применив их к обычным выражениям, как в

proc x -> do
        s <- ...
        (|runReader (do { ... })|) s

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

Командные версии лямбда-абстракции и применения аналогичны версиям для выражений. В частности, бета- и эта-правила описывают эквивалентности команд. Эти три особенности (операторы, лямбда-абстракция и применение) являются ядром обозначения; всё остальное можно построить с их помощью, хотя результаты будут несколько громоздкими. Например, мы могли бы смоделировать do-нотацию, определив

bind :: Arrow a => a (e,s) b -> a (e,(b,s)) c -> a (e,s) c
u `bind` f = returnA &&& u >>> f

bind_ :: Arrow a => a (e,s) b -> a (e,s) c -> a (e,s) c
u `bind_` f = u `bind` (arr fst >>> f)

Мы могли бы смоделировать if , определив

cond :: ArrowChoice a => a (e,s) b -> a (e,s) b -> a (e,(Bool,s)) b
cond f g = arr (\ (e,(b,s)) -> if b then Left (e,s) else Right (e,s)) >>> f ||| g

10.28.5. Отличия от статьи

  • Вместо единственной формы применения стрелки (хвост стрелки) с двумя переводами, реализация предоставляет две формы -< (первого порядка) и -<< (высшего порядка).
  • Определяемые пользователем операторы помечены скобками «бананов», а не новым ключевым словом form.
  • В статье и в предыдущей реализации значения в стеке были спарены справа от окружения в одном аргументе, но теперь окружение и стек являются отдельными аргументами.

10.28.6. Переносимость

Хотя только GHC реализует нотацию со стрелками напрямую, также существует препроцессор (доступный на веб-странице стрелок), который преобразует нотацию со стрелками в Haskell 98 для использования с другими системами Haskell. Вам по-прежнему хотелось бы проверить программы со стрелками с помощью GHC; отслеживание ошибок типов в выходных данных препроцессора нелегко. Модули, предназначенные как для GHC, так и для препроцессора, должны соблюдать некоторые дополнительные ограничения:

  • Модуль должен импортировать Control.Arrow.
  • Препроцессор не может обрабатывать другие расширения Haskell. Эти расширения должны находиться в отдельных модулях.
  • Поскольку препроцессор нацелен на Haskell (а не Core), let-связанные переменные являются мономорфными.

10.29. Паттерны bang и строгий Haskell

В высокопроизводительных кодах Haskell (например, числовых кодах) устранение туннелей из внутреннего цикла может существенно повысить производительность. GHC поддерживает три расширения, позволяющие программисту указать использование строгой (вызов по значению) оценки вместо ленивой (вызов по необходимости) оценки.

  • Паттерны bang (BangPatterns) делают сопоставление с образцом и связи let более строгими.
  • Строгие типы данных (StrictData) делают поля конструктора строгими по умолчанию на уровне модуля.
  • Строгое соответствие (Strict) делает все шаблоны и связи let строгими по умолчанию на уровне модуля.

Последние два расширения просто способ избежать засорения высокопроизводительного кода паттернами bang, делая его сложнее для чтения.

Паттерны bang и строгое соответствие не влияют на систему типов.

10.29.1. Паттерны bang

BangPatterns
С

6.8.1

Разрешить использование синтаксиса паттернов bang.

GHC поддерживает расширение сопоставления с образцом, называемое паттернами bang, записанное !pat. Паттерны bang рассматриваются для Haskell Prime. Описание функции Haskell prime содержит больше обсуждения и примеров, чем материал ниже.

Основная идея заключается в добавлении одной новой продукции к синтаксису шаблонов:

pat ::= !pat

Сопоставление выражения e с шаблоном !p выполняется путём предварительной оценки e (до WHNF) и последующего сопоставления результата с p. Пример:

f1 !x = True

Это определение делает f1 жёстким в x, в то время как без восклицательного знака оно было бы ленивым. Шаблоны с восклицательным знаком, конечно, могут быть вложенными:

f2 (!x, y) = [x,y]

Здесь, f2 жёсткий в x, но не в y.

Обратите внимание на следующие моменты:

  • Восклицательный знак оказывает реальное влияние только если он предшествует переменной или шаблону-подстановке:

    f3 !(x,y) = [x,y]
    f4 (x,y)  = [x,y]
    

    Здесь, f3 и f4 идентичны; добавление восклицательного знака перед шаблоном, который в любом случае заставляет выполнять оценку, ничего не меняет.

  • Шаблон с восклицательным знаком разрешен в предложении let или where и делает привязку жёсткой. Например:

    let !x = e in body
    let !(p,q) = e in body
    

    В обоих случаях e оценивается до начала оценки body.

    Однако вложенные восклицательные знаки в привязке let/where ведут себя единообразно со всеми другими формами сопоставления с образцом. Например

    let (!x,[y]) = e in b
    

    эквивалентно этому:

    let { t = case e of (x,[y]) -> x `seq` (x,y)
          x = fst t
          y = snd t }
    in b
    

    Привязка ленивая, но когда x или y оценивается b, весь шаблон сопоставляется, включая принудительную оценку x.

    См. Семантика привязок let с шаблонами с восклицательными знаками для подробной семантики.

  • Шаблон с восклицательным знаком на самом внешнем уровне не разрешен на верхнем уровне модуля.
  • Шаблоны с восклицательными знаками работают и в выражениях case , конечно:

    g5 x = let y = f x in body
    g6 x = case f x of { y -> body }
    g7 x = case f x of { !y -> body }
    

    Функции g5 и g6 означают ровно то же самое. Но g7 оценивает (f x), связывает y с результатом, а затем оценивает body.

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

    f !x = 3
    

    Это определение инфиксной функции «(!)», или «f» с шаблоном с восклицательным знаком? GHC разрешает эту неоднозначность в пользу последнего. Если вы хотите определить (!) с включёнными шаблонами с восклицательными знаками, вы должны сделать это с использованием префиксной записи:

    (!) f x = 3
    

10.29.2. Типы данных по умолчанию жёсткие

StrictData
С

8.0.1

Делает поля типов данных, определённых в текущем модуле, жёсткими по умолчанию.

Неформально расширение языка StrictData переключает объявления типов данных на жёсткие по умолчанию, позволяя полям быть ленивыми, добавив ~ перед полем.

Когда пользователь пишет

data T = C a
data T' = C' ~a

мы интерпретируем это так, как будто они написали

data T = C !a
data T' = C' a

Расширение затрагивает только определения в этом модуле.

10.29.3. Привязки шаблонов по умолчанию жёсткие

Strict
Подразумевает

StrictData

С

8.0.1

Делает привязки в текущем модуле жёсткими по умолчанию.

Неформально, расширение языка Strict переключает функции, типы данных и привязки на жёсткие по умолчанию, позволяя добавлять необязательную ленивость, добавив ~ перед переменной. Это по сути переворачивает текущую ситуацию, где ленивость является значением по умолчанию, а жёсткость может быть необязательно добавлена, добавив ! перед переменной.

Strict подразумевает StrictData.

  • Определения функций

    Когда пользователь пишет

    f x = ...
    

    мы интерпретируем это так, как будто они написали

    f !x = ...
    

    Добавление ~ перед x даёт обычное ленивое поведение.

    Преобразование шаблонов в неуклонные требует ~(~p) или (~ ~p) при включённом Strict.

  • Привязки let/where

    Когда пользователь пишет

    let x = ...
    let pat = ...
    

    мы интерпретируем это так, как будто они написали

    let !x = ...
    let !pat = ...
    

    Добавление ~ перед x даёт обычное ленивое поведение. Общее правило состоит в том, что мы добавляем неявный восклицательный знак к внешнему шаблону, если это не отключено с помощью ~.

  • Сопоставление шаблонов в выражениях case, лямбдах, do-нотации и т. д.

    Внешний шаблон всех сопоставлений с образцом получает неявный восклицательный знак, если это не отключено с помощью ~. Это относится к выражениям case, шаблонам в лямбдах, do-нотации, списковым включениям и т. д. Например

    case x of (a,b) -> rhs
    

    интерпретируется как

    case x of !(a,b) -> rhs
    

    Поскольку семантика сопоставления с образцом в выражениях case жёсткая, это обычно никак не влияет. Но это имеет значение в вырожденном случае переменных и новых типов. Поэтому

    case x of y -> rhs
    

    ленивая в Haskell, но с Strict интерпретируется как

    case x of !y -> rhs
    

    что оценивает x. Аналогично, если newtype Age = MkAge Int, то

    case x of MkAge i -> rhs
    

    ленивая в Haskell; но с Strict добавленный восклицательный знак делает её жёсткой.

    Аналогично

    \ x -> body
    do { x <- rhs; blah }
    [ e | x <- rhs; blah }
    

    все получают неявные восклицательные знаки на шаблоне x.

  • Вложенные шаблоны

    Обратите внимание, что мы не добавляем восклицательные знаки к вложенным шаблонам. Например

    let (p,q) = if flob then (undefined, undefined) else (True, False)
    in ...
    

    будет вести себя как

    let !(p,q) = if flob then (undefined, undefined) else (True,False)
    in ...
    

    что будет строго оценивать правую часть и связывать p и q с компонентами пары. Но сама пара ленивая (если мы также не скомпилируем Prelude с Strict; см. Модульность ниже). Так что p и q могут оказаться связанными с неопределёнными значениями. См. также Динамическая семантика шаблонов с восклицательными знаками ниже.

  • Привязки на верхнем уровне

    не затрагиваются Strict. Например:

    x = factorial 20
    (y,z) = if x > 10 then True else False
    

    Здесь x и привязка шаблона (y,z) остаются ленивыми. Причина: нет хорошей точки, чтобы их принудительно оценить, до первого использования.

  • Новые типы

    На новые типы нет влияния, они просто переименовывают существующие типы. Например:

    newtype T = C a
    f (C x)  = rhs1
    g !(C x) = rhs2
    

    В обычном Haskell, f ленивая в своём аргументе и поэтому в x; а g жёсткая в своём аргументе и поэтому также жёсткая в x. С Strict, оба становятся жёсткими, потому что аргумент f получает неявный восклицательный знак.

10.29.4. Модульность

Strict и StrictData влияют только на определения в том модуле, в котором они используются. Функции и типы данных, импортированные из других модулей, не затрагиваются. Например, мы не будем оценивать аргумент функции Just перед применением конструктора. Аналогично, мы не будем оценивать первый аргумент функции Data.Map.findWithDefault перед применением функции.

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

Кортежи, списки, Maybe, и все другие типы из Prelude сохраняют свою существующую ленивую семантику.

10.29.5. Динамическая семантика шаблонов с восклицательными знаками

Семантика сопоставления с образцом в Haskell описана в разделе 3.17.2 отчета Haskell. К этому описанию добавьте один дополнительный пункт 10, сказав:

  • Сопоставление шаблона !pat со значением v происходит следующим образом:

    • если v — это bottom, сопоставление расходится
    • в противном случае pat сопоставляется с v

Аналогично, в рис. 4 раздела 3.17.3 добавьте новый случай (t):

case v of { !pat -> e; _ -> e' }
   = v `seq` case v of { pat -> e; _ -> e' }

Это оставляет выражения let, чьё преобразование дано в разделе 3.12 отчёта Haskell. Замените «Преобразование» там на следующее. Дано let { bind1 ... bindn } in body:

ПРИНУДИТЕЛЬНОЕ ВЫПОЛНЕНИЕ

Замените любую привязку !p = e на v = case e of p -> (x1, ..., xn); (x1, ..., xn) = v и замените body на v seq body, где v — это новая переменная. Это преобразование хорошо работает, если p уже является переменной x, но, очевидно, может быть оптимизировано, не вводя новую переменную v.

РАЗДЕЛЕНИЕ

Замените любую привязку p = e, где p не является переменной, на v = e; x1 = case v of p -> x1; ...; xn = case v of p -> xn, где v — это новая переменная, а x1.. xn — это связываемые переменные p. Опять же, если e является переменной, это можно оптимизировать, не вводя новую переменную.

Результат будет (возможно) рекурсивным набором привязок, связывающих только простые переменные слева. (Можно пойти на шаг дальше, как в отчёте Haskell, и сделать рекурсивные привязки нерекурсивными с помощью fix, но мы этого не делаем в ядре, и это только усложняет вопрос, поэтому мы этого не делаем здесь.)

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

Вот несколько примеров, как работает это преобразование. Первое выражение в каждом ряду — это исходный код Haskell; последующие — Core.

Вот простой нерекурсивный случай:

let x :: Int     -- Non-recursive
    !x = factorial y
in body

===> (FORCE)
    let x = factorial y in x `seq` body

===> (inline seq)
    let x = factorial y in case x of x -> body

===> (inline x)
    case factorial y of x -> body

То же самое, только с привязкой к шаблону:

let !(Just x, Left y) = e in body

===> (FORCE)
    let v = case e of (Just x, Left y) -> (x,y)
        (x,y) = v
    in v `seq` body

===> (SPLIT)
    let v = case e of (Just x, Left y) -> (x,y)
        x = case v of (x,y) -> x
        y = case v of (x,y) -> y
    in v `seq` body

===> (inline seq, float x,y bindings inwards)
    let v = case e of (Just x, Left y) -> (x,y)
    in case v of v -> let x = case v of (x,y) -> x
                          y = case v of (x,y) -> y
                      in body

===> (fluff up v's pattern; this is a standard Core optimisation)
    let v = case e of (Just x, Left y) -> (x,y)
    in case v of v@(p,q) -> let x = case v of (x,y) -> x
                                y = case v of (x,y) -> y
                            in body

===> (case of known constructor)
    let v = case e of (Just x, Left y) -> (x,y)
    in case v of v@(p,q) -> let x = p
                                y = q
                            in body

===> (inline x,y, v)
    case (case e of (Just x, Left y) -> (x,y) of
        (p,q) -> body[p/x, q/y]

===> (case of case)
    case e of (Just x, Left y) -> body[p/x, q/y]

Конечная форма — именно то, что нам нужно: простое выражение case.

Вот рекурсивное выражение case

letrec xs :: [Int]  -- Recursive
        !xs = factorial y : xs
in body

===> (FORCE)
    letrec xs = factorial y : xs in xs `seq` body

===> (inline seq)
    letrec xs = factorial y : xs in case xs of xs -> body

===> (eliminate case of value)
    letrec xs = factorial y : xs in body

и полиморфное:

let f :: forall a. [a] -> [a]    -- Polymorphic
    !f = fst (reverse, True)
in body

===> (FORCE)
    let f = /\a. fst (reverse a, True) in f `seq` body
===> (inline seq, inline f)
    case (/\a. fst (reverse a, True)) of f -> body

Обратите внимание, что seq добавляется только при переводе в Core. Если бы мы сделали это в исходном коде Haskell, то

let f = ... in f `seq` body

тогда полиморфный тип f был бы подставлен, поэтому перевод Core был бы

let f = ... in f Any `seq` body

Когда задействовано перегрузка, результаты могут быть несколько неинтуитивными:

let f :: forall a. Eq a => a -> [a] -> Bool    -- Overloaded
    !f = fst (member, True)
in body

===> (FORCE)
    let f = /\a \(d::Eq a). fst (member, True) in f `seq` body

===> (inline seq, case of value)
    let f = /\a \(d::Eq a). fst (member, True) in body

Обратите внимание, что восклицательный знак в этом случае не оказывает никакого влияния

10.30. Утверждения

Если вы хотите использовать утверждения в вашем стандартном коде Haskell, вы можете определить функцию, подобную следующей:

assert :: Bool -> a -> a
assert False x = error "assertion failed!"
assert _     x = x

которая работает, но возвращает менее полезное сообщение об ошибке — утверждение не выполнилось, но какое и где?

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

GHC оказывает здесь помощь, выполняя все это за вас. Для каждого использования assert в исходном коде пользователя:

kelvinToC :: Double -> Double
kelvinToC k = assert (k >= 0.0) (k-273.15)

GHC будет переписывать это, включая также местоположение в исходном коде, где было сделано утверждение,

assert pred val ==> assertError "Main.hs|15" pred val

Переписывание выполняется только компилятором при обнаружении применений Control.Exception.assert, поэтому вы всё ещё можете определять и использовать собственные версии assert, если это необходимо. В противном случае, импортируйте Control.Exception для использования assert в вашем коде.

GHC игнорирует утверждения, когда оптимизация включена с флагом -O. То есть выражения вида assert pred e будут переписаны в e. Вы также можете отключить утверждения, используя опцию -fignore-asserts. Опция -fno-ignore-asserts позволяет включить утверждения даже при включенной оптимизации.

Ошибка утверждения может быть перехвачена, см. документацию для библиотеки Control.Exception для подробностей.

10.31. Статические указатели

StaticPointers
Since

7.10.1

Разрешить использование синтаксиса статических указателей.

Расширение языка StaticPointers добавляет новую синтаксическую форму static e, которая обозначает ссылку на замкнутое выражение ⟨e⟩. Эта ссылка стабильна и портативна, в том смысле, что она остается действительной в различных процессах, возможно, на разных машинах. Таким образом, процесс может создать ссылку и отправить ее другому процессу, который может разрешить ее в ⟨e⟩.

При включенном расширении static больше не является допустимым идентификатором.

Статические указатели были впервые предложены в статье Towards Haskell in the cloud, Джефф Эпштейн, Эндрю П. Блэк и Саймон Пейтон-Джонс, Труды четвертой конференции ACM по языку Haskell, стр. 118–129, ACM, 2011.

10.31.1. Использование статических указателей

Каждой ссылке присваивается ключ, который может использоваться для поиска ее во время выполнения с помощью GHC.StaticPtr.unsafeLookupStaticPtr, который использует глобальную и неизменяемую таблицу, называемую таблицей статических указателей. Компилятор включает записи в эту таблицу для всех статических форм, найденных в связанных модулях. Значение можно получить из ссылки с помощью GHC.StaticPtr.deRefStaticPtr.

Тело e выражения static e должно быть замкнутым выражением. Выражение считается замкнутым, если все его свободные (типовые) переменные замкнуты. Переменная замкнута, если она связана с помощью let с замкнутым выражением и ее тип также замкнут. Тип замкнут, если у него нет свободных переменных.

Все следующие варианты допустимы:

inc :: Int -> Int
inc x = x + 1

ref1 = static 1
ref2 = static inc
ref3 = static (inc 1)
ref4 = static ((\x -> x + 1) (1 :: Int))
ref5 y = static (let x = 1 in x)
ref6 y = let x = 1 in static x

В то время как следующие определения отклоняются:

ref7 y = let x = y in static x    -- x is not closed
ref8 y = static (let x = 1 in y)  -- y is not let-bound
ref8 (y :: a) = let x = undefined :: a
                 in static x      -- x has a non-closed type

Примечание

Модули, загруженные в GHCi с помощью команды :load, могут использовать StaticPointers и static выражения, но введенные в интерактивную оболочку (REPL) — нет. Это ограничение GHCi; см. #12356 для подробностей.

Примечание

Множество ключей, используемых для поиска статических указателей в таблице статических указателей, не гарантируется как стабильное для различных бинарных файлов программы. Или, другими словами, только процессы, запущенные из одного и того же бинарного файла программы, гарантированно используют один и тот же набор ключей.

10.31.2. Статическая семантика статических указателей

Неформально, если у нас есть замкнутое выражение

e :: forall a_1 ... a_n . t

то статическая форма имеет тип

static e :: (IsStatic p, Typeable a_1, ... , Typeable a_n) => p t

Статическая форма определяет значение типа StaticPtr t, но, как и OverloadedLists и OverloadedStrings, это литеральное выражение перегружено, чтобы позволить подъем StaticPtr в другой тип неявно через класс IsStatic:

class IsStatic p where
    fromStaticPtr :: StaticPtr a -> p a

Единственный предопределенный экземпляр — очевидный, который ничего не делает:

instance IsStatic StaticPtr where
    fromStaticPtr sptr = sptr

См. GHC.StaticPtr.IsStatic.

Кроме того, тип t должен иметь экземпляр Typeable. Следующие варианты недопустимы:

static show                    -- No Typeable instance for (Show a => a -> String)
static Control.Monad.ST.runST  -- No Typeable instance for ((forall s. ST s a) -> a)

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

{-# LANGUAGE ConstraintKinds           #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE Rank2Types                #-}
{-# LANGUAGE StandaloneDeriving        #-}
{-# LANGUAGE StaticPointers            #-}

import Control.Monad.ST
import Data.Typeable
import GHC.StaticPtr

data Dict c = c => Dict

g1 :: Typeable a => StaticPtr (Dict (Show a) -> a -> String)
g1 = static (\Dict -> show)

data Rank2Wrapper f = R2W (forall s. f s)
  deriving Typeable
newtype Flip f a s = Flip { unFlip :: f s a }
  deriving Typeable

g2 :: Typeable a => StaticPtr (Rank2Wrapper (Flip ST a) -> a)
g2 = static (\(R2W f) -> runST (unFlip f))

10.32. Директивы

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

Все директивы имеют вид {-# word ... #-} , где ⟨word⟩ указывает тип директивы и, необязательно, информацию, специфичную для этого типа. Регистр в ⟨word⟩ игнорируется. Различные значения для ⟨word⟩, понимаемые GHC, описаны в следующих разделах; любая встреченная директива с неизвестным ⟨word⟩ игнорируется. Правило выравнивания применяется к директивам, поэтому закрывающая #-} должна начинаться в столбце справа от открывающей {-#.

Определенные директивы являются директивами заголовка файла:

  • Директива заголовка файла должна предшествовать ключевому слову module в файле.
  • Можно использовать сколько угодно директив заголовка файла, и они могут быть предваряемы или следовать за комментариями.
  • Директивы заголовка файла читаются только один раз, до предварительной обработки файла (например, с помощью cpp).
  • Директивами заголовка файла являются {-# LANGUAGE #-}, {-# OPTIONS_GHC #-}, и {-# INCLUDE #-}.

10.32.1. LANGUAGE директива

{-# LANGUAGE ⟨ext⟩, ⟨ext⟩, ... #-}
Где

заголовок файла

Включить или отключить набор расширений языка.

Директива LANGUAGE позволяет включить расширения языка портативным способом. Предполагается, что все компиляторы Haskell поддерживают директиву LANGUAGE с тем же синтаксисом, хотя, конечно, не все расширения поддерживаются всеми компиляторами. Директиву LANGUAGE следует использовать вместо OPTIONS_GHC, если это возможно.

Например, для включения FFI и предварительной обработки с CPP:

{-# LANGUAGE ForeignFunctionInterface, CPP #-}

LANGUAGE — это директива заголовка файла (см. Директивы).

Каждое расширение языка также может быть преобразовано в флаг командной строки, добавив префикс «-X»; например, -XForeignFunctionInterface. (Аналогично, все флаги «-X» могут быть записаны как директивы LANGUAGE.)

Список всех поддерживаемых расширений языка можно получить, вызвав ghc --supported-extensions (см. --supported-extensions).

Любое расширение из типа Extension , определенного в Language.Haskell.Extension, может быть использовано. GHC сообщит об ошибке, если какие-либо запрошенные расширения не поддерживаются.

10.32.2. OPTIONS_GHC директива

{-# OPTIONS_GHC ⟨flags⟩ #-}
Где

заголовок файла

Директива OPTIONS_GHC используется для указания дополнительных опций, предоставляемых компилятору при компиляции этого исходного файла. Подробности см. в Параметры командной строки в исходных файлах.

Предыдущие версии GHC принимали OPTIONS вместо OPTIONS_GHC, но это устарело.

OPTIONS_GHC — это директива заголовка файла (см. Директивы).

10.32.3. INCLUDE директива

Директива INCLUDE раньше была необходима для указания заголовочных файлов для включения при использовании FFI и компиляции через C. Она больше не требуется для GHC, но принимается (и игнорируется) для совместимости с другими компиляторами.

10.32.4. WARNING и DEPRECATED директивы

{-# WARNING #-}
Где

объявление

Директива WARNING позволяет добавить произвольное предупреждение к определённой функции, классу или типу.

{-# DEPRECATED #-}
Где

объявление

Директива DEPRECATED указывает, что определённая функция, класс или тип устарели.

Существует два способа использования этих директив.

  • Вы можете работать с целым модулем следующим образом:

    module Wibble {-# DEPRECATED "Use Wobble instead" #-} where
      ...
    

    Или:

    module Wibble {-# WARNING "This is an unstable interface." #-} where
      ...
    

    При компиляции любого модуля, который импортирует Wibble, GHC выведет указанное сообщение.

  • Вы можете добавить предупреждение к функции, классу, типу или конструктору данных с помощью следующих объявлений на верхнем уровне:

    {-# DEPRECATED f, C, T "Don't use these" #-}
    {-# WARNING unsafePerformIO "This is unsafe; I hope you know what you're doing" #-}
    

    При компиляции любого модуля, который импортирует и использует любой из указанных объектов, GHC выведет указанное сообщение.

    Вы можете добавлять директивы только к объектам, объявленным на верхнем уровне в компилируемом модуле, и вы можете использовать только имена без квалификаторов в списке объектов. Заглавное имя, такое как T относится либо к конструктору типа T либо к конструктору данных T, или к обоим, если оба находятся в области видимости. Если оба находятся в области видимости, в настоящее время нет способа указать один без другого (см. фиксы Инфиксные конструкторы типов, классы и переменные типов).

Также обратите внимание, что аргумент к DEPRECATED и WARNING также может быть списком строк, в этом случае строки будут отображаться на отдельных строках в результирующем сообщении о предупреждении,

{-# DEPRECATED foo, bar ["Don't use these", "Use gar instead"] #-}

Предупреждения и устаревания не отображаются для (а) использования в модуле определения, (б) определения метода в экземпляре класса и (в) использования в списке экспорта. Последнее уменьшает ложные сообщения о проблемах в библиотеке, в которой один модуль собирает и повторно экспортирует экспорт нескольких других.

Вы можете подавить предупреждения с помощью флага -Wno-warnings-deprecations.

10.32.5. MINIMAL директива

{-# MINIMAL ⟨name⟩ | ⟨name⟩ , ... #-}
Где

в теле класса

Определите методы, необходимые для минимального полного экземпляра класса.

Директива MINIMAL используется для указания минимального полного определения класса, т.е. указания, какие методы должны быть реализованы всеми экземплярами. Если экземпляр не удовлетворяет минимальному полному определению, генерируется предупреждение. Это может быть полезно, когда класс имеет методы с циклическими значениями по умолчанию. Например

class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool
    x == y = not (x /= y)
    x /= y = not (x == y)
    {-# MINIMAL (==) | (/=) #-}

Без директивы MINIMAL предупреждение не будет генерироваться для экземпляра, реализующего ни один метод.

Синтаксис для минимального полного определения:

mindef ::= name
        |  '(' mindef ')'
        |  mindef '|' mindef
        |  mindef ',' mindef

Вертикальная черта обозначает дизъюнкцию, т.е. требуется один из двух сторон. Запятая обозначает конъюнкцию, т.е. необходимы обе стороны. Конъюнкция связывает сильнее, чем дизъюнкция.

Если в объявлении класса нет директивы MINIMAL , это равносильно тому, что была указана директива {-# MINIMAL op1, op2, ..., opn #-} , где opi — это методы, которым не хватает метода по умолчанию в объявлении класса (см. -Wmissing-methods, Предупреждения и проверки на корректность).

Это предупреждение можно отключить с помощью флага -Wno-missing-methods.

10.32.6. INLINE и NOINLINE директивы

Эти директивы управляют встраиванием определений функций.

10.32.6.1. INLINE директива

{-# INLINE ⟨name⟩ #-}
Где

на верхнем уровне

Принудительно выполнить встраивание значения.

GHC (с -O, как всегда) пытается встроить (или «развернуть») функции/значения, которые «достаточно малы», тем самым избегая накладных расходов вызова и, возможно, открывая другие более замечательные оптимизации. GHC имеет набор эвристик, настроенных в течение длительного времени с использованием многих бенчмарков, которые определяют, когда выгодно встроить функцию в её месте вызова. Эвристики разработаны для встраивания функций, когда это, по-видимому, выгодно, но без чрезмерного увеличения размера кода. Если функция кажется слишком большой, она не будет встроена, а функции, размер которых больше определённого значения, даже не будут экспортированы в файл интерфейса. Некоторые из пороговых значений, которые управляют этими эвристическими решениями, можно изменить с помощью флагов, см. -f*: независимые от платформы флаги.

Обычно GHC самостоятельно выполняет разумную работу по определению того, когда встраивание функции является хорошей идеей. Однако иногда вам может потребоваться переопределить стандартное поведение. Например, если у вас есть ключевая функция, которую важно встроить, потому что это приводит к дальнейшим оптимизациям, но GHC считает её слишком большой для встраивания.

Молотком, который вы можете использовать, является директива INLINE , используемая следующим образом:

key_function :: Int -> String -> (Bool, Double)
{-# INLINE key_function #-}

Основное действие директивы INLINE состоит в том, чтобы объявить «стоимость» функции очень низкой. Тогда стандартный механизм развертывания будет очень стараться встроить её. Однако директива INLINE для функции «f» имеет ряд других эффектов:

  • Хотя GHC стремится встроить функцию, он не делает этого слепо. Например, если вы напишете

    map key_function xs
    

    на самом деле нет смысла встраивать key_function , чтобы получить

    map (\x -> body) xs
    

    В общем случае GHC встраивает функцию только в том случае, если есть какие-либо причины (неважно насколько незначительные) полагать, что это полезно.

  • Кроме того, GHC будет встраивать функцию только в том случае, если она полностью применена, где «полностью применено» означает применено к такому количеству аргументов, какое указано (синтаксически) в левой части определения функции. Например:

    comp1 :: (b -> c) -> (a -> b) -> a -> c
    {-# INLINE comp1 #-}
    comp1 f g = \x -> f (g x)
    
    comp2 :: (b -> c) -> (a -> b) -> a -> c
    {-# INLINE comp2 #-}
    comp2 f g x = f (g x)
    

    Две функции comp1 и comp2 имеют одинаковый смысл, но comp1 будет встроена при применении к двум аргументам, в то время как comp2 требует трёх. Это может сильно отличаться, если вы скажете

    map (not `comp1` not) xs
    

    что будет оптимизировано лучше, чем соответствующее использование comp2.

  • Полезно для GHC оптимизировать определение функции INLINE f так же, как и любую другую функцию, не являющуюся INLINE, на случай, если в конечном итоге вызывается не встроенная версия f . Но мы не хотим встраивать оптимизированную версию f; основная причина директив INLINE — раскрыть функции в правой части f , которые имеют правила переписывания, и это не сработает, если эти функции были оптимизированы.

    Таким образом, GHC гарантирует встраивание именно того кода, который вы написали, ни больше, ни меньше. Он делает это, создавая копию определения функции для использования при встраивании (мы называем это «inline-RHS»), которую он оставляет без изменений, оптимизируя обычную правую часть как обычно. Для внешних функций inline-RHS (а не оптимизированная RHS) записывается в файле интерфейса.

  • Функция INLINE не обрабатывается анализом строгости, она встраивается полностью вместо этого.

GHC гарантирует, что встраивание не может продолжаться бесконечно: каждая взаимно-рекурсивная группа разбивается одним или несколькими прерывателями цикла, которые никогда не встраиваются (см. Secrets of the GHC inliner, JFP 12(4) July 2002). GHC пытается не выбирать функцию с директивой INLINE в качестве прерывателя цикла, но когда нет выбора, даже функция INLINE может быть выбрана, в этом случае директива INLINE игнорируется. Например, для саморекурсивной функции прерывателем цикла может быть только сама функция, поэтому директива INLINE всегда игнорируется.

Синтаксически, директива INLINE для функции может быть размещена в любом месте, где может быть размещена её сигнатура типа.

Директивы INLINE особенно полезны для функций then/return (или bind/unit ) в монаде. Например, в собственном коде монады UniqueSupply GHC есть:

{-# INLINE thenUs #-}
{-# INLINE returnUs #-}

См. также директивы NOINLINE (Директива NOINLINE) и INLINABLE (Директива INLINABLE).

10.32.6.2. INLINABLE директива

{-# INLINABLE ⟨name⟩ #-}
Где

на верхнем уровне

Предлагает компилятору всегда рассматривать встраивание name.

Директива INLINABLE для функции f имеет следующее поведение:

  • В то время как INLINE говорит «пожалуйста, встройте меня», INLINABLE говорит «чувствуйте себя свободно, встраивайте меня; используйте свое усмотрение». Другими словами, выбор оставляется GHC, который использует те же правила, что и для функций без псевдонима.
  • В отличие от INLINE, это решение принимается на уровне вызова и поэтому будет зависеть от порога вставки, уровня оптимизации и т. д.
  • Как и INLINE, директива INLINABLE сохраняет копию исходного правого члена для целей вставки и сохраняет её в файле интерфейса, независимо от размера правого члена.
  • Один из способов использования INLINABLE — в сочетании со специальной функцией inline (Специальные встроенные функции). Вызов inline f прилагает значительные усилия для вставки f. Чтобы убедиться, что f может быть вставлен, рекомендуется пометить определение f как INLINABLE, чтобы GHC гарантировал раскрытие независимо от того, насколько оно велико. Кроме того, аннотируя f как INLINABLE, вы обеспечиваете, что исходный правый член f будет вставлен, а не любая случайная оптимизированная версия f оптимизатора GHC.
  • Директива INLINABLE также работает с SPECIALISE: если вы пометите функцию f как INLINABLE, то вы можете в дальнейшем SPECIALISE в другом модуле (см. Директиву SPECIALIZE).
  • В отличие от INLINE, использование директивы INLINABLE для рекурсивной функции допустимо. Основная причина для этого — разрешить последующее использование SPECIALISE.

Альтернативное написание INLINEABLE также поддерживается GHC.

10.32.6.3. Директива NOINLINE

{-# NOINLINE ⟨name⟩ #-}
Где

глобальный уровень

Инструктирует компилятор не вставлять значение.

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

NOTINLINE — синоним для NOINLINE (NOINLINE определена в Haskell 98 как стандартный способ отключения вставки, поэтому её следует использовать, если вы хотите обеспечить совместимость кода).

10.32.6.4. Модификатор CONLIKE

{-# CONLIKE #-}
Где

модифицирует директиву INLINE или NOINLINE

Инструктирует GHC считать значение особенно дешёвым для вставки.

Директива INLINE или NOINLINE может иметь модификатор CONLIKE, который влияет на сопоставление в RULE (только). См. Как правила взаимодействуют с директивами CONLIKE.

10.32.6.5. Управление фазами

Иногда необходимо точно контролировать, когда в процессе GHC включается директива INLINE. Вставка происходит только во время выполнения упростителя. Каждый запуск упростителя имеет свой номер фазы; номер фазы уменьшается к нулю. Если вы используете -dverbose-core2core, вы увидите последовательность номеров фаз для последовательных запусков упростителя. В директиве INLINE можно необязательно указать номер фазы, таким образом:

  • «INLINE[k] f» означает: не вставлять f до фазы k, но начиная с фазы k активно вставлять.
  • «INLINE[~k] f» означает: активно вставлять f до фазы k, но начиная с фазы k — не вставлять.
  • «NOINLINE[k] f» означает: не вставлять f до фазы k, но начиная с фазы k — вставлять (как если бы директива отсутствовала).
  • «NOINLINE[~k] f» означает: активно вставлять f до фазы k, но начиная с фазы k — не вставлять.

Та же информация суммирована здесь:

                         -- Before phase 2     Phase 2 and later
{-# INLINE   [2]  f #-}  --      No                 Yes
{-# INLINE   [~2] f #-}  --      Yes                No
{-# NOINLINE [2]  f #-}  --      No                 Maybe
{-# NOINLINE [~2] f #-}  --      Maybe              No

{-# INLINE   f #-}       --      Yes                Yes
{-# NOINLINE f #-}       --      No                 No

Под «Возможно» подразумевается, что применяются обычные правила вставки (если тело функции мало, или оно применяется к интересным аргументам и т. д.). Другой способ понять семантику — такой:

  • Для INLINE и NOINLINE номер фазы определяет, когда вообще разрешена вставка.
  • Директива INLINE дополнительно заставляет тело функции казаться малым, так что при разрешении вставки она очень вероятно произойдёт.

Такой же контроль по номерам фаз доступен для RULE (Правила переписывания).

10.32.7. Директива LINE

{-# LINE ⟨lineno⟩ "⟨file⟩" #-}
Где

в любом месте

Генерируется препроцессорами для передачи номеров строк исходного кода.

Эта директива похожа на директиву C #line, и используется в основном в автоматически генерируемом Haskell-коде. Она позволяет указывать номер строки и имя файла исходного кода; например:

{-# LINE 42 "Foo.vhs" #-}

если текущий файл был сгенерирован из чего-то, что называлось Foo.vhs, и эта строка соответствует строке 42 в оригинале. GHC скорректирует свои сообщения об ошибках, чтобы сослаться на строку/файл, указанные в директиве LINE.

Директивы LINE, сгенерированные из Template Haskell, устанавливают позицию файла и строки на время вставки и ограничены этой вставкой. Обратите внимание, что поскольку Template Haskell обрабатывает абстрактный синтаксис, позиции файлов не вычисляются автоматически.

10.32.8. Директива COLUMN

Это аналог директивы LINE и предназначена для использования в автоматически генерируемом Haskell-коде. Она позволяет указывать номер столбца исходного кода; например:

foo = do
  {-# COLUMN 42 #-}pure ()
  pure ()

Это устанавливает все номера столбцов сразу после директивы на 42. Наличие этой директивы влияет только на качество диагностики и не изменяет синтаксис кода.

10.32.9. Директива RULES

Директива RULES позволяет задавать правила переписывания. Она описана в Правилах переписывания.

10.32.10. Директива SPECIALIZE

{-# SPECIALIZE ⟨name⟩ :: ⟨type⟩ #-}

Запрашивает специализацию полиморфного значения для определённого типа.

(Альтернативное написание также поддерживается.) Для ключевых перегруженных функций можно создавать дополнительные версии (обратите внимание: за счёт увеличения размера кода), специализированные под определённые типы. Таким образом, если у вас есть перегруженная функция:

hammeredLookup :: Ord key => [(key, value)] -> key -> value

Если она интенсивно используется со списками с ключами Widget, вы можете специализировать её следующим образом:

{-# SPECIALIZE hammeredLookup :: [(Widget, value)] -> Widget -> value #-}
  • Предикат SPECIALIZE для функции может быть помещен в любом месте, где может быть помещено её описание типа. Кроме того, вы также можете SPECIALIZE импортированную функцию, при условии, что ей был присвоен предикат INLINABLE в месте её определения (Предикат INLINABLE).
  • Предикат SPECIALIZE имеет эффект генерации (а) специализированной версии функции и (б) правила переписывания (см. Правила переписывания), которое переписывает вызов неспециализированной функции в вызов специализированной. Кроме того, учитывая предикат SPECIALIZE для функции f, GHC автоматически создаст специализации для любых функций с перегрузкой типа класса, вызываемых f, если они находятся в том же модуле, что и предикат SPECIALIZE, или если они INLINABLE; и так далее, транзитивно.
  • Вы можете добавить управление фазами (Управление фазами) к правилу RULE, сгенерированному предикатом SPECIALIZE, точно так же, как если бы вы написали предикат RULE напрямую. Например:

    {-# SPECIALIZE [0] hammeredLookup :: [(Widget, value)] -> Widget -> value #-}
    

    генерирует правило специализации, которое срабатывает только на Фазе 0 (конечной фазе). Если вы не укажете никакого управления фазами в предикате SPECIALIZE, управление фазами унаследуется от предиката inline (если таковой имеется) функции. Например:

    foo :: Num a => a -> a
    foo = ...blah...
    {-# NOINLINE [0] foo #-}
    {-# SPECIALIZE foo :: Int -> Int #-}
    

    Предикат NOINLINE сообщает GHC не встраивать foo до Фазы 0; и это свойство унаследуется правилом специализации RULE, которое, следовательно, будет срабатывать только на Фазе 0.

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

  • Тип в предикате SPECIALIZE может быть любым типом, менее полиморфным, чем тип исходной функции. В конкретных терминах, если исходная функция — f, то предикат

    {-# SPECIALIZE f :: <type> #-}
    

    действителен тогда и только тогда, когда определение

    f_spec :: <type>
    f_spec = f
    

    действительно. Вот несколько примеров (где мы даём только описание типа исходной функции, а не её код):

    f :: Eq a => a -> b -> b
    {-# SPECIALISE f :: Int -> b -> b #-}
    
    g :: (Eq a, Ix b) => a -> b -> b
    {-# SPECIALISE g :: (Eq a) => a -> Int -> Int #-}
    
    h :: Eq a => a -> a -> a
    {-# SPECIALISE h :: (Eq a) => [a] -> [a] -> [a] #-}
    

    Последний из этих примеров сгенерирует RULE с несколько сложной левой частью (попробуйте сами), поэтому он может не сработать очень хорошо. Если вы используете этот вид специализации, сообщите нам, как он работает.

10.32.10.1. SPECIALIZE INLINE

{-# SPECIALIZE INLINE ⟨name⟩ :: ⟨type⟩ #-}
Где

глобальный уровень

Предикат SPECIALIZE может быть необязательно дополнен предикатами INLINE или NOINLINE, необязательно с указанием фазы, как описано в предикатах INLINE и NOINLINE. Предикат INLINE влияет на специализированную версию функции (только) и применяется даже если функция рекурсивная. Пример — это:

-- A GADT for arrays with type-indexed representation
data Arr e where
  ArrInt :: !Int -> ByteArray# -> Arr Int
  ArrPair :: !Int -> Arr e1 -> Arr e2 -> Arr (e1, e2)

(!:) :: Arr e -> Int -> e
{-# SPECIALISE INLINE (!:) :: Arr Int -> Int -> Int #-}
{-# SPECIALISE INLINE (!:) :: Arr (a, b) -> Int -> (a, b) #-}
(ArrInt _ ba)     !: (I# i) = I# (indexIntArray# ba i)
(ArrPair _ a1 a2) !: i      = (a1 !: i, a2 !: i)

Здесь, (!:) — рекурсивная функция, индексирующая массивы типа Arr e. Рассмотрим вызов (!:) типа (Int,Int). Вторая специализация сработает, и специализированная функция будет встроена. У неё есть два вызова (!:), оба типа Int. Оба эти вызова запускают первую специализацию, тело которой также встроенно. Результатом является основанное на типе развертывание функции индексирования.

Вы можете добавить явное управление фазами (Управление фазами) к предикату SPECIALISE INLINE так же, как и к предикату INLINE; если вы это сделаете, то та же фаза будет использоваться для правила переписывания и управления INLINE специализированной функции.

Предупреждение

Вы можете вызвать расхождение в GHC, используя SPECIALISE INLINE для обычно рекурсивной функции.

10.32.10.2. SPECIALIZE для импортированных функций

Как правило, вы можете использовать предикат SPECIALIZE только для функций, определённых в том же модуле. Однако, если функция f имеет предикат INLINABLE в месте своего определения, то её можно специализировать, импортируя модули (см. Предикат INLINABLE). Например

module Map( lookup, blah blah ) where
  lookup :: Ord key => [(key,a)] -> key -> Maybe a
  lookup = ...
  {-# INLINABLE lookup #-}

module Client where
  import Map( lookup )

  data T = T1 | T2 deriving( Eq, Ord )
  {-# SPECIALISE lookup :: [(T,a)] -> T -> Maybe a

Здесь, lookup объявлена INLINABLE, но не может быть специализирована для типа T в месте своего определения, потому что этот тип ещё не существует. Вместо этого модуль клиента может определить T и затем специализировать lookup для этого типа.

Кроме того, каждый модуль, который импортирует Client (или импортирует модуль, который импортирует Client, транзитивно), «увидит» и воспользуется специализированной версией lookup. Вам не нужно размещать предикат SPECIALIZE в каждом модуле.

Кроме того, вам часто даже не нужен предикат SPECIALIZE в первую очередь. При компиляции модуля M, оптимизатор GHC (при заданном флаге -O) автоматически рассматривает каждую функцию с перегрузкой, объявленную на верхнем уровне в M, и специализирует её для различных типов, в которых она вызывается в M. Оптимизатор также рассматривает каждую импортированную функцию с перегрузкой INLINABLE и специализирует её для различных типов, в которых она вызывается в M. Таким образом, в нашем примере достаточно, чтобы lookup вызывалась с типом T:

module Client where
  import Map( lookup )

  data T = T1 | T2 deriving( Eq, Ord )

  findT1 :: [(T,a)] -> Maybe a
  findT1 m = lookup m T1   -- A call of lookup at type T

Однако иногда таких вызовов нет, в этом случае предикат может быть полезен.

10.32.11. SPECIALIZE предикат экземпляра

{-# SPECIALIZE instance ⟨instance head⟩ #-}
Где

тело экземпляра

Та же идея, за исключением объявлений экземпляров. Например:

instance (Eq a) => Eq (Foo a) where {
   {-# SPECIALIZE instance Eq (Foo [(Int, Bar)]) #-}
   ... usual stuff ...
 }

Предикат должен находиться внутри части where объявления экземпляра.

10.32.12. UNPACK предикат

{-# UNPACK #-}
Где

поле конструктора данных

Инструктирует компилятор распаковывать содержимое поля конструктора в сам конструктор.

Предикат UNPACK указывает компилятору, что он должен распаковать содержимое поля конструктора в сам конструктор, устранив уровень косвенности. Например:

data T = T {-# UNPACK #-} !Float
           {-# UNPACK #-} !Float

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

Распаковка полей конструкторов должна использоваться только совместно с -O 1, чтобы открыть для компилятора развертывания, чтобы повторная упаковка могла быть удалена насколько возможно. Например:

f :: T -> Float
f (T f1 f2) = f1 + f2

Компилятор избежит повторной упаковки f1 и f2 путём встраивания + для чисел с плавающей точкой, но только при включенном -O.

Любая однокомпонентная структура данных подходит для распаковки; например

data T = T {-# UNPACK #-} !(Int,Int)

будет хранить две Int непосредственно в конструкторе T, сглаживая пару. Также поддерживается многоуровневая распаковка:

data T = T {-# UNPACK #-} !S
data S = S {-# UNPACK #-} !Int {-# UNPACK #-} !Int

будет хранить две не упакованные Int# непосредственно в конструкторе T.

Также смотрите флаг -funbox-strict-fields, который фактически имеет эффект добавления {-# UNPACK #-} к каждому строгому полю конструктора.

1

На самом деле, UNPACK не имеет эффекта без -O по техническим причинам (см. #5252).

10.32.13. NOUNPACK предикат

{-# NOUNPACK #-}
Где

глобальный уровень

Инструктирует компилятор не распаковывать поле конструктора.

Предикат NOUNPACK указывает компилятору не распаковывать содержимое поля конструктора. Пример:

data T = T {-# NOUNPACK #-} !(Int,Int)

Даже с флагами -funbox-strict-fields и -O, поле конструктора T не распаковывается.

10.32.14. SOURCE предикат

{-# SOURCE #-}
Где

после оператора import

Импортировать модуль, используя файл hs-boot, чтобы разорвать цикл импорта модуля.

Предикат {-# SOURCE #-} используется только в объявлениях import, чтобы разорвать цикл импорта модулей. Подробное описание см. в Как компилировать взаимно рекурсивные модули.

10.32.15. COMPLETE предикаты

{-# COMPLETE #-}
Где

на верхнем уровне

Укажите набор конструкторов или синонимов шаблонов, которые составляют полное соответствие.

Предикат COMPLETE используется для информирования проверяющего шаблонов о том, что определенный набор шаблонов является полным, и что любая функция, которая соответствует всем указанным шаблонам, является полной.

Наиболее распространенное использование предикатов COMPLETE — с Синонимами шаблонов. Сам по себе проверяющий является очень наивным и предполагает, что любое соответствие, включающее синоним шаблона, будет неудачным. В результате любое соответствие шаблонов синониму шаблона считается неполным, если пользователь не добавляет универсальный случай.

Например, типы данных 2 * A и A + A изоморфны, но некоторые вычисления выражены более естественным образом в терминах одного или другого. Чтобы получить лучшее из обоих миров, мы можем выбрать один в качестве реализации, а затем предоставить набор синонимов шаблонов, чтобы пользователи могли использовать другое представление, если они этого захотят. Затем мы можем указать предикат COMPLETE, чтобы проинформировать проверяющий шаблонов о том, что функция, которая соответствует как LeftChoice, так и RightChoice, является полной.

data Choice a = Choice Bool a

pattern LeftChoice :: a -> Choice a
pattern LeftChoice a = Choice False a

pattern RightChoice :: a -> Choice a
pattern RightChoice a = Choice True a

{-# COMPLETE LeftChoice, RightChoice #-}

foo :: Choice Int -> Int
foo (LeftChoice n) = n * 2
foo (RightChoice n) = n - 2

Предикаты COMPLETE используются только проверяющим шаблонов. Если определение функции соответствует всем конструкторам, указанным в предикате, то компилятор не выдаст предупреждения.

Предикаты COMPLETE могут содержать любые конструкторы данных или синонимы шаблонов, которые находятся в области видимости, но должны упоминать как минимум один конструктор данных или синоним шаблона, определенный в том же модуле. Предикаты COMPLETE могут появляться только на верхнем уровне модуля. После определения они автоматически импортируются и экспортируются из модулей. Предикаты COMPLETE следует рассматривать как утверждение универсальной истины о наборе шаблонов и, следовательно, не следует использовать для подавления предупреждений о неполном соответствии в конкретном контексте.

При указании предиката COMPLETE, типы результата всех шаблонов должны быть согласованы друг с другом. Это проверка на корректность, поскольку невозможно соответствовать всем шаблонам, если типы не согласованы.

Тип результата также должен быть однозначным. Обычно это можно вывести, но когда все синонимы шаблонов в группе полиморфны в конструкторе, пользователь должен предоставить подпись типа.

class LL f where
  go :: f a -> ()

instance LL [] where
  go _ = ()

pattern T :: LL f => f a
pattern T <- (go -> ())

{-# COMPLETE T :: [] #-}

-- No warning
foo :: [a] -> Int
foo T = 5

10.32.16. OVERLAPPING, OVERLAPPABLE, OVERLAPS, и INCOHERENT предикаты

{-# OVERLAPPING #-}
{-# OVERLAPPABLE #-}
{-# OVERLAPS #-}
{-# INCOHERENT #-}
Где

в заголовке экземпляра

Предикаты OVERLAPPING, OVERLAPPABLE, OVERLAPS, INCOHERENT используются для указания поведения перекрытия для отдельных экземпляров, как описано в разделе Перекрывающиеся экземпляры. Предикаты записываются сразу после ключевого слова instance, например так:

instance {-# OVERLAPPING #-} C t where ...

10.33. Правила переписывания

{-# RULES "⟨name⟩" forall ⟨binder⟩ ... . ⟨expr⟩ = ⟨expr⟩ ... #-}
Где

на верхнем уровне

Определите правило переписывания, которое будет использоваться для оптимизации исходной программы.

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

{-# RULES
      "map/map"    forall f g xs.  map f (map g xs) = map (f.g) xs
  #-}

Используйте флаг отладки -ddump-simpl-stats, чтобы увидеть, какие правила были применены. Если вам нужна более подробная информация, то -ddump-rule-firings показывает каждый отдельный запуск правила, а -ddump-rule-rewrites также показывает, как код выглядит до и после переписывания.

-fenable-rewrite-rules

Разрешить компилятору применять правила переписывания к исходной программе.

10.33.1. Синтаксис

С точки зрения синтаксиса:

  • В предикате RULES может быть ноль или более правил, разделенных точками с запятой (которые могут быть сгенерированы правилом форматирования).
  • Правило форматирования применяется в предикате. В настоящее время новый уровень отступа не устанавливается, поэтому если вы поместите несколько правил в один предикат RULES и хотите использовать форматирование для их разделения, вы должны выровнять начало на том же столбце, что и окружающие определения.

    {-# RULES
          "map/map"    forall f g xs.  map f (map g xs) = map (f.g) xs
          "map/append" forall f xs ys. map f (xs ++ ys) = map f xs ++ map f ys
      #-}
    

    Кроме того, закрывающая #-} должна начинаться со столбца справа от открывающей {-#.

  • Каждое правило имеет имя, заключенное в двойные кавычки. Само имя не имеет никакого значения. Оно используется только при сообщении о том, сколько раз правило было применено.
  • Правило может необязательно иметь номер фазы управления (см. Управление фазой) сразу после имени правила. Таким образом:

    {-# RULES
          "map/map" [2]  forall f g xs. map f (map g xs) = map (f.g) xs
      #-}
    

    [2] означает, что правило активно в фазе 2 и последующих фазах. Обратный запрос [~2] также принимается, что означает, что правило активно до, но не включая, фазу 2.

    Правила поддерживают специальный синтаксис управления фазой [~], что означает, что правило никогда не активно. Эта функция поддерживает плагины (см. Плагины компилятора), позволяя определить правило, которое никогда не выполняется GHC, но тем не менее анализируется, проверяется по типам и т. д., так что оно доступно плагину.

  • Каждая переменная (терма), упомянутая в правиле, должна либо находиться в области видимости (например, map), либо быть связана с forall (например, f, g, xs). Переменные, связанные с forall называются переменными шаблона. Они разделены пробелами, так же, как и в типе forall.
  • Переменная шаблона может необязательно иметь подпись типа. Если тип переменной шаблона полиморфен, у него должна быть подпись типа. Например, вот правило foldr/build:

    "fold/build"  forall k z (g::forall b. (a->b->b) -> b -> b) .
                  foldr k z (build g) = g k z
    

    Поскольку g имеет полиморфный тип, у него должна быть подпись типа.

  • Если включено расширение ExplicitForAll, переменные типа/вида также могут быть явно связаны. Например:

    {-# RULES "id" forall a. forall (x :: a). id @a x = x #-}
    

    Если присутствует явное forall на уровне типа, каждая переменная типа/вида, упомянутая в нём, должна быть либо в области видимости, либо связана с forall. В частности, в отличие от некоторых других мест в Haskell, это означает, что свободные переменные вида не будут неявно связаны. Например:

    "this_is_bad" forall (c :: k). forall (x :: Proxy c) ...
    "this_is_ok"  forall k (c :: k). forall (x :: Proxy c) ...
    

    Когда нужны связанные переменные типа/вида, оба ключевых слова forall должны всегда включаться, хотя если переменные шаблона не нужны, второе можно оставить пустым. Например:

    {-# RULES "map/id" forall a. forall. map (id @a) = id @[a] #-}
    
  • Левая часть правила должна состоять из переменной верхнего уровня, применяемой к произвольным выражениям. Например, это не подходит:

    "wrong1"   forall e1 e2.  case True of { True -> e1; False -> e2 } = e1
    "wrong2"   forall f.      f True = True
    "wrong3"   forall x.      Just x = Nothing
    

    В "wrong1", левая часть не является применением; в "wrong2", левая часть имеет переменную шаблона в заголовке. В "wrong3", левая часть состоит из конструктора, а не переменной, применяемого к аргументу.

  • Правило не обязательно должно находиться в том же модуле, что и (любые) переменные, которые оно упоминает, хотя, конечно, они должны быть в области видимости.
  • Все правила неявно экспортируются из модуля и, следовательно, действуют в любом модуле, который импортирует модуль, который их определил, прямо или косвенно. (То есть, если A импортирует B, который импортирует C, то правила C действуют при компиляции A.) Ситуация очень похожа на ситуацию с объявлениями экземпляров.
  • Внутри предиката RULES “forall” обрабатывается как ключевое слово, независимо от настроек других флагов. Кроме того, внутри предиката RULES расширение языка ScopedTypeVariables автоматически включено; см. Локально-скопированные переменные типа.
  • Как и другие предикаты, предикаты RULES всегда проверяются на ошибки области видимости и проверяются по типам. Проверка по типам означает, что левая и правая части правила проверяются по типам и должны иметь один и тот же тип. Однако правила включаются только при включенном флаге -fenable-rewrite-rules (см. Семантика).

10.33.2. Семантика

С точки зрения семантики:

  • Правила включены (то есть используются во время оптимизации) флагом -fenable-rewrite-rules. Этот флаг подразумевается флагом -O и может быть выключен (как обычно) флагом -fno-enable-rewrite-rules. (Примечание: включение -fenable-rewrite-rules без -O может привести к неожиданному результату, поскольку без -O GHC игнорирует всю информацию об оптимизации в файлах интерфейса; см. -fignore-interface-pragmas). Обратите внимание, что -fenable-rewrite-rules является флагом оптимизации и не оказывает влияния на разбор или проверку типов.
  • Правила рассматриваются как правила переписывания слева направо. Когда GHC находит выражение, которое является заменой левой части правила, он заменяет выражение правой частью (соответственно заменённой). Под «заменой» подразумевается, что левую часть можно сделать равной выражению, заменив переменные шаблона.
  • GHC совершенно не пытается проверить, что левая и правая части правила имеют одинаковый смысл. Это, вообще говоря, неразрешимая проблема и невыполнимо в большинстве интересных случаев. Ответственность полностью лежит на программисте!
  • GHC не пытается убедиться, что правила являются конfluent или конечными. Например:

    "loop"        forall x y.  f x y = f y x
    

    Это правило заставит компилятор попасть в бесконечный цикл.

  • Если несколько правил соответствуют вызову, GHC произвольно выберет одно для применения.
  • GHC в настоящее время использует очень простой синтаксический алгоритм сопоставления для сопоставления левой части правила с выражением. Он ищет замену, которая делает левую часть и выражение синтаксически равными с учётом альфа-преобразования. Шаблон (правило), но не выражение, расширяется эта-преобразованием, если необходимо. (Расширение эта-преобразованием выражения может привести к ошибкам ленивости.) Но не бета-преобразование (это называется сопоставлением высшего порядка).

    Сопоставление выполняется на промежуточном языке GHC, который включает абстракции типов и приложения. Поэтому правило соответствует только в том случае, если типы также совпадают. См. Специализация ниже.

  • GHC продолжает попытки применить правила по мере оптимизации программы. Например, рассмотрим:

    let s = map f
        t = map g
    in
    s (t xs)
    

    Выражение s (t xs) не соответствует правилу "map/map", но GHC заменит s и t, получив выражение, которое соответствует. Если s или t использовались более одного раза и были большими или редексом, то они не будут заменены, и правило не сработает.

10.33.3. Как правила взаимодействуют с директивами INLINE/NOINLINE

Обычное встраивание происходит одновременно с переписыванием правил, что может привести к неожиданным результатам. Рассмотрим этот (искусственный) пример

f x = x
g y = f y
h z = g True

{-# RULES "f" f True = False #-}

Поскольку правая часть f мала, она встраивается в g, что даёт

g y = y

Теперь g встраивается в h, но RULE f не имеет возможности сработать. Если бы GHC сначала встроил g в h, то было бы больше шансов, что правило f RULES могло бы сработать.

Чтобы получить предсказуемое поведение, используйте директиву NOINLINE или директиву INLINE[⟨phase⟩] для f, чтобы убедиться, что она не встраивается до тех пор, пока её правила RULES не будут иметь возможности сработать. Флаг предупреждений -Winline-rule-shadowing (см. Предупреждения и проверка корректности) предупреждает об этой ситуации.

10.33.4. Как правила взаимодействуют с директивами CONLIKE

GHC очень осторожен в отношении дублирования работы. Например, рассмотрим

f k z xs = let xs = build g
           in ...(foldr k z xs)...sum xs...
{-# RULES "foldr/build" forall k z g. foldr k z (build g) = g k z #-}

Поскольку xs используется дважды, GHC не запускает правило foldr/build. Правильно, потому что вычисление xs может потребовать значительных ресурсов, которые были бы дублированы при запуске правила.

Иногда такой подход излишне осторожен, и нам нужно, чтобы правило запускалось, даже если это приведёт к дублированию редекса. GHC не может определить, когда это хорошая идея, поэтому мы предоставляем директиву CONLIKE для объявления этого, например:

{-# INLINE CONLIKE [1] f #-}
f x = blah

CONLIKE — модификатор для директивы INLINE или NOINLINE. Он указывает, что применение f к одному аргументу (в общем случае, к числу аргументов слева от знака =) должно считаться достаточно дешёвым для дублирования, если такое дублирование запустит правило. (Название «CONLIKE» сокращение от «подобный конструктору», поскольку у конструкторов определённо есть такое свойство.) Директива CONLIKE — модификатор для INLINE/NOINLINE, потому что это действительно имеет смысл только для сопоставления f с левой частью правила, если вы уверены, что f не будет встроен до того, как правило будет иметь шанс запуститься.

10.33.5. Как правила взаимодействуют с методами классов

Не стоит давать правило для метода класса:

class C a where
  op :: a -> a -> a

instance C Bool where
  op x y = ...rhs for op at Bool...

{-# RULES "f" op True y = False #-}

В этом примере op — не обычная функция верхнего уровня; это метод класса. GHC быстро переписывает любые вхождения op-used-at-type-Bool в специализированную функцию, например, opBool, где

opBool :: Bool -> Bool -> Bool
opBool x y = ..rhs for op at Bool...

Поэтому правило никогда не получит возможности запуститься по тем же причинам, что и в Как правила взаимодействуют с директивами INLINE/NOINLINE.

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

instance C Bool where
  op = opBool

opBool :: Bool -> Bool -> Bool
{-# NOINLINE [1] opBool #-}
opBool x y = ..rhs for op at Bool...

{-# RULES "f" opBool True y = False #-}

Если вы хотите правило, которое действительно относится к перегруженному методу класса, единственный способ сделать это — так:

class C a where
  op_c :: a -> a -> a

op :: C a => a -> a -> a
{-# NOINLINE [1] op #-}
op = op_c

{-# RULES "reassociate" op (op x y) z = op x (op y z) #-}

Теперь встраивание op откладывается до тех пор, пока правило не получит возможность запуститься. Недостаток в том, что объявления экземпляров должны определять op_c, но все остальные использования должны происходить через op.

10.33.6. Слияние списков

Механизм RULES используется для реализации слияния (дефорестации) общих функций над списками. Если «хороший потребитель» потребляет промежуточный список, построенный «хорошим производителем», промежуточный список должен быть полностью устранён.

Хорошими производителями являются:

  • Списки-понимания
  • Перечисления Int, Integer и Char (например, ['a'..'z']).
  • Явные списки (например, [True, False])
  • Конструктор cons (например 3:4:[])
  • ++
  • map
  • take, filter
  • iterate, repeat
  • zip, zipWith

Хорошими потребителями являются:

  • Списки-понимания
  • array (по второму аргументу)
  • ++ (по первому аргументу)
  • foldr
  • map
  • take, filter
  • concat
  • unzip, unzip2, unzip3, unzip4
  • zip, zipWith (но только по одному аргументу; если оба — хорошие производители, zip будет сливаться с одним, но не с другим)
  • partition
  • head
  • and, or, any, all
  • sequence_
  • msum

Например, следующее не должно генерировать промежуточных списков:

array (1,10) [(i,i*i) | i <- map (+ 1) [0..9]]

Этот список можно расширить; если есть часто используемые функции из Prelude, которые отсутствуют, сообщите нам.

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

10.33.7. Специализация

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

genericLookup :: Ord a => Table a b   -> a   -> b
intLookup     ::          Table Int b -> Int -> b

где intLookup — это реализация genericLookup, которая работает очень быстро для ключей типа Int. Вы можете попросить GHC использовать intLookup вместо genericLookup всякий раз, когда последний вызывается с типом Table Int b -> Int -> b. Раньше это можно было сделать так

{-# SPECIALIZE genericLookup :: Table Int b -> Int -> b = intLookup #-}

Эта функция больше не поддерживается в GHC, но правила переписывания позволяют сделать то же самое:

{-# RULES "genericLookup/Int" genericLookup = intLookup #-}

Это немного странное правило инструктирует GHC заменить genericLookup на intLookup при совпадении типов. Более того, это правило не обязательно должно быть в том же файле, что и genericLookup, в отличие от директивы SPECIALIZE , которые в настоящее время это делают (чтобы была доступна исходная реализация для специализации).

Вы несёте ответственность за то, чтобы intLookup действительно вела себя как специализированная версия genericLookup!

Пример, в котором использование RULES для специализации даст значительный выигрыш:

toDouble :: Real a => a -> Double
toDouble = fromRational . toRational

{-# RULES "toDouble/Int" toDouble = i2d #-}
i2d (I# i) = D# (int2Double# i) -- uses Glasgow prim-op directly

Функция i2d фактически является одной машинной инструкцией; по сравнению с этим, стандартное преобразование — через промежуточное Rational — неимоверно дорого.

10.33.8. Управление правилами переписывания

  • Используйте -ddump-rules, чтобы увидеть правила, определённые в данном модуле. Это включает правила, сгенерированные на этапе специализации, но исключает правила, импортированные из других модулей.
  • Используйте -ddump-simpl-stats, чтобы увидеть, какие правила применяются. Если вы добавите -dppr-debug, вы получите более подробный список.
  • Используйте -ddump-rule-firings или -ddump-rule-rewrites, чтобы подробно увидеть, какие правила применяются. Если вы добавите -dppr-debug, вы получите ещё более подробный список.
  • Определение (скажем) build в GHC/Base.hs выглядит так:

    build   :: forall a. (forall b. (a -> b -> b) -> b -> b) -> [a]
    {-# INLINE build #-}
    build g = g (:) []
    

    Обратите внимание на INLINE! Это предотвращает встраивание (:) при компиляции PrelBase, чтобы импортирующий модуль «видел» (:), и мог сопоставить его с левой частью правила. INLINE предотвращает любое встраивание в правой части INLINE вещи. Мне жаль, что это так сложно.

  • В libraries/base/GHC/Base.hs посмотрите на правила для map , чтобы узнать, как написать правила, которые будут выполнять слияние, и тем не менее, дадут эффективную программу, даже если слияние не произойдёт. Больше правил в GHC/List.hs.

10.34. Специальные встроенные функции

В GHC есть несколько встроенных функций со специальным поведением. В частности:

  • GHC.Exts.inline позволяет управлять встраиванием на уровне отдельных вызовов.
  • GHC.Exts.lazy ограничивает анализатор строгости.
  • GHC.Exts.oneShot даёт подсказку компилятору о частоте вызова функции.

10.35. Обобщённые классы

В GHC раньше была реализация обобщённых классов, как определено в статье «Derivable type classes», Ralf Hinze и Simon Peyton Jones, Haskell Workshop, Монреаль, сентябрь 2000 г., стр. 94-105. Они были удалены и заменены более общей поддержкой обобщённого программирования.

10.36. Обобщённое программирование

Используя комбинацию DeriveGeneric, DefaultSignatures и DeriveAnyClass, можно легко реализовать программирование, ориентированное на тип данных, используя фреймворк GHC.Generics. В этом разделе представлено очень краткое описание того, как это сделать.

Поддержка обобщённого программирования в GHC позволяет определять классы с методами, для которых не требуется пользовательская спецификация при инстанцировании: тело метода автоматически выводится GHC. Это похоже на то, что происходит для стандартных классов, таких как Read и Show, например, но теперь для пользовательских классов.

10.36.1. Вывод представлений

Сначала нам нужны обобщённые представления. Модуль GHC.Generics определяет несколько примитивных типов, которые используются для представления типов данных Haskell:

-- | Unit: used for constructors without arguments
data U1 p = U1

-- | Constants, additional parameters and recursion of kind Type
newtype K1 i c p = K1 { unK1 :: c }

-- | Meta-information (constructor names, etc.)
newtype M1 i c f p = M1 { unM1 :: f p }

-- | Sums: encode choice between constructors
infixr 5 :+:
data (:+:) f g p = L1 (f p) | R1 (g p)

-- | Products: encode multiple arguments to constructors
infixr 6 :*:
data (:*:) f g p = f p :*: g p

Классы Generic и Generic1 служат посредниками между пользовательскими типами данных и их внутренним представлением как суммой произведений:

class Generic a where
  -- Encode the representation of a user datatype
  type Rep a :: Type -> Type
  -- Convert from the datatype to its representation
  from  :: a -> (Rep a) x
  -- Convert from the representation to the datatype
  to    :: (Rep a) x -> a

class Generic1 (f :: k -> Type) where
  type Rep1 f :: k -> Type

  from1  :: f a -> Rep1 f a
  to1    :: Rep1 f a -> f a

Generic1 используется для функций, которые могут быть определены только над контейнерами типов, такими как map. Обратите внимание, что Generic1 по умолчанию перебирает типы вида Type -> Type, но если включен расширение PolyKinds, то он может перебирать типы вида k -> Type, для любого вида k.

DeriveGeneric
С

7.2.1

Разрешает автоматический вывод инстансов для типа класса Generic.

Инстансы этих классов могут быть выведены GHC с помощью расширения DeriveGeneric и необходимы для автоматического определения обобщённых инстансов.

Например, пользовательский тип данных деревьев

data UserTree a = Node a (UserTree a) (UserTree a) | Leaf

в модуле Main в пакете с именем foo получит следующее представление:

instance Generic (UserTree a) where
  -- Representation type
  type Rep (UserTree a) =
    M1 D ('MetaData "UserTree" "Main" "package-name" 'False) (
          M1 C ('MetaCons "Node" 'PrefixI 'False) (
                M1 S ('MetaSel 'Nothing
                               'NoSourceUnpackedness
                               'NoSourceStrictness
                               'DecidedLazy)
                     (K1 R a)
            :*: M1 S ('MetaSel 'Nothing
                               'NoSourceUnpackedness
                               'NoSourceStrictness
                               'DecidedLazy)
                     (K1 R (UserTree a))
            :*: M1 S ('MetaSel 'Nothing
                               'NoSourceUnpackedness
                               'NoSourceStrictness
                               'DecidedLazy)
                     (K1 R (UserTree a)))
      :+: M1 C ('MetaCons "Leaf" 'PrefixI 'False) U1)

  -- Conversion functions
  from (Node x l r) = M1 (L1 (M1 (M1 (K1 x) :*: M1 (K1 l) :*: M1 (K1 r))))
  from Leaf         = M1 (R1 (M1 U1))
  to (M1 (L1 (M1 (M1 (K1 x) :*: M1 (K1 l) :*: M1 (K1 r))))) = Node x l r
  to (M1 (R1 (M1 U1)))                                      = Leaf

Это представление генерируется автоматически, если к типу данных добавлена фраза deriving Generic. Также можно использовать автоматический вывод.

10.36.2. Написание обобщённых функций

Обобщённая функция определяется путём создания класса и предоставления инстансов для каждого из типов представлений GHC.Generics. В качестве примера покажем обобщённую сериализацию:

data Bin = O | I

class GSerialize f where
  gput :: f a -> [Bin]

instance GSerialize U1 where
  gput U1 = []

instance (GSerialize a, GSerialize b) => GSerialize (a :*: b) where
  gput (x :*: y) = gput x ++ gput y

instance (GSerialize a, GSerialize b) => GSerialize (a :+: b) where
  gput (L1 x) = O : gput x
  gput (R1 x) = I : gput x

instance (GSerialize a) => GSerialize (M1 i c a) where
  gput (M1 x) = gput x

instance (Serialize a) => GSerialize (K1 i a) where
  gput (K1 x) = put x

Ограничение: эта стратегия кодирования может быть ненадёжной в разных версиях GHC. При выводе инстанса Generic может быть выбран любое вложенное сочетание :+: и :*:, которое он выбирает, так что если GHC выберет (a :+: b) :+: c, то кодирование для a будет [O, O], b будет [O, I], а c будет [I]. Однако, если GHC выберет a :+: (b :+: c), то кодирование для a будет [O], b будет [I, O], а c будет [I, I]. (На практике текущая реализация пытается создать более-менее сбалансированное вложение :+: и :*: таким образом, чтобы обход структуры типа данных от корня до конкретного компонента мог быть выполнен за логарифмическое, а не линейное время.)

Как правило, этот класс GSerialize не будет экспортироваться, так как имеет смысл иметь инстансы только для типов представлений.

10.36.3. Неподнятые типы представлений

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

data family URec a p

data instance URec (Ptr ()) p = UAddr   { uAddr#   :: Addr#   }
data instance URec Char     p = UChar   { uChar#   :: Char#   }
data instance URec Double   p = UDouble { uDouble# :: Double# }
data instance URec Int      p = UInt    { uInt#    :: Int#    }
data instance URec Float    p = UFloat  { uFloat#  :: Float#  }
data instance URec Word     p = UWord   { uWord#   :: Word#   }

Для удобства предоставляются шесть синонимов типов:

type UAddr   = URec (Ptr ())
type UChar   = URec Char
type UDouble = URec Double
type UFloat  = URec Float
type UInt    = URec Int
type UWord   = URec Word

Например, эта декларация данных:

data IntHash = IntHash Int#
  deriving Generic

приводит к следующему инстансу Generic:

instance 'Generic' IntHash where
  type 'Rep' IntHash =
    'D1' ('MetaData "IntHash" "Main" "package-name" 'False)
      ('C1' ('MetaCons "IntHash" 'PrefixI 'False)
        ('S1' ('MetaSel 'Nothing
                        'NoSourceUnpackedness
                        'NoSourceStrictness
                        'DecidedLazy)
              'UInt'))

Пользователь может предоставить, например, инстанс GSerialize UInt, чтобы инстанс Serialize IntHash мог быть легко определён в терминах GSerialize.

10.36.4. Обобщённые значения по умолчанию

Теперь всё, что осталось сделать, это определить класс «переднего плана», который доступен пользователю:

class Serialize a where
  put :: a -> [Bin]

  default put :: (Generic a, GSerialize (Rep a)) => a -> [Bin]
  put = gput . from

Здесь мы используем подпись по умолчанию, чтобы указать, что пользователь не должен предоставлять реализацию для put, пока существует инстанс Generic для типа, который подлежит инстанцированию. Для типа UserTree, например, пользователь может просто написать:

instance (Serialize a) => Serialize (UserTree a)

Затем используется метод по умолчанию для put, соответствующий обобщённой реализации сериализации. Если вы используете DeriveAnyClass, тот же инстанс генерируется путём простого добавления фразы deriving Serialize к декларации типа данных UserTree. Более подробные примеры обобщённых функций можно найти в пакете generic-deriving на Hackage.

10.36.5. Дополнительная информация

Дополнительную информацию можно найти на странице Википедии Haskell или в оригинальной статье [Generics2010].

Generics2010

Jose Pedro Magalhaes, Atze Dijkstra, Johan Jeuring, and Andres Loeh. A generic deriving mechanism for Haskell. Proceedings of the third ACM Haskell symposium on Haskell (Haskell’2010), pp. 37-48, ACM, 2010.

10.37. Роли

Используя GeneralizedNewtypeDeriving (Обобщённые производные инстансы для newtype), программист может взять существующие инстансы классов и «поднять» их в инстансы этого класса для newtype. Однако, это не всегда безопасно. Например, рассмотрим следующее:

newtype Age = MkAge { unAge :: Int }

type family Inspect x
type instance Inspect Age = Int
type instance Inspect Int = Bool

class BadIdea a where
  bad :: a -> Inspect a

instance BadIdea Int where
  bad = (> 0)

deriving instance BadIdea Age    -- not allowed!

Если разрешено выведенный инстанс, каков будет тип его метода bad? Похоже, это будет Age -> Inspect Age, что эквивалентно Age -> Int, согласно семейству типов Inspect. Однако, если мы просто адаптируем реализацию из инстанса для Int, реализация для bad даст Bool, и у нас возникают проблемы.

Способ определения таких ситуаций — присвоение ролей переменным типов в типах данных, классах и синонимах типов.

Роли, реализованные в GHC, взяты из упрощенной версии работы, описанной в Generative type abstraction and type-level computation, опубликованной в POPL 2011.

10.37.1. Номинальные, Представительные и Фантомные

Цель системы ролей — отслеживать, когда два типа имеют одинаковое основание. В примере выше, Age и Int имеют одинаковое представление. Однако, соответствующие экземпляры BadIdea не будут иметь одинаковое представление, потому что типы реализаций bad будут разными.

Предположим, что у нас есть два использования конструктора типа, каждый из которых применяется к тем же параметрам, за исключением одного различия. (Например, T Age Bool c и T Int Bool c для некоторого типа T.) Роль параметра типа говорит, что нам нужно знать о двух отличающихся аргументах типа, чтобы понять, что два внешних типа имеют одинаковое представление (в примере, что должно быть истинным для Age и Int для того, чтобы показать, что T Age Bool c имеет такое же представление, как T Int Bool c).

GHC поддерживает три разные роли для параметров типа: номинальные, представительные и фантомные. Если параметр типа имеет номинальную роль, то два отличающихся типа фактически не должны отличаться: они должны быть идентичны (после сокращения семейства типов). Если параметр типа имеет представительную роль, то два типа должны иметь одинаковое представление. (Если роль первого параметра T — представительная, то T Age Bool c и T Int Bool c будут иметь одинаковое представление, потому что Age и Int имеют одинаковое представление.) Если параметр типа имеет фантомную роль, то нам не нужна дополнительная информация.

Вот несколько примеров:

data Simple a = MkSimple a          -- a has role representational

type family F
type instance F Int = Bool
type instance F Age = Char

data Complex a = MkComplex (F a)    -- a has role nominal

data Phant a = MkPhant Bool         -- a has role phantom

Тип Simple имеет свой параметр роли представительный, что, как правило, является наиболее распространённым случаем. Simple Age будет иметь такое же представление, как Simple Int. Тип Complex, с другой стороны, имеет свой параметр с ролью номинальный, потому что Complex Age и Complex Int не совпадают. Наконец, Phant Age и Phant Bool имеют одинаковое представление, даже если Age и Bool не связаны.

10.37.2. Вычисление роли

Какую роль должен иметь данный параметр типа? GHC выполняет вычисление роли для определения правильной роли для каждого параметра. Он начинается с нескольких базовых фактов: (->) имеет два представительных параметра; (~) имеет два номинальных параметра; все параметры семейств типов — номинальные; и все параметры, подобные GADTs, — номинальные. Затем эти факты распространяются на все места, где используются эти типы. По умолчанию, для типов данных и синонимов роль — фантомная; по умолчанию, для классов — номинальная. Таким образом, для типов данных и синонимов любые параметры, не используемые в правой части (или используемые только в других типах во фантомных позициях), будут фантомными. Всякий раз, когда параметр используется в представительной позиции (то есть используется в качестве аргумента типа конструктора, соответствующая переменная которого имеет роль представительную), его роль повышается с фантомной до представительной. Аналогично, когда параметр используется в номинальной позиции, его роль повышается до номинальной. Мы никогда не понижаем роль с номинальной до фантомной или представительной, или с представительной до фантомной. Таким образом, мы вычисляем наиболее общую роль для каждого параметра.

Классы имеют номинальные роли по умолчанию для обеспечения согласованности экземпляров классов. Если C Int хранился бы в типе данных, было бы очень плохо, если бы он каким-то образом изменился на C Age где-то, особенно если другой C Age был объявлен!

Существует один особенно сложный случай, который следует объяснить:

data Tricky a b = MkTricky (a b)

Каковы должны быть роли Tricky? На первый взгляд, казалось бы, что a и b должны иметь роль представительную, поскольку оба используются в правой части и ни один из них не участвует в семействе типов. Однако это было бы неправильно, как показывает следующий пример:

data Nom a = MkNom (F a)   -- type family F from example above

Является ли Tricky Nom Age представительно равным Tricky Nom Int? Нет! Первый хранит Char, а второй — Bool. Решение в том, чтобы потребовать, чтобы все параметры переменных типа имели номинальную роль. Таким образом, GHC вычислит роль представительную для a но роль номинальную для b.

10.37.3. Аннотации роли

RoleAnnotations
Since

7.8.1

Разрешить синтаксис аннотаций роли.

Иногда программист хочет ограничить процесс вычисления. Например, в базовой библиотеке содержится следующее определение:

data Ptr a = Ptr Addr#

Идея в том, что a должно быть представительным параметром, но вычисление роли присваивает ему фантомную. Это имеет определённый смысл: указатель на Int действительно представительно совпадает с указателем на Bool. Но это совсем не то, как мы хотим использовать Ptr!

Поэтому мы хотим иметь возможность сказать

type role Ptr representational
data Ptr a = Ptr Addr#

Объявление type role (активированное с помощью RoleAnnotations) заставляет параметр a иметь роль представительный, а не фантомную. Затем GHC проверяет предоставленные пользователем роли, чтобы убедиться, что они не нарушают никаких обещаний. Например, было бы плохо, если бы пользователь мог сделать роль BadIdea представительной.

В качестве другого примера, мы можем рассмотреть тип Set a, который представляет собой набор данных, упорядоченных по a экземпляру Ord. Хотя в общем случае безопасно считать a представительным параметром, возможно, что у newtype и его базового типа разные порядки, закодированные в их соответствующих экземплярах Ord. Это приведёт к некорректной работе во время выполнения. Таким образом, автор типа данных Set хотел бы, чтобы его параметр имел номинальную роль. Это делается с помощью объявления

type role Set nominal

Аннотации роли также могут быть использованы, если программист хочет написать класс с представительной (или фантомной) ролью. Однако, поскольку класс с неноминальными ролями может быстро привести к несогласованности экземпляров класса, необходимо также указать IncoherentInstances, чтобы разрешить неноминальные роли для классов.

Другое место, где аннотации роли могут быть необходимы, — в файлах hs-boot (Как компилировать взаимно рекурсивные модули), где правые части определений могут быть опущены. Как обычно, типы/классы, объявленные в файле hs-boot должны совпадать с определениями в файле hs, включая роли. По умолчанию, для типов данных роль представительная в файлах hs-boot в соответствии с распространённым случаем использования.

Аннотации роли разрешены для объявлений данных, newtype и классов. Объявление аннотации роли начинается с type role и за ним следует один список ролей для каждого параметра типа. (Этот счётчик параметров включает параметры, неявно указанные сигнатурой типа в объявлении данных или newtype в стиле GADT.) Каждый список ролей — это роль (nominal, representational, или phantom или _. Использование _ означает, что GHC должен вычислить эту роль. Аннотация роли может находиться в любом месте того же модуля, что и определение типа данных или класса (подобно сигнатуре типа на уровне значений). Вот некоторые примеры:

type role T1 _ phantom
data T1 a b = MkT1 a     -- b is not used; annotation is fine but unnecessary

type role T2 _ phantom
data T2 a b = MkT2 b     -- ERROR: b is used and cannot be phantom

type role T3 _ nominal
data T3 a b = MkT3 a     -- OK: nominal is higher than necessary, but safe

type role T4 nominal
data T4 a = MkT4 (a Int) -- OK, but nominal is higher than necessary

type role C representational _   -- OK, with -XIncoherentInstances
class C a b where ...    -- OK, b will get a nominal role

type role X nominal
type X a = ...           -- ERROR: role annotations not allowed for type synonyms

10.38. HasCallStack

GHC.Stack.HasCallStack — лёгкий способ получения частичной стека вызовов в любой точке программы.

Функция может запросить своё место вызова с помощью ограничения HasCallStack и получить доступ к нему как к значению Haskell с помощью callStack.

Затем можно использовать функции из GHC.Stack для проверки или красивого вывода (как показано в f ниже) стека вызовов.

f :: HasCallStack => IO () f = putStrLn (prettyCallStack callStack)

g :: HasCallStack => IO () g = f

Прямое вычисление f показывает стек вызовов с одной записью, в то время как вычисление g, которое также запрашивает свой место вызова, показывает две записи, по одной для каждой вычисления, «помечаемой» HasCallStack.

ghci> f
CallStack (from HasCallStack):
  f, called at <interactive>:19:1 in interactive:Ghci1
ghci> g
CallStack (from HasCallStack):
  f, called at <interactive>:17:5 in main:Main
  g, called at <interactive>:20:1 in interactive:Ghci2

Функция error из Prelude поддерживает вывод стека вызовов, который привёл к ошибке, в дополнение к обычному сообщению об ошибке:

ghci> error "bad"
*** Exception: bad
CallStack (from HasCallStack):
  error, called at <interactive>:25:1 in interactive:Ghci5

Здесь стек вызовов состоит из одной записи, определяющей источник вызова error. Однако, пометив несколько вычислений HasCallStack, определение точных обстоятельств и последовательности вызовов, которые приводят к вызову error становится намного проще, как показано на простом примере ниже.

f :: HasCallStack => IO ()
f = error "bad bad bad"

g :: HasCallStack => IO ()
g = f

h :: HasCallStack => IO ()
h = g
ghci> h
*** Exception: bad bad bad
CallStack (from HasCallStack):
  error, called at call-stack.hs:4:5 in main:Main
  f, called at call-stack.hs:7:5 in main:Main
  g, called at call-stack.hs:10:5 in main:Main
  h, called at <interactive>:28:1 in interactive:Ghci1

CallStack будет расширяться только до тех пор, пока это позволяют типы, например

myHead :: HasCallStack => [a] -> a
myHead []     = errorWithCallStack "empty"
myHead (x:xs) = x

bad :: Int
bad = myHead []
ghci> bad
*** Exception: empty
CallStack (from HasCallStack):
  errorWithCallStack, called at Bad.hs:8:15 in main:Bad
  myHead, called at Bad.hs:12:7 in main:Bad

включает место вызова errorWithCallStack в myHead, и место вызова myHead в bad, но не место вызова bad в интерактивной оболочке GHCi.

GHC решает ограничения HasCallStack в двух шагах:

  1. Если в области видимости есть CallStack (т.е. в окружающем определении есть ограничение HasCallStack), GHC добавит новое место вызова в существующий CallStack.
  2. В противном случае GHC решит ограничение HasCallStack для единственного CallStack содержащего только текущее место вызова.

Важно, что GHC никогда не выведет ограничение HasCallStack, вы должны запросить его явно.

CallStack остается абстрактным, но GHC предоставляет функцию

getCallStack :: CallStack -> [(String, SrcLoc)]

для доступа к отдельным местам вызова в стеке. String — это имя функции, которая была вызвана, а SrcLoc предоставляет имя пакета, модуля и файла, а также номера строки и столбца.

GHC.Stack также экспортирует функцию withFrozenCallStack, которая позволяет пользователям заморозить текущий CallStack, предотвращая любое последующее выполнение операций добавления в стек. Это может использоваться авторами библиотек, чтобы предотвратить, чтобы CallStack не раскрывали ненужные детали реализации. Рассмотрим пример myHead выше, строка errorWithCallStack в выведенном стеке не особенно информативна, поэтому мы можем подавить её, заморозив CallStack, которое мы передаём в errorWithCallStack.

myHead :: HasCallStack => [a] -> a
myHead []     = withFrozenCallStack (errorWithCallStack "empty")
myHead (x:xs) = x
ghci> myHead []
*** Exception: empty
CallStack (from HasCallStack):
  myHead, called at Bad.hs:12:7 in main:Bad

ПРИМЕЧАНИЕ: Внимательный пользователь может заметить, что HasCallStack — это просто псевдоним неявного параметра ?callStack :: CallStack. Это деталь реализации и не должно рассматриваться как часть API CallStack, мы можем изменить реализацию в будущем.

10.38.1. Сравнение со другими источниками трассировок стека

HasCallStack не взаимодействует с RTS и не требует компиляции с -prof. С другой стороны, поскольку CallStack строится явно с помощью ограничений HasCallStack, он, как правило, не будет содержать столько же информации, сколько моделированные стеки вызовов, поддерживаемые RTS.

© 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/8.10.2/docs/html/users_guide/glasgow_exts.html

Spec-Zone.ru

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