Novedades de gran poder

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é datos
No, 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 {- -- instance Foldable [] foldr :: (a -> b -> b) -> b -> [a] -> 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) {- ghci> aggregate [1, 2, 3, 4] (Just 4, 10) -} 

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 detalle
Un 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 detallado
Recordemos 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 #-} -- already in Data.Semigroup & Data.Monoid newtype Sum a = Sum {getSum :: a} deriving (Num, Eq, Ord) instance (Num a, Ord a) => Semigroup (Sum a) where (<>) = (+) instance (Num a, Ord a) => Monoid (Sum a) where mempty = Sum 0 newtype Max a = Max {getMax :: a} deriving (Num, Eq, Ord) instance (Num a, Ord a) => Semigroup (Max a) where (<>) = max 

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.

 -- already in Prelude data Maybe a = Nothing | Just a instance Semigroup a => Semigroup (Maybe a) where Nothing <> b = b b <> Nothing = b (Just a) <> (Just b) = Just (a <> b) instance Semigroup a => Monoid (Maybe a) where mempty = Nothing -- ------ instance Functor Maybe where fmap _ Nothing = Nothing fmap f (Just b) = Just (fb) 

¡El elemento conjugado Quizás convierte un semigrupo en un monoide!

Liberalización de restricciones en versiones recientes de GHC
De 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:

 -- already in Data.Semigroup & Data.Monoid newtype Option a = Option {getOption :: Maybe a} deriving (Eq, Ord, Semigroup) instance (Ord a, Semigroup a) => Monoid (Option a) where mempty = Option Nothing 

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 detallada
Las 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ón
En 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)) {- -- for GHC 8.2 aggregate = (fmap getMax . getOption *** getSum) . (foldMap (Option . 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)) -- coerce :: (Maybe (Max Integer), Sum Integer) -> (Maybe Integer, Integer) 

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?

 -- already in Data.Ord -- Down a - reversed order newtype Down a = Down a deriving (Eq, Show) instance Ord a => Ord (Down a) where compare (Down x) (Down y) = y `compare` x import Data.List(sort) -- Sorted data Sorted a = Sorted [a] deriving (Show, Eq, Ord) fromList2Sorted :: Ord a => [a] -> Sorted a fromList2Sorted = Sorted . sort -- minimum: O(1) ! minView :: Sorted a -> Maybe a minView (Sorted []) = Nothing minView (Sorted (a : _)) = Just a 

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!

Source: https://habr.com/ru/post/es425405/


All Articles