Spec-Zone.ru › Haskell 9

6.20. Прагмы

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

Все прагмы имеют форму {-# word ... #-}, где ⟨word⟩ обозначает тип прагмы, за которым может следовать информация, относящаяся к этому типу прагмы. Регистр букв в ⟨word⟩ не учитывается. Различные значения ⟨word⟩, которые распознаёт GHC, описаны в следующих разделах; любая прагма с нераспознанным значением ⟨word⟩ игнорируется.

Некоторые прагмы являются прагмами заголовка файла:

  • Прагма заголовка файла должна находиться перед ключевым словом module в файле.
  • Можно указать любое количество прагм заголовка файла, перед которыми или после которых могут располагаться комментарии.
  • Прагмы заголовка файла считываются только один раз, до предварительной обработки файла (например, с помощью cpp).
  • Прагмы заголовка файла: {-# LANGUAGE #-}, {-# OPTIONS_GHC #-} и {-# INCLUDE #-}.

6.20.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 сообщит об ошибке, если какие-либо запрошенные расширения не поддерживаются.

6.20.2. Прагма OPTIONS_GHC

{-# OPTIONS_GHC ⟨flags⟩ #-}
Где:

заголовок файла

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

В предыдущих версиях GHC вместо OPTIONS_GHC принималась запись OPTIONS, но теперь она считается устаревшей.

OPTIONS_GHC — это прагма заголовка файла (см. Прагмы).

6.20.3. Прагма INCLUDE

Раньше INCLUDE требовалась для указания заголовочных файлов, которые нужно включить при использовании FFI и компиляции через C. Для GHC она больше не нужна, но принимается (и игнорируется) для совместимости с другими компиляторами.

6.20.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, либо к обоим, если доступны оба. Если доступны оба, сейчас невозможно указать один из них, не затронув другой (ср. фиксации Инфиксные конструкторы типов, классы и переменные типов).

  • Можно добавить предупреждение к экземпляру (включая выведенные экземпляры):

    instance {-# DEPRECATED "Don't use" #-} Show T1 where { .. }
    instance {-# WARNING "Don't use either" #-} Show G1 where { .. }
    
    deriving instance {-# DEPRECATED "to be removed" #-} Eq T2
    deriving instance {-# WARNING "to be removed as well" #-} Eq G2
    

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

    foo = show (MkT1 :: T1) -- warning: uses "instance Show T1"
    
    bar :: forall a. Eq a => a -> Bool
    bar x = x == x
    baz :: T2 -> Bool
    baz = bar -- warning: uses "instance Eq T2"
    quux :: Eq T2 => T2 -> Bool
    quux = bar -- no warning: does not use "instance Eq T2"
    

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

  • Наконец, предупреждение можно прикрепить к полю экспорта — как к обычному экспорту:

    module Wibble (
        {-# DEPRECATED "Do not use this type" #-} T,
        {-# WARNING "This is a hacky function" #-} f
      ) where
      ...
    

    Так и к повторному экспорту импорта из другого модуля:

    module Wibble (
        {-# DEPRECATED "Import this function from A instead" #-} g
      ) where
    import A
    

    Или к повторному экспорту целого модуля:

    module Wibble (
        {-# DEPRECATED "This declaration has been moved to B instead" #-}
          module B
      ) where
    import B
    

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

    Предупреждение о сущности будет выдано, только если все её экспорты объявлены устаревшими:

    module Wibble (
        {-# WARNING "This would not be warned about" #-} g,
        module A
      )
    import A (g)
    

    Если включён флаг :ghc-flag: -Wincomplete-export-warnings, о таких вхождениях выдаются предупреждения.

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

    module Wibble (
        {-# WARNING "This would throw an error" #-} T(T1),
        {-# WARNING "Because the warning messages differ for T" #-} T,
    )
    ...
    

Также обратите внимание: аргументом DEPRECATED и WARNING может быть список строк; в этом случае строки будут выведены на отдельных строках итогового сообщения предупреждения,

{-# DEPRECATED foo, bar ["Don't use these", "Use gar instead"] #-}

Предупреждения и сообщения об устаревании не выдаются при (a) использовании внутри определяющего модуля, (b) определении метода в экземпляре класса, (c) неквалифицированном использовании сущности, импортированной через разные модули, если предупреждение выдано не для всех этих импортов, и (d) использовании в списке экспорта (кроме предупреждений об экспорте). Последнее позволяет избежать ложных предупреждений в библиотеке, где один модуль объединяет и повторно экспортирует экспорты нескольких других модулей.

Прагма WARNING (но не прагма DEPRECATED) может дополнительно указывать категорию предупреждения в виде строкового литерала после ключевого слова in. Это влияет на флаг, используемый для подавления предупреждения. В приведённых выше примерах категория не указана, поэтому применяется категория по умолчанию deprecations; такие предупреждения можно подавить флагом -Wno-deprecations (и его синонимом -Wno-warnings-deprecations).

Если категория указана, предупреждение можно подавить флагом -Wno-x-⟨category⟩; например, предупреждения от следующей прагмы можно подавить с помощью -Wno-x-partial:

{-# WARNING in "x-partial" head "This function is partial..." #-}

Кроме того, предупреждения от всех прагм WARNING и DEPRECATED, независимо от категории, можно подавить флагом -Wno-extended-warnings.

Если устаревшее имя встречается и в пространстве имён значений, и в пространстве имён типов (то есть используется punning), прагмы WARNING и DEPRECATED повлияют на оба варианта:

{-# LANGUAGE PatternSynonyms #-}

data D = MkD
pattern D = MkD
{-# DEPRECATED D "This will deprecate both the type D and the pattern synonym D" #-}

Можно указать пространство имён имени, о котором должно выдаваться предупреждение или которое объявляется устаревшим, с помощью спецификаторов type и data, однако для этой возможности требуется включить ExplicitNamespaces:

{-# LANGUAGE PatternSynonyms #-}

data D = MkD
pattern D = MkD
{-# DEPRECATED data D "This will deprecate only the pattern synonym D" #-}
{-# DEPRECATED type D "This will deprecate only the type D" #-}

6.20.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.

6.20.6. Прагмы INLINE и NOINLINE

Эти прагмы управляют встраиванием определений функций.

6.20.6.1. Прагма INLINE

{-# INLINE ⟨name⟩ #-}
Где:

определение любой функции

Заставляет GHC встроить значение.

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»), и оптимизирует её только способами, не нарушающими эту гарантию. Например, если встраивание явно отложено с помощью управления фазами, GHC применит к inline-RHS оптимизации, выполняемые до активации прагмы INLINE, а обычную RHS будет оптимизировать как обычно. Для функций, видимых извне, в файл интерфейса записывается inline-RHS (а не оптимизированная RHS).

  • К функции INLINE анализ строгости не применяет преобразование worker/wrapper. Вместо этого она будет встроена целиком.

GHC гарантирует, что встраивание не будет продолжаться бесконечно: каждую взаимно рекурсивную группу разрывает один или несколько ограничителей цикла, которые никогда не встраиваются (см. Secrets of the GHC inliner, JFP 12(4) July 2002). GHC старается не выбирать функцию с прагмой INLINE в качестве ограничителя цикла, но если выбора нет, может быть выбрана даже функция INLINE; в этом случае прагма INLINE игнорируется. Например, для саморекурсивной функции ограничителем цикла может быть только сама функция, поэтому прагма INLINE всегда игнорируется.

Прагмы INLINE особенно полезны для функций then/return (или bind/unit) в монаде. Например, в собственном коде монады UniqueSupply в GHC есть:

{-# INLINE thenUs #-}
{-# INLINE returnUs #-}

См. также прагмы NOINLINE (Прагма NOINLINE) и INLINABLE (Прагма INLINABLE).

6.20.6.1.1. Влияние прагмы INLINE в различных местах

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

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

Рассмотрим определение класса с двумя экземплярами:

class C a where
    op1 :: a -> a

    op2 :: [a] -> [a]
    op2 xs = reverse (xs ++ xs)
    {-# INLINE op2 #-}

instance C T1 where
    op1 x = ...blah...

instance C T2 where
    {-# INLINE op1 #-}
    op1 x = ...blah...
    op2 xs = ...blah...

В этом случае op2 для экземпляра T1 получит неявную прагму INLINE. Это происходит потому, что RHS метода по умолчанию используется для op2, которая сохраняет свою прагму INLINE.

В экземпляре T2 op1 получает прагму INLINE и ведёт себя соответственно. Однако op2 для T2 не реализуется методом по умолчанию. Это означает, что прагма в определении класса к этому экземпляру не применяется. Поскольку явно не указана никакая прагма, GHC самостоятельно выберет подходящее поведение встраивания для метода op2 класса T2.

6.20.6.2. Прагма INLINABLE

{-# INLINABLE ⟨name⟩ #-}
Где:

определение любой функции

Предлагает компилятору всегда рассматривать возможность встраивания name.

Прагма {-# INLINABLE f #-} для функции f имеет следующее поведение:

  • Если INLINE означает «пожалуйста, встрои меня», то INLINABLE означает «можешь встроить меня; решай сам». Иными словами, выбор остаётся за GHC, который применяет те же правила, что и для функций без прагм. В отличие от INLINE, решение принимается в месте вызова и поэтому зависит от порога встраивания, уровня оптимизации и других параметров.
  • Как и INLINE, прагма INLINABLE сохраняет копию исходной RHS для встраивания и записывает её в файл интерфейса независимо от размера RHS.
  • Один из способов использовать INLINABLE — вместе со специальной функцией inline (Специальные встроенные функции). Вызов inline f очень настойчиво пытается встроить f. Чтобы гарантировать возможность встраивания f, рекомендуется пометить её определение как INLINABLE с помощью f, чтобы GHC гарантированно предоставил раскрытие независимо от его размера. Кроме того, пометив f как INLINABLE, вы гарантируете, что будет встроена исходная RHS f, а не произвольная оптимизированная версия f, созданная оптимизатором GHC.
  • Прагма INLINABLE также работает с SPECIALISE: если пометить функцию f как INLINABLE, впоследствии в другом модуле можно применить к ней SPECIALISE (см. Прагма SPECIALIZE).
  • В отличие от INLINE, прагму INLINABLE допустимо использовать для рекурсивной функции. Основная причина — возможность впоследствии использовать SPECIALISE

GHC также принимает альтернативную запись INLINEABLE.

6.20.6.3. Прагма NOINLINE

{-# NOINLINE ⟨name⟩ #-}
Где:

определение любой функции

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

Прагма NOINLINE делает именно то, чего от неё можно ожидать: запрещает компилятору встраивать указанную функцию. Обычно это не требуется, если только вы не уделяете особое внимание размеру кода.

NOTINLINE — синоним NOINLINE (NOINLINE указан в Haskell 98 как стандартный способ отключить встраивание, поэтому его следует использовать, если код должен быть переносимым).

6.20.6.4. Модификатор CONLIKE

{-# CONLIKE #-}
Где:

модифицирует прагму INLINE или NOINLINE

Указывает GHC считать значение особенно дешёвым для встраивания.

Прагма INLINE или NOINLINE может иметь модификатор CONLIKE, влияющий (только) на сопоставление в прагмах RULE. См. раздел Взаимодействие правил с прагмами CONLIKE.

6.20.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 (Правила переписывания).

6.20.7. Прагма OPAQUE

{-# OPAQUE ⟨name⟩ #-}
Где:

верхний уровень

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

Прагма OPAQUE — ещё более строгий вариант прагмы NOINLINE. Как и функции, помеченные прагмой NOINLINE, функции, помеченные прагмой OPAQUE, не встраиваются и не специализируются. В отличие от прагмы NOINLINE, функции, помеченные прагмой OPAQUE, не затрагиваются преобразованием Worker/Wrapper. В отличие от NOINLINE, прагма OPAQUE не поддерживает управление фазами.

Фактически каждый вызов функции, помеченной прагмой OPAQUE, остаётся вызовом этой функции, а не некоторого варианта с изменённым именем. Обычно прагма OPAQUE не нужна, если только изменение имён не имеет для вас значения.

6.20.8. Прагма LINE

{-# LINE ⟨lineno⟩ "⟨file⟩" #-}
Где:

в любом месте

Создаётся препроцессорами для передачи номеров строк исходного кода.

Эта прагма аналогична прагме C #line и предназначена главным образом для использования в автоматически сгенерированном коде Haskell. Она позволяет указать номер строки и имя файла исходного кода; например:

{-# LINE 42 "Foo.vhs" #-}

если текущий файл был создан из файла с именем Foo.vhs и эта строка соответствует строке 42 оригинала. GHC скорректирует сообщения об ошибках так, чтобы в них указывались строка и файл, заданные прагмой LINE.

Прагмы LINE, сгенерированные Template Haskell, задают положение файла и строки на время сплайса и ограничены этим сплайсом. Обратите внимание: поскольку сплайсы Template Haskell работают с абстрактным синтаксисом, позиции файлов не увеличиваются автоматически.

6.20.9. Прагма COLUMN

Это аналог прагмы LINE, также предназначенный для использования в автоматически сгенерированном коде Haskell. Она позволяет указать номер столбца исходного кода; например:

foo = do
  {-# COLUMN 42 #-}pure ()
  pure ()

Эта прагма задаёт начало отсчёта всех номеров столбцов сразу после неё со значения 42. Она влияет только на качество диагностических сообщений и не меняет синтаксис самого кода.

6.20.10. Прагма RULES

Прагма RULES позволяет задавать правила переписывания. Она описана в разделе Правила переписывания.

6.20.11. SPECIALIZE pragma

{-# SPECIALIZE ⟨expr⟩ #-}

Попросить GHC создать копию функции с определёнными аргументами; чаще всего — копию перегруженной функции с определёнными аргументами-словарями классов типов.

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

hammeredLookup :: Ord key => [(key, value)] -> key -> value

Если она часто используется со списками с ключами Widget, её можно специализировать одним из следующих способов (второй синтаксис, добавленный в GHC 9.14, дополнительно требует TypeApplications):

{-# SPECIALIZE hammeredLookup :: [(Widget, value)] -> Widget -> value #-}
{-# SPECIALIZE hammeredLookup @Widget #-}

Вместо получения неизвестного словаря Ord key во время выполнения специализированная версия hammeredLookup будет использовать конкретную реализацию Ord Widget, что, вероятно, позволит получить более эффективный код.

  • Прагму SPECIALIZE для функции можно поместить в любое место, где могла бы находиться её сигнатура типа. Кроме того, можно также SPECIALIZE импортированную функцию, если в месте её определения для неё указана прагма INLINABLE (Прагма INLINABLE).
  • Прагма SPECIALIZE приводит к генерации (a) специализированной версии функции и (b) правила переписывания (см. Правила переписывания), которое заменяет вызов неспециализированной функции вызовом специализированной. Кроме того, если для функции f указана прагма SPECIALIZE, GHC автоматически создаст специализации для любых вызываемых функцией f функций с перегрузкой по классу типов, если они находятся в том же модуле, что и прагма SPECIALIZE, или если они INLINABLE; и так далее, транзитивно.
  • Для правила RULE, созданного прагмой SPECIALIZE, можно задать управление фазами (Управление фазами) так же, как и при непосредственном написании правила RULE. Например:

    {-# SPECIALIZE [0] hammeredLookup :: [(Widget, value)] -> Widget -> value #-}
    

    создаёт правило специализации, которое срабатывает только в фазе 0 (последней фазе). Если в прагме SPECIALIZE управление фазами не указано, оно наследуется от прагмы встраивания функции (если она есть). Например:

    foo :: Num a => a -> a
    foo = ...blah...
    {-# NOINLINE [0] foo #-}
    {-# SPECIALIZE foo :: Int -> Int #-}
    

    Прагма NOINLINE указывает GHC не встраивать foo до фазы 0; это свойство наследуется правилом специализации RULE, которое поэтому срабатывает только в фазе 0.

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

  • Прагма SPECIALIZE допустима только в том случае, если выражение типизировано корректно. Например, прагма specialize вида

    {-# SPECIALIZE f :: <type> #-}
    

    допустима, только если определение

    f_spec :: <type>
    f_spec = f
    

    корректно. Ниже приведено несколько примеров (указана только сигнатура типа исходной функции, но не её код):

    f :: Eq a => a -> b -> b
    {-# SPECIALISE f :: Int -> b -> b #-}
    {-# SPECIALISE f @Float #-}
    
    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 с довольно сложной левой частью (попробуйте сами), поэтому оно может срабатывать не очень часто. Если вы используете такой вид специализации, сообщите нам, насколько хорошо он работает.

  • Начиная с GHC 9.14, функцию также можно специализировать для определённых значений аргументов, например:

    fn :: Bool -> Int -> Double
    fn b i = ...
      where
        ... = if b then helper1 else helper2
    {-# SPECIALISE fn True #-}
    {-# SPECIALISE fn False #-}
    

    Будут созданы две копии fn: одна для True, другая для False. Затем они будут оптимизированы так, чтобы напрямую вызывать helper1 или helper2 соответственно, вместо диспетчеризации по b во время выполнения. Вызовы (с литералом True или False) будут переписаны так, чтобы использовать специализированные версии.

6.20.11.1. SPECIALIZE INLINE

{-# SPECIALIZE INLINE ⟨expr⟩ #-}
Где:

верхний уровень

После прагмы 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 специализированной функции.

Предупреждение

Используя SPECIALISE INLINE для обычной рекурсивной функции, можно вызвать зацикливание GHC.

6.20.11.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

Однако иногда таких вызовов нет; в этом случае прагма может оказаться полезной.

6.20.12. SPECIALIZE instance pragma

{-# SPECIALIZE instance ⟨instance head⟩ #-}
Где:

тело экземпляра

Та же идея, но для объявлений экземпляров. Например:

instance (Eq a) => Eq (Foo a) where {
   {-# SPECIALIZE instance Eq (Foo [(Int, Bar)]) #-}
   ... usual stuff ...
 }

Прагма должна находиться внутри части where объявления экземпляра.

6.20.13. UNPACK pragma

{-# 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. Распаковщик также умеет проходить сквозь newtype.

Начиная с версии 9.6.1 можно распаковывать и типы данных с несколькими конструкторами; фактически поле преобразуется в распакованную несумму, содержащую распакованные представления каждого конструктора (см. UnboxedSums).

См. также флаг -funbox-strict-fields, который, по существу, добавляет {-# UNPACK #-} к каждому строгому полю конструктора с единственным конструктором. Однако суммовые типы не будут распаковываться автоматически: для этого нужна явная прагма.

Также обратите внимание, что GHC объединяет соседние поля размером менее слова в слова. Например, рассмотрим следующий случай (на 64-разрядной платформе):

data T = T {-# UNPACK #-} !Word32 {-# UNPACK #-} !Word32

Поскольку Word32 представляется не подъёмным типом Word32# размером 32 бита, конструктор T будет хранить два своих поля Word32 всего в одном 64-разрядном слове.

Обратите внимание, что при объединении для каждого поля добавляются выравнивающие байты, чтобы сохранить естественное выравнивание. Например, на 64-разрядной платформе

data T = T {-# UNPACK #-} !Word32
           {-# UNPACK #-} !Word8
           {-# UNPACK #-} !Word32

для полей T требуются два 64-разрядных слова, поскольку после Word8 необходимы выравнивающие байты, чтобы следующее поле Word64 было выровнено естественным образом:

┌───────────────────────────────────┐
│ Header                            │
├─────────────────┬────────┬────────┤
│ Word32          │ Word8  │ padding│
├─────────────────┼────────┴────────┤
│ Word32          │ padding         │
└─────────────────┴─────────────────┘
[1]

На самом деле UNPACK не действует без -O по техническим причинам (см. #5252).

6.20.14. NOUNPACK pragma

{-# NOUNPACK #-}
Где:

верхний уровень

Указывает компилятору не распаковывать поле конструктора.

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

data T = T {-# NOUNPACK #-} !(Int,Int)

Даже при включённых флагах -funbox-strict-fields и -O поле конструктора T не распаковывается.

6.20.15. SOURCE pragma

{-# SOURCE #-}
Где:

после инструкции import

Импортирует модуль из файла hs-boot, чтобы разорвать цикл модулей.

Прагма {-# SOURCE #-} используется только в объявлениях import для разрыва цикла модулей. Подробное описание приведено в разделе Взаимно рекурсивные модули и файлы hs-boot.

6.20.16. Прагмы 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: после списка конструкторов нужно указать двойное двоеточие ::, а затем конструктор результирующего типа, ограничивающий случаи применения прагмы. GHC сравнит указанный конструктор результирующего типа с конструктором типа в заголовке типа анализируемого выражения, чтобы определить, должна ли к нему применяться прагма COMPLETE.

Это особенно полезно, если указанные конструкторы полиморфны, например:

data Proxy a = Proxy

class IsEmpty a where
  isEmpty :: a -> Bool

class IsCons a where
  type Elt a
  isCons :: a -> Maybe (Elt a, a)

pattern Empty :: IsEmpty a => a
pattern Empty <- (isEmpty -> True)

pattern Cons :: IsCons a => Elt a -> a -> a
pattern Cons x xs <- (isCons -> Just (x,xs))

instance IsEmpty (Proxy a) where
  isEmpty Proxy = True

instance IsEmpty [a] where
  isEmpty = null

instance IsCons [a] where
  type Elt [a] = a
  isCons [] = Nothing
  isCons (x:xs) = Just (x,xs)

{-# COMPLETE Empty :: Proxy #-}
{-# COMPLETE Empty, Cons :: [] #-}

foo :: Proxy a -> Int
foo Empty = 0

bar :: [a] -> Int
bar Empty = 0
bar (Cons _ _) = 1

baz :: [a] -> Int
baz Empty = 0

В этом примере предупреждения не будут выданы для foo и bar, поскольку их сопоставления с образцом покрываются двумя приведёнными выше прагмами COMPLETE; однако для baz будет выдано предупреждение о неполном сопоставлении.

6.20.17. Прагмы OVERLAPPING, OVERLAPPABLE, OVERLAPS и INCOHERENT

{-# OVERLAPPING #-}
{-# OVERLAPPABLE #-}
{-# OVERLAPS #-}
{-# INCOHERENT #-}
Где:

в заголовке экземпляра

Прагмы OVERLAPPING, OVERLAPPABLE, OVERLAPS и INCOHERENT используются для указания поведения перекрытия отдельных экземпляров, как описано в разделе Перекрывающиеся экземпляры. Прагмы записываются сразу после ключевого слова instance, например:

instance {-# OVERLAPPING #-} C t where ...

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

Spec-Zone.ru

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