haskell types language-design type-systems

¿Por qué hay "datos" y "nuevo tipo" en Haskell?



types language-design (4)

De acuerdo con Learn You a Haskell :

En lugar de la palabra clave data, se usa la palabra clave newtype. Ahora ¿por qué es eso? Bueno, para uno, newtype es más rápido. Si usa la palabra clave de datos para ajustar un tipo, hay una sobrecarga para todo ese ajuste y desempaquetado cuando su programa se está ejecutando. Pero si usa newtype, Haskell sabe que solo lo está usando para envolver un tipo existente en un tipo nuevo (de ahí el nombre), porque quiere que sea el mismo internamente pero tenga un tipo diferente. Con eso en mente, Haskell puede deshacerse de la envoltura y desenvolver una vez que resuelve qué valor es de qué tipo.

Entonces, ¿por qué no usar newtype todo el tiempo en lugar de datos entonces? Bueno, cuando crea un tipo nuevo de un tipo existente utilizando la palabra clave newtype, solo puede tener un constructor de valor y ese constructor de valor solo puede tener un campo. Pero con los datos, puede hacer tipos de datos que tienen varios constructores de valores y cada constructor puede tener cero o más campos:

data Profession = Fighter | Archer | Accountant data Race = Human | Elf | Orc | Goblin data PlayerCharacter = PlayerCharacter Race Profession

Al usar newtype, está restringido a un solo constructor con un campo.

Ahora considere el siguiente tipo:

data CoolBool = CoolBool { getCoolBool :: Bool }

Es su tipo de datos algebraicos corrientes que se definió con la palabra clave de datos. Tiene un constructor de valor, que tiene un campo cuyo tipo es Bool. Hagamos una función que coincida con el patrón en CoolBool y devuelve el valor "hola" independientemente de si el Bool dentro de CoolBool era verdadero o falso:

helloMe :: CoolBool -> String helloMe (CoolBool _) = "hello"

En lugar de aplicar esta función a una CoolBool normal, ¡vamos a lanzar una bola curva y aplicarla a indefinido!

ghci> helloMe undefined "*** Exception: Prelude.undefined

¡Ay! ¡Una excepción! Ahora, ¿por qué ocurrió esta excepción? Los tipos definidos con la palabra clave data pueden tener múltiples constructores de valor (aunque CoolBool solo tiene uno). Entonces, para ver si el valor otorgado a nuestra función cumple con el patrón (CoolBool _), Haskell tiene que evaluar el valor lo suficiente como para ver qué valor constructor se usó cuando hicimos el valor. Y cuando tratamos de evaluar un valor indefinido, aunque sea un poco, se lanza una excepción.

En lugar de usar la palabra clave de datos para CoolBool, intentemos usar newtype:

newtype CoolBool = CoolBool { getCoolBool :: Bool }

No tenemos que cambiar nuestra función helloMe, porque la sintaxis de coincidencia de patrones es la misma si utiliza newtype o data para definir su tipo. Hagamos lo mismo aquí y aplique helloMe a un valor indefinido:

ghci> helloMe undefined "hello"

¡Funcionó! Hmmm, ¿por qué es eso? Bueno, como hemos dicho, cuando usamos Newtype, Haskell puede representar internamente los valores del nuevo tipo de la misma manera que los valores originales. No tiene que agregar otra caja alrededor de ellos, solo tiene que ser consciente de que los valores son de diferentes tipos. Y como Haskell sabe que los tipos creados con la palabra clave newtype solo pueden tener un constructor, no tiene que evaluar el valor pasado a la función para asegurarse de que se ajusta al patrón (CoolBool _) porque los tipos de tipo nuevo solo pueden tener uno posible valor constructor y un campo!

Esta diferencia en el comportamiento puede parecer trivial, pero en realidad es bastante importante porque nos ayuda a darnos cuenta de que aunque los tipos definidos con data y newtype se comportan de manera similar desde el punto de vista del programador porque ambos tienen constructores y campos de valor, en realidad son dos mecanismos diferentes . Mientras que los datos se pueden usar para crear sus propios tipos desde cero, newtype es para crear un tipo completamente nuevo a partir de un tipo existente. La coincidencia de patrones en los valores de tipo nuevo no es como sacar algo de una caja (como ocurre con los datos); se trata más bien de realizar una conversión directa de un tipo a otro.

Aquí hay otra fuente. De acuerdo con este artículo de Newtype :

Una declaración newtype crea un nuevo tipo casi de la misma manera que los datos. La sintaxis y el uso de newtypes es prácticamente idéntico al de las declaraciones de datos; de hecho, puede reemplazar la palabra clave newtype con datos y aún así compilar, de hecho, incluso hay buenas posibilidades de que su programa siga funcionando. Sin embargo, lo contrario no es cierto: los datos solo se pueden reemplazar con newtype si el tipo tiene exactamente un constructor con exactamente un campo dentro.

Algunos ejemplos:

newtype Fd = Fd CInt -- data Fd = Fd CInt would also be valid -- newtypes can have deriving clauses just like normal types newtype Identity a = Identity a deriving (Eq, Ord, Read, Show) -- record syntax is still allowed, but only for one field newtype State s a = State { runState :: s -> (s, a) } -- this is *not* allowed: -- newtype Pair a b = Pair { pairFst :: a, pairSnd :: b } -- but this is: data Pair a b = Pair { pairFst :: a, pairSnd :: b } -- and so is this: newtype Pair'' a b = Pair'' (a, b)

¡Suena bastante limitado! Entonces, ¿por qué alguien usa newtype?

La versión corta La restricción a un constructor con un campo significa que el nuevo tipo y el tipo del campo están en correspondencia directa:

State :: (s -> (a, s)) -> State s a runState :: State s a -> (s -> (a, s))

o en términos matemáticos son isomórficos. Esto significa que después de que se comprueba el tipo en tiempo de compilación, en tiempo de ejecución los dos tipos se pueden tratar esencialmente de la misma manera, sin la sobrecarga o la indirección normalmente asociada con un constructor de datos. Por lo tanto, si desea declarar diferentes instancias de clase de tipo para un tipo particular o desea hacer un resumen de tipo, puede envolverlo en un tipo nuevo y se lo considerará distinto al verificador de tipos, pero idéntico en tiempo de ejecución. A continuación, puede utilizar todo tipo de trucos profundos como fantasmas o recursivos sin preocuparse por GHC barajando cubos de bytes sin ninguna razón.

Ver el artículo para los bits desordenados ...

Esta pregunta ya tiene una respuesta aquí:

Parece que una definición de tipo nuevo es solo una definición de data que obedece a algunas restricciones (por ejemplo, solo un constructor), y que debido a estas restricciones el sistema de tiempo de ejecución puede manejar newtype más eficiente. Y el manejo de la coincidencia de patrones para valores indefinidos es ligeramente diferente.

Pero supongamos que Haskell solo conociera definiciones de data , no newtype : ¿no podría el compilador averiguar por sí mismo si una definición de datos dada obedece a estas restricciones y tratarla de manera más eficiente?

Estoy seguro de que me estoy perdiendo algo, debe haber alguna razón más profunda para esto.


La parte superior de mi cabeza; las declaraciones de datos utilizan evaluación diferida en el acceso y almacenamiento de sus "miembros", mientras que newtype no lo hace. Newtype también elimina todas las instancias de tipos anteriores de sus componentes, ocultando efectivamente su implementación; mientras que los datos dejan la implementación abierta.

Tiendo a usar newtype''s cuando evito el código repetitivo en tipos de datos complejos donde no necesariamente necesito acceso a las partes internas cuando las uso. Esto acelera la compilación y la ejecución, y reduce la complejidad del código donde se usa el nuevo tipo.

Cuando leí por primera vez acerca de esto, encontré este capítulo de una introducción suave a Haskell bastante intuitivo.


Tanto newtype como los data un solo constructor introducen un único constructor de valor, pero el constructor de valor introducido por newtype es estricto y el constructor de valor introducido por los data es flojo. Entonces si tienes

data D = D Int newtype N = N Int

Entonces N undefined es equivalente a undefined y causa un error cuando se evalúa. Pero D undefined no es equivalente a undefined , y se puede evaluar siempre y cuando no intente echar un vistazo dentro.

¿No podría el compilador manejar esto por sí mismo?

No, en realidad no, este es un caso en el que, como programador, se puede decidir si el constructor es estricto o flojo. Para entender cuándo y cómo hacer que los constructores sean estrictos o perezosos, debes entender mucho mejor la evaluación perezosa que yo. Me atiendo a la idea en el Informe, a saber, que newtype está ahí para que puedas renombrar un tipo existente, como tener varios tipos de mediciones incompatibles:

newtype Feet = Feet Double newtype Cm = Cm Double

ambos se comportan exactamente como Double en tiempo de ejecución, pero el compilador promete no dejarlos confundir.


Versión simple para gente obsesionada con listas de viñetas (no pude encontrar ninguna, así que tengo que escribirla sola):

data - crea un nuevo tipo algebraico con constructores de valor

  • Puede tener varios constructores de valor
  • Los constructores de valor son flojos
  • Los valores pueden tener varios campos
  • Afecta tanto a la compilación como al tiempo de ejecución, tiene una sobrecarga de tiempo de ejecución
  • El tipo creado es un nuevo tipo distintivo
  • Puede tener sus propias instancias de clase de tipo
  • Cuando coincidan los patrones con los constructores de valores, se evaluará al menos con la forma normal de la cabeza débil (WHNF) *
  • Se usa para crear un nuevo tipo de datos (ejemplo: Dirección {zip :: String, street :: String})

newtype : crea un nuevo tipo de "decoración" con un constructor de valor

  • Puede tener solo un constructor de valor
  • El constructor de valor es estricto
  • El valor puede tener solo un campo
  • Afecta únicamente a la compilación, sin sobrecarga de tiempo de ejecución
  • El tipo creado es un nuevo tipo distintivo
  • Puede tener sus propias instancias de clase de tipo
  • Cuando se compara el patrón con el constructor de valor, CAN no se puede evaluar en absoluto *
  • Se usa para crear un concepto de nivel superior basado en el tipo existente con un conjunto distinto de operaciones admitidas o que no es intercambiable con el tipo original (ejemplo: Medidor, Cm, Pies es doble)

type - crea un nombre alternativo (sinónimo) para un tipo (como typedef en C)

  • Sin constructores de valor
  • Sin campos
  • Afecta únicamente a la compilación, sin sobrecarga de tiempo de ejecución
  • No se crea ningún tipo nuevo (solo un nuevo nombre para el tipo existente)
  • NO puede tener sus propias instancias de clase de tipo
  • Cuando la coincidencia de patrones con el constructor de datos se comporta de la misma manera que el tipo de original
  • Se usa para crear un concepto de nivel superior basado en el tipo existente con el mismo conjunto de operaciones admitidas (ejemplo: String is [Char])

[*] En la pereza de coincidencia de patrones:

data DataBox a = DataBox Int newtype NewtypeBox a = NewtypeBox Int dataMatcher :: DataBox -> String dataMatcher (DataBox _) = "data" newtypeMatcher :: NewtypeBox -> String newtypeMatcher (NewtypeBox _) = "newtype" ghci> dataMatcher undefined "*** Exception: Prelude.undefined ghci> newtypeMatcher undefined “newtype"