Newtype es una declaración de tipo de datos especializada. De tal manera que solo contiene un constructor y un campo.
newtype Foo a = Bar a newtype Id = MkId Word

Preguntas típicas de novato
¿Cuál es la diferencia con los datos del tipo de datos? data Foo a = Bar a data Id = MkId Word
La principal especificidad de
newtype es que consta de las mismas partes que su único campo. Más precisamente, difiere del original en el nivel de tipo, pero tiene la misma representación en la memoria y se calcula estrictamente (no perezosamente).
En resumen, los
newtype son más efectivos debido a su presentación.
Sí, no significa nada para mí ... usaré datosNo, bueno, al final, siempre puede habilitar la extensión
-funpack-strict-fields :) para
campos estrictos (no perezosos) o especificar directamente
data Id = MkId !Word
Aún así, el poder de
newtype no se limita a la eficiencia computacional. ¡Son mucho más fuertes!
3 roles de nuevo tipo

Ocultar implementación
module Data.Id (Id()) where newtype Id = MkId Word
newtype difiere del original, internamente solo
Word .
Pero
ocultamos el constructor
MkId fuera del módulo.
Implementación implementación
{-# LANGUAGE GeneralizedNewtypeDeriving #-} newtype Id = MkId Word deriving (Num, Eq)
Aunque esto no está en el estándar Haskell2010, al expandir la salida de newTypes genéricos, puede inferir automáticamente el comportamiento de
newtype igual que el comportamiento del campo interno. En nuestro caso, el comportamiento de
Eq Id y
Num Id es el mismo que
Eq Word y
Num Word .
Se puede lograr mucho más mediante la expansión de la cría refinada (
DerivingVia ), pero más sobre eso más adelante.
Implementación de elección
A pesar de su propio constructor, en algunos casos puede usar su representación interna.
Desafío
Hay una lista de enteros. Encuentre la cantidad máxima y total en un solo pase en la lista.
Y no use
paquetes de pliegues y
pliegues .
Respuesta tipica
Por supuesto,
doblar ! :)
foldr :: Foldable t => (a -> b -> b) -> b -> ta -> b
Y, la función final se describe así:
aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = foldr (\el (m, s) -> (Just el `max` m, el + s)) (Nothing, 0)
Si observa de cerca, puede ver operaciones similares en ambos lados:
solo el `max` my el + s . En ambos casos, mapeo y operación binaria. Y los elementos vacíos son
Nothing y
0 .
Sí, estos son monoides!
Monoide y Semigroup en más detalleUn semigrupo es una propiedad de una operación binaria asociativa.
x ⋄ (y ⋄ z) == (x ⋄ y) ⋄ z
Un monoide es una propiedad de una operación asociativa (es decir, un semigrupo)
x ⋄ (y ⋄ z) == (x ⋄ y) ⋄ z
que tiene un elemento vacío que no cambia ningún elemento ni a la derecha ni a la izquierda
x ⋄ empty == x == empty ⋄ x
Tanto
max como
(+) son asociativos, ambos tienen elementos vacíos:
Nothing y
0 .
¡Y la combinación de mapeo de monoides junto con la convolución es
plegable !
Plegable más detalladoRecordemos la definición de plegado:
class Foldable t where foldMap :: (Monoid m) => (a -> m) -> ta -> m ...
Apliquemos el comportamiento de plegado a
max y
(+) . No podemos organizar más de una implementación del monoide de
Word . ¡Es hora de aprovechar la implementación de la
nueva opción de tipo!
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
Es necesario hacer un comentario.
El hecho es que para ser un monoide para el tipo de datos
Max a , necesitamos un elemento mínimo, es decir, para que exista un elemento vacío. Entonces, solo un
Max a limitado puede ser un monoide.
Monoide de elemento máximo teóricamente correcto newtype Max a = Max a instance Ord a => Semigroup (Max a) instance Bounded a => Monoid (Max a)
Entonces, de alguna manera, tendremos que convertir nuestro tipo de datos para que aparezca un elemento vacío y podamos usar la coagulación.
¡El elemento conjugado
Quizás convierte un semigrupo en un monoide!
Liberalización de restricciones en versiones recientes de GHCDe vuelta en GHC 8.2, se requería una restricción de tipo monoide
instance Monoid a => Monoid (Maybe a)
lo que significa que necesitábamos otro tipo nuevo:
Y ya es mucho más simple en GHC 8.4, donde solo se necesita un semigrupo en restricción de tipo, e incluso no hay necesidad de crear un tipo de Opción.
instance Semigroup a => Monoid (Maybe a)
Respuesta plegable
Bueno, ahora actualice el código usando la plegabilidad y las flechas.
Recordamos que (.) Es solo una composición funcional:
(.) :: (b -> c) -> (a -> b) -> a -> c f . g = \x -> f (gx)
Y recuerda que
fmap es un functor:
fmap :: Functor f => (a -> b) -> fa -> fb
y su implementación para
Quizás se describe un poco más arriba.
Flecha mas detalladaLas flechas son propiedades de algunas funciones que le permiten trabajar con ellas en un diagrama de bloques.
Se pueden encontrar más detalles aquí:
Flechas: una interfaz general para la computaciónEn nuestro caso, usamos la función Flechas
Eso es
instance Arrow (->)
Utilizaremos las funciones:
(***) :: Arrow a => abc -> ab' c' -> a (b, b') (c, c') (&&&) :: Arrow a => abc -> abc' -> ab (c, c')
Para nuestro caso
abc == (->) bc == b -> c
Y, en consecuencia, la firma de nuestras funciones se reduce a:
(***) :: (b -> c) -> (b' -> c') -> ((b, b') -> (c, c')) (&&&) :: (b -> c) -> (b -> c') -> (b -> (c, c'))
O, en palabras bastante simples, la función
(***) combina dos funciones con un argumento (y un tipo de salida) en una función con el trabajo de un par de argumentos en la entrada y en la salida, respectivamente, un par de tipos de salida.
La función
(&&&) es una versión
simplificada (***) , donde el tipo de los argumentos de entrada de las dos funciones es el mismo, y en la entrada no tenemos un par de argumentos, sino un argumento.
Total, la función unificadora ha adquirido la forma:
import Data.Semigroup import Data.Monoid import Control.Arrow aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = (fmap getMax *** getSum) . (foldMap (Just . Max &&& Sum))
Resultó muy brevemente!
¡Pero todavía es agotador envolver e invertir datos de tipos anidados!
¡Puede reducirlo aún más, y una conversión forzada sin recursos nos ayudará!
Conversión forzada segura y sin recursos y roles de tipo
Hay una función del paquete
Unsafe.Coerce -
unsafeCoerce import Unsafe.Coerce(unsafeCoerce) unsafeCoerce :: a -> b
La función insegura por la fuerza convierte el tipo: de
a a
b .
De hecho, la función es mágica, le dice al compilador que considere los datos de tipo
a como tipo
b , sin tener en cuenta las consecuencias de este paso.
Se puede usar para convertir tipos anidados, pero debe tener mucho cuidado.
En 2014, hubo una revolución con el
nuevo tipo , a saber, una conversión forzada segura y sin recursos.
import Data.Coerce(coerce) coerce :: Coercible ab => a -> b
Esta característica ha abierto una nueva era al trabajar con
newtype .
Coercible Forced Converter funciona con tipos que tienen la misma estructura en la memoria. Parece una clase de tipo, pero de hecho el GHC convierte los tipos en tiempo de compilación y no es posible definir las instancias usted mismo.
La función
Data.Coerce.coerce le permite convertir tipos sin recursos, pero para esto necesitamos tener acceso a constructores de tipos.
Ahora simplifica nuestra función:
import Data.Semigroup import Data.Monoid import Control.Arrow import Data.Coerce aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = coerce . (foldMap (Just . Max &&& Sum))
Evitamos la rutina de extraer tipos anidados; lo hicimos sin desperdiciar recursos con una sola función.
Roles de los tipos de datos anidados
Con la función de
coerción, podemos forzar la conversión de cualquier tipo anidado.
¿Pero es necesario usar esta función tan ampliamente?
Semánticamente, es absurdo convertir a
Ordenado a
Ordenado (Abajo a) .
Sin embargo, puedes probar:
ghci> let h = fromList2Sorted [1,2,3] :: Sorted Int ghci> let hDown = fromList2Sorted $ fmap Down [1,2,3] :: Sorted (Down Int) ghci> minView h Just (Down 1) ghci> minView (coerce h :: Sorted (Down Int)) Just (Down 1) ghci> minView hDown Just (Down 3)
Todo estaría bien, pero la respuesta correcta es
Just (Down 3) .
Es decir, para cortar el comportamiento incorrecto, se introdujeron los roles de tipo.
{-# LANGUAGE RoleAnnotations #-} type role Sorted nominal
Probemos ahora:
ghci> minView (coerce h :: Sorted (Down Int)) error: Couldn't match type 'Int' with 'Down Int' arising from a use of 'coerce'
Significativamente mejor!
En total hay 3 roles (
tipo de rol ):
- representacional - equivalente si la misma representación
- nominal : debe tener exactamente el mismo tipo
- fantasma : independiente del contenido real. Equivalente a cualquier cosa
En la mayoría de los casos, el compilador es lo suficientemente inteligente como para revelar el papel del tipo, pero puede ser ayudado.
Derivación refinada Derivación Comportamiento Via
Gracias a la expansión del lenguaje
DerivingVia , la función de distribución de
newtype ha mejorado.
Comenzando con GHC 8.6, que se lanzó recientemente, apareció esta nueva extensión.
{-# LANGUAGE DerivingVia #-} newtype Id = MkId Word deriving (Semigroup, Monoid) via Max Word
Como puede ver, el comportamiento de tipo se infiere automáticamente debido al refinamiento de la salida.
DerivingVia se puede aplicar a cualquier tipo que admita
Coercible y lo
más importante, ¡completamente sin consumo de recursos!
Aún más,
DerivingVia se puede aplicar no solo a los
tipos nuevos , sino también a cualquier tipo isomórfico si admiten
genéricos genéricos y conversión forzada
coercible .
Conclusiones
Tipos
newtype es una fuerza poderosa que simplifica y mejora enormemente el código, elimina la rutina y reduce el consumo de recursos.
Traducción original :
El gran poder de los nuevos tipos (Hiromi Ishii)PD : Creo que, después de este artículo, publicado hace más de un año [no por mi] artículo, ¡La magia del newtype en Haskell sobre los nuevos Tipos se volverá un poco más clara!