Spec-Zone.ru › Haskell 9

6.20. Прагмы

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

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

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

  • Прагма заголовка файла должна предшествовать ключевому слову 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 вместо OPTIONS_GHC, но это теперь устаревшая форма.

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.

Когда устаревшее имя появляется как в пространстве имён значений, так и в пространстве имён типов (т.е. происходит омонимия) прагмы 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 pragma

{-# MINIMAL ⟨name⟩ | ⟨name⟩ , ... #-}
Где:

в теле класса

Определяет методы, необходимые для минимально полного экземпляра класса.

Pragma MINIMAL используется для указания минимального полного определения класса, т.е. для указания, какие методы должны быть реализованы всеми экземплярами. Если экземпляр не удовлетворяет минимальному полному определению, то генерируется предупреждение. Это может быть полезно, когда у класса есть методы с циклическими значениями по умолчанию. Например

class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool
    x == y = not (x /= y)
    x /= y = not (x == y)
    {-# MINIMAL (==) | (/=) #-}

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

Синтаксис для минимального полного определения:

mindef ::= name
        |  '(' mindef ')'
        |  mindef '|' mindef
        |  mindef ',' mindef

Вертикальная черта обозначает дизъюнкцию, т.е. один из двух вариантов обязателен. Запятая обозначает конъюнкцию, т.е. оба варианта обязательны. Конъюнкция имеет более высокий приоритет, чем дизъюнкция.

Если в объявлении класса нет pragma MINIMAL , то это то же самое, что и pragma {-# 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»), которое он оставляет без изменений, одновременно оптимизируя обычную правую часть (RHS) так же, как обычно. Для функций, видимых извне, inline-RHS (а не оптимизированная RHS) записывается в файл интерфейса.

  • Функция с директивой INLINE не является функцией-обёрткой для анализа строгости. Она будет встроена целиком вместо этого.

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

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

{-# 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 определит надлежащее поведение встраивания для T2 метод op2 самостоятельно.

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 может быть встроена, рекомендуется пометить определение f как INLINABLE, чтобы GHC гарантировал раскрытие, независимо от его размера. Более того, пометив f как INLINABLE, вы обеспечиваете встраивание исходной правой части (RHS) f, а не любой случайной оптимизированной версии f , которую выдал оптимизатор GHC.
  • Директива INLINABLE также работает с SPECIALISE: если вы помечаете функцию f как INLINABLE, то вы можете затем SPECIALISE в другом модуле (см. директиву SPECIALIZE).
  • В отличие от INLINE, использование директивы INLINABLE для рекурсивной функции допустимо. Основная причина сделать это — разрешить последующее использование SPECIALISE

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

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 ⟨name⟩ :: ⟨type⟩ #-}

Запросить, чтобы GHC специализировал полиморфное значение на определённый тип.

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

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

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

{-# SPECIALIZE hammeredLookup :: [(Widget, value)] -> Widget -> value #-}
  • Pragma SPECIALIZE для функции может быть помещено в любом месте, где могла бы стоять её сигнатура типа. Более того, вы также можете SPECIALIZE импортированную функцию, при условии, что ей была задана pragma INLINABLE в месте её определения (Pragma INLINABLE).
  • Pragma SPECIALIZE имеет эффект генерации (а) специализированной версии функции и (б) правила переписывания (см. Правила переписывания), которое переписывает вызов неспециализированной функции в вызов специализированной. Кроме того, если для функции f задана pragma SPECIALIZE, GHC автоматически создаст специализации для всех функций, перегруженных с помощью класса типов, вызываемых f, если они находятся в том же модуле, что и pragma SPECIALIZE, или если они INLINABLE; и так далее, транзитивно.
  • Вы можете добавить управление фазами (Управление фазами) к правилу RULE, сгенерированному pragma SPECIALIZE, так же, как если бы вы написали pragma RULE непосредственно. Например:

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

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

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

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

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

  • Тип в pragma SPECIALIZE может быть любым типом, который менее полиморфен, чем тип исходной функции. В конкретных терминах, если исходная функция — f, то pragma

    {-# 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 с несколько сложной левой частью (попробуйте сами), поэтому оно может не сработать должным образом. Если вы используете этот тип специализации, сообщите нам, насколько хорошо он работает.

6.20.11.1. SPECIALIZE INLINE

{-# SPECIALIZE INLINE ⟨name⟩ :: ⟨type⟩ #-}
Where:

верхнего уровня

Pragma SPECIALIZE может быть дополнительно сопровождаема pragmas INLINE или NOINLINE, дополнительно сопровождаемыми фазой, как описано в INLINE и NOINLINE pragmas. Pragma 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. Оба этих вызова задействуют первую специализацию, тело которой также встроено. В результате получается основанное на типах развертывание индексирующей функции.

Вы можете добавить явное управление фазами (Управление фазами) к pragma SPECIALISE INLINE, как и в pragma INLINE; если вы сделаете это, в правиле переписывания и управлении INLINE специализированной функции будет использоваться та же фаза.

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

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

6.20.11.2. SPECIALIZE для импортированных функций

В целом, вы можете задать pragma SPECIALIZE только для функции, определённой в том же модуле. Однако, если функции f задана pragma INLINABLE в месте её определения, то её можно впоследствии специализировать, импортируя модули (см. Pragma 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. Вам не нужно помещать pragma SPECIALIZE в каждый модуль.

Более того, часто вам даже не нужна pragma 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

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

6.20.12. SPECIALIZE instance pragma

{-# SPECIALIZE instance ⟨instance head⟩ #-}
Where:

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

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

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

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

6.20.13. 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. Распаковщик также может видеть newtypes.

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

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

[1]

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

6.20.14. NOUNPACK директива

{-# NOUNPACK #-}
Где:

на верхнем уровне

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

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

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

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

6.20.15. SOURCE директива

{-# 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 сравнит анотированный конструктор типа результата с конструктором типа в заголовке типа scrutinee в сопоставлении шаблонов, чтобы определить, предназначена ли директива 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.12.1/docs/users_guide/exts/pragmas.html

Spec-Zone.ru

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