Nouveaux types de puissance

Newtype est une déclaration de type de données spécialisée. Telle qu'elle ne contient qu'un constructeur et un champ.

newtype Foo a = Bar a newtype Id = MkId Word 


Questions typiques pour les débutants


Quelle est la différence avec les données de type de données?

 data Foo a = Bar a data Id = MkId Word 

La principale spécificité de newtype est qu'il se compose des mêmes parties que son seul champ. Plus précisément, il diffère de l'original au niveau du type, mais a la même représentation en mémoire, et est calculé strictement (pas paresseusement).
En bref - les nouveaux types sont plus efficaces en raison de leur présentation.

Oui, cela ne signifie rien pour moi ... je vais utiliser des données
Non, enfin, vous pouvez toujours activer l'extension -funpack-strict-fields :) pour les champs stricts (pas paresseux) ou spécifier directement

 data Id = MkId !Word 

Pourtant, la puissance de newtype ne se limite pas à l'efficacité de calcul. Ils sont beaucoup plus forts!

3 rôles newtype




Masquer la mise en œuvre


 module Data.Id (Id()) where newtype Id = MkId Word 

newtype diffère de l'original, en interne uniquement Word .
Mais nous masquons le constructeur MkId en dehors du module.

Mise en œuvre mise en œuvre


 {-# LANGUAGE GeneralizedNewtypeDeriving #-} newtype Id = MkId Word deriving (Num, Eq) 

Bien que cela ne soit pas dans la norme Haskell2010, en développant la sortie de newTypes génériques, vous pouvez automatiquement déduire le comportement de newtype de la même manière que le comportement du champ interne. Dans notre cas, le comportement de Eq Id et Num Id est le même que Eq Word et Num Word .

Beaucoup plus peut être réalisé grâce à l'expansion de l'élevage raffiné ( DerivingVia ), mais plus à ce sujet plus tard.

Mise en œuvre du choix


Malgré son propre constructeur, dans certains cas, vous pouvez utiliser votre représentation interne.

Défi


Il y a une liste d'entiers. Trouvez le montant maximum et total en un seul passage sur la liste.
Et n'utilisez pas de plis et de plis .

Réponse typique


Bien sûr, pliez ! :)

 foldr :: Foldable t => (a -> b -> b) -> b -> ta -> b {- -- instance Foldable [] foldr :: (a -> b -> b) -> b -> [a] -> b -} 

Et, la fonction finale est décrite comme ceci:

 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 vous regardez attentivement, vous pouvez voir des opérations similaires des deux côtés: juste el `max` m et el + s . Dans les deux cas, cartographie et opération binaire. Et les éléments vides sont Nothing et 0 .

Oui, ce sont des monoides!

Monoïde et semi-groupe plus en détail
Un semi-groupe est une propriété d'une opération binaire associative

 x ⋄ (y ⋄ z) == (x ⋄ y) ⋄ z 

Un monoïde est une propriété d'une opération associative (c'est-à-dire un semi-groupe)

 x ⋄ (y ⋄ z) == (x ⋄ y) ⋄ z 

qui a un élément vide qui ne change aucun élément ni à droite ni à gauche

 x ⋄ empty == x == empty ⋄ x 


Max et (+) sont associatifs, les deux ont des éléments vides - rien et 0 .

Et la combinaison de la cartographie des monoïdes avec la convolution est pliable !

Pliable plus détaillé
Rappelons la définition du pliage:

 class Foldable t where foldMap :: (Monoid m) => (a -> m) -> ta -> m ... 


Appliquons le comportement de pliage à max et (+) . Nous ne pouvons pas organiser plus d'une implémentation du monoïde Word . Il est temps de profiter de l'implémentation du choix newtype !

 {-# 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 

Il faut faire une remarque.

Le fait est que pour être monoïde pour le type de données Max , nous avons besoin d'un élément minimal, c'est-à-dire pour qu'un élément vide existe. Ainsi, seul un Max a limité peut être un monoïde.

Monoïde d'élément maximal théoriquement correct
 newtype Max a = Max a instance Ord a => Semigroup (Max a) instance Bounded a => Monoid (Max a) 


Donc, d'une manière ou d'une autre, nous devrons convertir notre type de données afin qu'un élément vide apparaisse et que nous puissions utiliser la coagulation.

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

L'élément conjugué Maybe transforme un semi-groupe en un monoïde!

Libéralisation des restrictions dans les versions récentes du GHC
De retour dans GHC 8.2, une contrainte de type monoïde était requise

 instance Monoid a => Monoid (Maybe a) 

ce qui signifie que nous avions besoin d'un autre nouveau type:

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

Et c'est déjà beaucoup plus simple dans GHC 8.4, où seul un semi-groupe en restriction de type est nécessaire, et même il n'est pas nécessaire de créer un type d'option.

 instance Semigroup a => Monoid (Maybe a) 


Réponse pliante


Eh bien, mettez à jour le code en utilisant la pliabilité et les flèches.
Nous rappelons que (.) Est juste une composition fonctionnelle:

  (.) :: (b -> c) -> (a -> b) -> a -> c f . g = \x -> f (gx) 

Et rappelez-vous que fmap est un foncteur:

 fmap :: Functor f => (a -> b) -> fa -> fb 

et son implémentation pour Maybe est décrite un peu plus haut.

Flèche plus détaillée
Les flèches sont des propriétés de certaines fonctions qui vous permettent de travailler avec elles dans un diagramme.
Plus de détails peuvent être trouvés ici: Flèches: une interface générale pour le calcul
Dans notre cas, nous utilisons la fonction Arrows
C’est

 instance Arrow (->) 

Nous utiliserons les fonctions:

 (***) :: Arrow a => abc -> ab' c' -> a (b, b') (c, c') (&&&) :: Arrow a => abc -> abc' -> ab (c, c') 

Pour notre cas
 abc == (->) bc == b -> c 

Et, en conséquence, la signature de nos fonctions se réduit à:

 (***) :: (b -> c) -> (b' -> c') -> ((b, b') -> (c, c')) (&&&) :: (b -> c) -> (b -> c') -> (b -> (c, c')) 

Ou en termes assez simples, la fonction (***) combine deux fonctions avec un argument (et un type de sortie) en une fonction avec le travail d'une paire d'arguments à l'entrée et à la sortie, respectivement, une paire de types de sortie.

La fonction (&&&) est une version allégée (***) , où le type des arguments d'entrée des deux fonctions est le même, et à l'entrée nous n'avons pas une paire d'arguments, mais un argument.

Au total, la fonction unificatrice a acquis la forme:

 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)) -} 

Cela s'est avéré très brièvement!

Mais, il est toujours fatigant d'encapsuler et d'inverser les données des types imbriqués!
Vous pouvez le réduire davantage et une conversion forcée sans ressources nous aidera!

Rôles de conversion forcée et de type sécurisés et sans ressources


Il y a une fonction du paquet Unsafe.Coerce - unsafeCoerce

 import Unsafe.Coerce(unsafeCoerce) unsafeCoerce :: a -> b 

La fonction forcée non sécurisée convertit le type: de a en b .
En fait, la fonction est magique, elle indique au compilateur de considérer les données de type a comme de type b , sans prendre en compte les conséquences de cette étape.

Il peut être utilisé pour convertir des types imbriqués, mais vous devez être très prudent.

En 2014, il y a eu une révolution avec le nouveau type, à savoir une conversion forcée sûre et sans ressources!

 import Data.Coerce(coerce) coerce :: Coercible ab => a -> b 

Cette fonctionnalité a ouvert une nouvelle ère en travaillant avec newtype .

Coercible Forced Converter fonctionne avec des types qui ont la même structure en mémoire. Elle ressemble à une classe de type, mais en fait le GHC convertit les types au moment de la compilation et il n'est pas possible de définir des instances par lui-même.
La fonction Data.Coerce.coerce vous permet de convertir des types sans ressources, mais pour cela, nous devons avoir accès aux constructeurs de types.

Simplifions maintenant notre fonction:

 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) 

Nous avons évité la routine de tirer des types imbriqués; nous l'avons fait sans gaspiller les ressources avec une seule fonction.

Rôles des types de données imbriqués


Avec la fonction de coercition, nous pouvons forcer la conversion de tous les types imbriqués.
Mais est-il nécessaire d'utiliser si largement cette fonctionnalité?

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

Sémantiquement, il est absurde de convertir en Sorted a de Sorted (Down a) .
Cependant, vous pouvez essayer:

 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) 

Tout irait bien, mais la bonne réponse est Just (Down 3) .
À savoir, afin de supprimer les comportements incorrects, des rôles de type ont été introduits.

 {-# LANGUAGE RoleAnnotations #-} type role Sorted nominal 

Essayons maintenant:

 ghci> minView (coerce h :: Sorted (Down Int)) error: Couldn't match type 'Int' with 'Down Int' arising from a use of 'coerce' 

Beaucoup mieux!

Au total, il y a 3 rôles ( type rôle ):

  • représentation - équivalent si la même représentation
  • nominal - doit avoir exactement le même type
  • fantôme - indépendant du contenu réel. Équivalent à n'importe quoi

Dans la plupart des cas, le compilateur est suffisamment intelligent pour révéler le rôle du type, mais il peut être utile.

Dérivation raffinée DerivingVia Behavior


Grâce à l'expansion du langage DerivingVia , le rôle de distribution de newtype s'est amélioré.

À partir de GHC 8.6, qui a été récemment publié, cette nouvelle extension est apparue.

 {-# LANGUAGE DerivingVia #-} newtype Id = MkId Word deriving (Semigroup, Monoid) via Max Word 

Comme vous pouvez le voir, le comportement du type est automatiquement déduit en raison du raffinement de la sortie.
DerivingVia peut être appliqué à n'importe quel type qui prend en charge Coercible et surtout - complètement sans consommation de ressources!

De plus, DerivingVia peut être appliqué non seulement à newtype , mais également à tous les types isomorphes s'ils prennent en charge les génériques génériques et la conversion forcée coercitive .

Conclusions


Types newtype est une force puissante qui simplifie et améliore considérablement le code, élimine la routine et réduit la consommation de ressources.

Traduction originale : La grande puissance des nouveaux types (Hiromi Ishii)

PS Je pense qu'après cet article, publié il y a plus d'un an [pas par mon] article, la magie du newtype dans Haskell sur les nouveaux types deviendra un peu plus claire!

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


All Articles