GHC se basa en una serie de operaciones y tipos de datos primitivos; “Primitivos” en el sentido de que no pueden definirse en Haskell mismo. Si bien realmente puede usar estas cosas para escribir código rápido, generalmente nos resulta mucho menos doloroso y más satisfactorio a largo plazo usar funciones y bibliotecas de lenguaje de nivel superior. Con un poco de suerte, el código que escriba se optimizará a la versión eficiente sin caja en cualquier caso. Y si no es así, nos gustaría saberlo.

Todos estos tipos de datos primitivos y operaciones son exportados por la biblioteca. GHC.Prim. (Esta documentación se genera a partir del archivo compiler/GHC/Builtin/primops.txt.pp.)

Si desea mencionar cualquiera de los tipos de datos primitivos u operaciones en su programa, primero debe importar GHC.Prim para llevarlos al alcance. Muchos de ellos tienen nombres que terminan en #, y para mencionar tales nombres necesita la MagicHash extensión.

Los primops hacen un uso extensivo de tipos sin caja y tuplas sin caja, que resumimos brevemente aquí.

6.16.1. Tipos sin caja

La mayoría de los tipos en GHC están encuadrados, lo que significa que los valores de ese tipo están representados por un puntero a un objeto de montón. La representación de un Haskell Int, por ejemplo, es un objeto de montón de dos palabras. Sin embargo, un tipo sin caja está representado por el valor en sí mismo, no se incluyen punteros ni asignación de montón.

Los tipos sin caja corresponden a los tipos de “máquina sin procesar” que usaría en C: Int# (int largo), Double# (doble), Addr# (void *), etc. operaciones primitivas (PrimOps) en estos tipos es lo que podría esperar; p.ej, (+#) es una adición en Int#s, y es la adición de la máquina que todos conocemos y amamos, generalmente una instrucción.

Los tipos primitivos (sin caja) no se pueden definir en Haskell y, por lo tanto, están integrados en el lenguaje y el compilador. Los tipos primitivos siempre están sin levantar; es decir, un valor de tipo primitivo no puede ser inferior. (Nota: un tipo “en caja” significa que un valor está representado por un puntero a un objeto de montón; un tipo “levantado” significa que los términos de ese tipo pueden estar en la parte inferior. Consulte el siguiente párrafo para ver un ejemplo). Usamos la convención (pero es solo una convención) que los tipos, valores y operaciones primitivas tienen un # sufijo (ver El hachís mágico). Para algunos tipos primitivos tenemos una sintaxis especial para literales, también descrita en la misma sección.

Los valores primitivos a menudo se representan mediante un patrón de bits simple, como Int#, Float#, Double#. Pero este no es necesariamente el caso: un valor primitivo podría estar representado por un puntero a un objeto asignado al montón. Ejemplos incluyen Array#, el tipo de matrices primitivas. Por lo tanto, Array# es un tipo en caja sin levantar. Un primitivo array se asigna en montón porque es un valor demasiado grande para caber en un registro y sería demasiado caro copiarlo; en cierto sentido, es accidental que esté representado por un puntero. Si un puntero representa un valor primitivo, entonces realmente apunta a ese valor: sin procesadores no evaluados, sin indirecciones. No puede haber nada en el otro extremo del puntero que el valor primitivo. Un programa numéricamente intensivo que utiliza tipos sin caja puede ser un lote más rápido que su contraparte “estándar”; vimos una aceleración tres veces mayor en un ejemplo.

6.16.2. Tipos de tipo sin caja

Debido a que los tipos sin caja se representan sin el uso de punteros, no podemos almacenarlos en un tipo de datos polimórfico. Por ejemplo, el Just nodo de Just 42# tendría que ser diferente de la Just nodo de Just 42; el primero almacena un número entero directamente, mientras que el segundo almacena un puntero. Actualmente, GHC no admite esta variedad de Just nodos (ni para ningún otro tipo de datos). En consecuencia, el amable de un tipo sin caja es diferente del tipo de un tipo en caja.

El Informe Haskell describe que * (espelta Type e importado de Data.Kind en el dialecto GHC de Haskell) es el tipo de tipos de datos ordinarios, como Int. Además, los constructores de tipos pueden tener tipos con flechas; por ejemplo, Maybe tiene amabilidad Type -> Type. Los tipos sin caja tienen un tipo que especifica su representación en tiempo de ejecución. Por ejemplo, el tipo Int# tiene amabilidad TYPE 'IntRep y Double# tiene amabilidad TYPE 'DoubleRep. Estos tipos dicen que la representación en tiempo de ejecución de un Int# es un entero de máquina, y la representación en tiempo de ejecución de un Double# es una máquina de punto flotante de doble precisión. En contraste, el tipo Type en realidad es solo un sinónimo de TYPE
'LiftedRep
. Más detalles del TYPE Los mecanismos aparecen en la sección sobre el polimorfismo de representación en tiempo de ejecución.

Dado que Int#el tipo no es Type, entonces se sigue que Maybe
Int#
no está permitido. De manera similar, debido a que las variables de tipo tienden a ser de tipo Type (por ejemplo, en (.) :: (b -> c) -> (a -> b) -> a -> c, todas las variables de tipo tienen tipo Type), el polimorfismo tiende a no funcionar sobre tipos primitivos. Dando un paso atrás, esto tiene cierto sentido, porque una función polimórfica necesita manipular los punteros a sus datos, y la mayoría de los tipos primitivos no están incluidos en la caja.

Existen algunas restricciones sobre el uso de tipos primitivos:

  • No puede definir un nuevo tipo cuyo tipo de representación (el tipo de argumento del constructor de datos) sea un tipo sin caja. Por lo tanto, esto es ilegal:

    newtype A = MkA Int#
    

    Sin embargo, esta restricción se puede relajar habilitando UnliftedNewtypes. La sección sobre nuevos tipos sin levantar detalla el comportamiento de dichos tipos.

  • No puede vincular una variable con un tipo sin caja en un nivel superior vinculante.
  • No puede vincular una variable con un tipo sin caja en un recursivo vinculante.
  • Puede vincular variables sin caja en una vinculación de patrón (no recursiva, no de nivel superior), pero debe hacer que cualquier coincidencia de patrón sea estricta. (De no hacerlo, se emite una advertencia -Wunbanged-strict-patterns.) Por ejemplo, en lugar de:

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

    debes escribir:

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

    ya que b tiene tipo Int#.

6.16.3. Tuplas sin caja

UnboxedTuples
Ya que: 6.8.1

Las tuplas sin caja no son realmente exportadas por GHC.Exts; son una extensión sintácticaUnboxedTuples). Una tupla sin caja se ve así:

(# e_1, ..., e_n #)

dónde e_1..e_n son expresiones de cualquier tipo (primitivas o no primitivas). El tipo de tupla sin caja tiene el mismo aspecto.

Tenga en cuenta que cuando se habilitan las tuplas sin caja, (# es un solo lexema, por ejemplo, cuando se utilizan operadores como # y #- necesitas escribir ( # ) y ( #- ) en vez de (#) y (#-).

Las tuplas sin caja se utilizan para funciones que necesitan devolver varios valores, pero evitan la asignación de montón normalmente asociada con el uso de tuplas completas. Cuando se devuelve una tupla sin caja, los componentes se colocan directamente en los registros o en la pila; la tupla sin caja en sí no tiene una representación compuesta. Muchas de las operaciones primitivas enumeradas en primops.txt.pp devolver tuplas sin caja. En particular, el IO y ST Las mónadas utilizan tuplas sin caja para evitar asignaciones innecesarias durante las secuencias de operaciones.

Existen algunas restricciones sobre el uso de tuplas sin caja:

  • El uso típico de tuplas sin caja es simplemente devolver múltiples valores, uniendo esos múltiples resultados con un case expresión, así:

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

    Puede tener una tupla sin caja en un enlace de patrón, por lo tanto

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

    Si los tipos de p y q no están sin caja, la unión resultante es perezosa como cualquier otra unión de patrón de Haskell. El ejemplo anterior desugars así:

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

    De hecho, las vinculaciones pueden incluso ser recursivas.

6.16.4. Sumas sin caja

UnboxedSums
Ya que: 8.2.1

Habilite el uso de sintaxis de suma sin caja.

-XUnboxedSums habilita una nueva sintaxis para tipos de suma anónimos y sin caja. La sintaxis para un tipo de suma sin caja con N alternativas es

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

dónde t_1t_N son tipos (que pueden estar sin levantar, incluidas las sumas y tuplas sin caja).

Las tuplas sin caja se pueden utilizar para alternativas multiarity. Por ejemplo:

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

La sintaxis de nivel de término es similar. Barras iniciales y anteriores (|) indicar qué alternativa es. Aquí hay dos términos del tipo que se muestra arriba:

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

(# | True #) -- second alternative

La sintaxis del patrón refleja la sintaxis del término:

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

Las sumas sin caja son “sin caja” en el sentido de que, en lugar de asignar sumas en el montón y representar valores como punteros, las sumas sin caja se representan como sus componentes, al igual que las tuplas sin caja. Estos “componentes” dependen de alternativas de tipo suma. Al igual que las tuplas sin caja, las sumas sin caja son perezosas en sus componentes elevados.

El generador de código intenta generar un diseño lo más compacto posible para cada suma sin caja. En el mejor de los casos, el tamaño de una suma sin caja es el tamaño de su alternativa más grande más una palabra (para una etiqueta). El algoritmo para generar el diseño de memoria para un tipo de suma funciona así:

  • Todos los tipos se clasifican en una de estas clases: palabra de 32 bits, palabra de 64 bits, flotante de 32 bits, flotante de 64 bits, puntero.
  • Para cada alternativa del tipo de suma, se genera un diseño que consta de estos campos. Por ejemplo, si una alternativa tiene Int, Float# y String campos, el diseño tendrá una palabra de 32 bits, campos flotantes y de puntero de 32 bits.
  • Luego, los campos de diseño se superponen para que el diseño final sea lo más compacto posible. Por ejemplo, supongamos que tenemos la suma sin caja:

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

    El diseño final será algo así como

    Int32, Float32, Float32, Word32, Pointer
    

    El primero Int32 es para la etiqueta. Hay dos Float32 campos porque los tipos de punto flotante no pueden superponerse con otros tipos, debido a las limitaciones del generador de código que esperamos superar en el futuro. La segunda alternativa necesita dos Float32 campos: El Word32 el campo es para el Word32# en la primera alternativa. los Pointer el campo es compartido entre String y Maybe Int valores de las alternativas.

    Como otro ejemplo, este es el diseño de la versión sin caja de Maybe a escribe, (# (# #) | a #):

    Int32, Pointer
    

    los Pointer el campo no se usa cuando la etiqueta dice que es Nothing. De lo contrario Pointer apunta al valor en Just. Como se mencionó anteriormente, este tipo es vago en su campo elevado. Por lo tanto, el tipo

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

    es precisamente isomorfo al tipo Maybe a, aunque su recuerdo la representación es diferente.

    En el caso degenerado donde todas las alternativas tienen ancho cero, como el Bool-igual que (# (# #) | (# #) #), el diseño de suma sin caja solo tiene un Int32 campo de etiqueta (es decir, todo está representado por un número entero).

6.16.5. Nuevos tipos sin levantar

UnliftedNewtypes
Ya que: 8.10.1

Habilite el uso de nuevos tipos sobre tipos con representaciones de tiempo de ejecución no levantadas.

GHC implementa un UnliftedNewtypes extensión como se especifica en esta propuesta de GHC. UnliftedNewtypes relaja las restricciones sobre qué tipos pueden aparecer dentro de un newtype. Por ejemplo, el tipo

newtype A = MkA Int#

se acepta cuando esta extensión está habilitada. Esto crea un tipo A :: TYPE 'IntRep y un constructor de datos MkA :: Int# -> A. Aunque el tipo de A es inferido por GHC, no hay nada visualmente distintivo en este tipo que indique que no es del tipo Type como suelen ser los nuevos tipos. GADTSyntax se puede utilizar para proporcionar una firma amable para mayor claridad

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

los Coercible La maquinaria funciona con nuevos tipos sin levantar al igual que con los tipos levantados. En cualquiera de las formulaciones equivalentes de A dado anteriormente, los usuarios también tendrían acceso a una coerción entre A y Int#.

Como consecuencia de la restricción de enlace polimórfico de levedad, los campos polimórficos de levedad no están permitidos en los constructores de datos de tipos de datos declarados mediante data. Sin embargo, desde newtype La aplicación del constructor de datos se implementa como una coerción en lugar de como una aplicación de función, esta restricción no se aplica al campo dentro de un newtype constructor de datos. Por lo tanto, el verificador de tipos acepta

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

Y con UnboxedSums habilitado

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

Esta extensión también relaja algunas de las restricciones en torno a las instancias de familias de datos. En particular, UnliftedNewtypes permite un newtype instance para recibir una especie de devolución TYPE r, No solo Type. Por ejemplo, lo siguiente newtype instance se permitirían declaraciones:

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#

Cabe resaltar que UnliftedNewtypes es no necesario para dar a las familias de datos tipos de retorno que implican TYPE, tales como el FooKey y BarType ejemplos anteriores. La extensión solo es necesaria para newtype instance declaraciones, como FooKeyBoolC y BarTypeWorkRepC encima.

Esta extensión afecta la determinación de si un nuevo tipo tiene o no una firma completa especificada por el usuario (CUSK). El impacto exacto se especifica en el apartado de CUSK.