Spec-Zone.ru › Haskell 9

6.16. Неупакованные типы и примитивные операции

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

Все эти примитивные типы данных и операции экспортируются модулем GHC.Exts.

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

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

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

6.16.1. Неупакованные типы

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

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

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

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

6.16.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#. См. Динамическая семантика шаблонов bang.

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

UnboxedTuples
Подразумевает:

UnboxedSums

С:

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

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

Для ссылки на конструкторы типов неупакованных кортежей самих по себе, например, если вы хотите прикрепить к ним экземпляры, используйте (# #), (#,#), (#,,#) и т. д. Это отражает синтаксис для упакованных кортежей (), (,), (,,) и т. д.

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

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

UnboxedTuples

С:

8.2.1

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

-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 #) -> ...

Обратите внимание, что пробелы всегда требуются вокруг черт. Например, (# | 1# | | #) — допустимо, но (# | 1# || #) и (#| 1# | | #) — недопустимы.

Конструкторы типов сами могут быть записаны в префиксной форме как (# | #), (# | | #), (# | | | #) и т. д. Частичные применения также должны использовать префиксную форму, т. е. (# | #) Int#. Насыщенные применения могут быть записаны любым способом, поэтому (# | #) Int# Float# эквивалентно (# Int# | Float# #).

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

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

  • Все типы классифицируются как один из этих классов: 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 (т. е. все представлено целым числом).

6.16.5. Неподнятые newtypes

UnliftedNewtypes
С:

8.10.1

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

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

newtype A = MkA Int#

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

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

Механизм Coercible работает с неупакованными newtypes так же, как и с поднятыми типами. В любом из эквивалентных представлений 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 разрешает назначение семейству данных возвращаемого вида 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.

6.16.6. Неподнятые типы данных

UnliftedDatatypes
Подразумевает:

DataKinds, StandaloneKindSignatures

С:

9.2.1

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

GHC реализует расширение UnliftedDatatypes в соответствии со спецификацией в предложении GHC #265. UnliftedDatatypes ослабляет ограничения на то, какие типы результатов допускаются в объявлениях данных. Например, тип

data UList a :: UnliftedType where
  UCons :: a -> UList a -> UList a
  UNil :: UList a

определяет тип списка, который находится в виде UnliftedType (например, TYPE (BoxedRep Unlifted)). Таким образом, каждый экземпляр такого типа предполагается вычисляемым (и компилятор гарантирует, что это так). Другими словами: неподнятые типы данных ведут себя как типы данных в строгих языках, таких как OCaml или Idris. Однако, в отличие от StrictData, это расширение не изменит, являются ли поля (возможно, неподнятого) типа данных строгими или ленивыми. Например, UCons ленивый по своему первому аргументу, поскольку его поле имеет вид Type.

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

Вышеприведенное объявление данных в синтаксисе GADTs правильно предполагает, что неподнятые типы данных совместимы с полным набором функций GADTs. Скорее наоборот, вы также можете объявить неподнятые типы данных в синтаксисе Haskell98, что требует указания типа результата с помощью StandaloneKindSignatures:

type UList :: Type -> UnliftedType
data UList a = UCons a (UList a) | UNil

Вы даже можете объявить левительно-полиморфные типы данных:

type PEither :: Type -> Type -> TYPE (BoxedRep l)
data PEither l r = PLeft l | PRight r

f :: PEither @Unlifted Int Bool -> Bool
f (PRight b) = b
f _          = False

Хотя f выше мог бы быть левительно-полиморфным (поскольку он вычисляет свой аргумент в любом случае), GHC в настоящее время не допускает более общего типа PEither @l Int Bool -> Bool. Это следствие ограничения на ограничения связывания полиморфизма представления.

Сопоставление с образцом с неподнятым типом данных работает так же, как и с поднятыми типами; но см. Динамическая семантика шаблонов bang для семантики связываний с образцом, включающих неподнятые типы данных.

Из-за #19487, в настоящее время невозможно объявлять левительно-полиморфные типы данных с нулевыми конструкторами данных. Однако существует обходной путь:

type T :: TYPE (BoxedRep l)
data T where
  MkT :: forall l. (() :: Constraint) => T @l

Использование => делает тип MkT поднятым. Если вам нужен альтернативный вариант без затрат на выполнение, используйте MkT :: Proxy# () -> T @l вместо него и смиритесь с дополнительным аргументом proxy# в местах создания.

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

data family F a :: UnliftedType
data instance F Int = FInt

data family G a :: TYPE (BoxedRep l)
data instance G Int = GInt Int -- defaults to Type
data instance G Bool :: UnliftedType where
  GBool :: Bool -> G Bool
data instance G Char :: Type where
  GChar :: Char -> G Char
data instance G Double :: forall l. TYPE (BoxedRep l) where
  GDouble :: Int -> G @l Double

Стоит отметить, что UnliftedDatatypes не обязан предоставлять семействам данных самим типы результатов, включающие TYPE, такие как пример G выше. Расширение требуется только для объявлений data instance, таких как FInt и GBool выше.

6.16.7. Примитивные строковые литералы

Примитивные строковые литералы, также называемые неподнятыми строковыми литералами или Addr# литералами, — это строковые литералы с постфиксом решетки #. Они включаются с помощью расширения MagicHash и имеют тип Addr#.

Примитивный строковый литерал представляет собой последовательность байтов. Вы можете встроить байт NULL, как в "foo\0bar"#, что соответствует байтам [102,111,111,0,98,97,114] :: [Word8] (с неявным NULL в конце). Кроме того, вы можете встроить только символ, код которого меньше 256; "\xFF"# допустимо и представляет собой [255] :: [Word8], но "\x3B1"# недопустимо. Другими словами, он закодирован в кодировке Latin-1.

Как и в C-строках, примитивные строковые литералы неявно завершаются байтом NULL.

Примитивные строковые литералы неизменяемы, и попытка записи по их адресу приводит к неопределенному поведению.

6.16.8. Раскрытие строковых литералов

Строковый литерал ASCII без байта NULL раскрывается в вызов GHC.CString.unpackCString# с содержимым как примитивным строковым литералом. Например, "some string" преобразуется в GHC.CString.unpackCString# "some string"#.

Если строка содержит байты NULL или не-ASCII символы (символы с кодами точек >= 128), строка кодируется в кодировке Modified UTF-8, а затем передается в GHC.CString.unpackCStringUtf8#. Modified UTF-8 отличается от стандартного UTF-8 двумя способами:

  • Байт NULL кодируется как "\xC0\x80"# (т. е. чрезмерное кодирование U+0000).
  • Допускаются суррогатные коды точек, и они кодируются как другие коды точек.

Примеры:

  • "Hello\0world!" раскрывается в GHC.CString.unpackCStringUtf8# "Hello\xC0\x80world!"#.
  • "café" раскрывается в GHC.CString.unpackCStringUtf8# "caf\xC3\xA9"#.
  • "\x732B\x1F431" раскрывается в GHC.CString.unpackCStringUtf8# "\xE7\x8C\xAB\xF0\x9F\x90\xB1"#.
  • "\xDFFF\xD800" раскрывается в GHC.CString.unpackCStringUtf8# "\xED\xBF\xBF\xED\xA0\x80"#.

© 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/primitives.html

Spec-Zone.ru

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