Pour moi, il semble qu'une définition de nouveau type est juste une définition de données qui obéit à certaines restrictions (un seul constructeur et ainsi de suite), et que grâce à ces restrictions, le système d'exécution peut gérer les nouveaux types plus efficacement. Ok, et la gestion du filtrage pour les valeurs indéfinies est légèrement différente. Mais supposons que Haskell ne connaisse que des définitions de données, pas de nouveaux types : Le compilateur ne pourrait-il pas découvrir par lui-même si une définition de données donnée obéit à ces restrictions, et la traiter automatiquement de manière plus efficace ? Je suis sûr que je rate quelque chose, ces concepteurs de Haskell sont si intelligents, il doit y avoir une raison plus profonde pour cela...
Réponses
Trop de publicités?Les deux sites newtype
et le constructeur unique data
introduisent un seul constructeur de valeur, mais le constructeur de valeur introduit par newtype
est strict et le constructeur de valeur introduit par data
est paresseux. Donc si vous avez
data D = D Int
newtype N = N Int
Puis N undefined
est équivalent à undefined
et provoque une erreur lors de son évaluation. Mais D undefined
es no équivalent à undefined
et il peut être évalué tant que vous n'essayez pas de jeter un coup d'œil à l'intérieur.
Le compilateur ne pourrait-il pas s'en charger lui-même ?
Non, pas vraiment - c'est un cas où, en tant que programmeur, vous devez décider si le constructeur est strict ou paresseux. Pour comprendre quand et comment rendre les constructeurs stricts ou paresseux, vous devez avoir une bien meilleure compréhension de l'évaluation paresseuse que moi. Je m'en tiens à l'idée du rapport, à savoir que newtype
est là pour vous permettre de renommer un type existant, comme avoir plusieurs types de mesures incompatibles :
newtype Feet = Feet Double
newtype Cm = Cm Double
Les deux se comportent exactement comme Double
au moment de l'exécution, mais le compilateur promet de ne pas vous laisser les confondre.
Selon Apprendre un Haskell :
Au lieu du mot clé data, on utilise le mot clé newtype. Mais pourquoi pourquoi ? Tout d'abord, newtype est plus rapide. Si vous utilisez le mot-clé data pour pour envelopper un type, il y a une certaine surcharge à tout cet emballage et déballage pendant l'exécution de votre programme. Mais si vous utilisez newtype, Haskell sait que vous l'utilisez simplement pour envelopper un type existant dans un nouveau type. (d'où le nom), parce que vous voulez que ce soit le même type en interne mais avoir un type différent. En gardant cela à l'esprit, Haskell peut se débarrasser de la fonction une fois qu'il a déterminé quelle valeur est de quel type.
Alors pourquoi ne pas utiliser newtype tout le temps au lieu de data ? Et bien, lorsque vous créez un nouveau type à partir d'un type existant en utilisant le mot-clé newtype vous ne pouvez avoir qu'un seul constructeur de valeur et cette valeur et ce constructeur de valeur ne peut avoir qu'un seul champ. Mais avec les données, vous pouvez créer des types de données de données qui possèdent plusieurs constructeurs de valeur et chaque constructeur peut avoir zéro ou plusieurs champs :
data Profession = Fighter | Archer | Accountant
data Race = Human | Elf | Orc | Goblin
data PlayerCharacter = PlayerCharacter Race Profession
Lorsque vous utilisez newtype, vous êtes limité à un seul constructeur avec un seul champ.
Considérons maintenant le type suivant :
data CoolBool = CoolBool { getCoolBool :: Bool }
C'est un type de données algébriques ordinaire qui a été défini avec le mot-clé le mot-clé data. Il a un constructeur de valeur, qui a un champ dont le type est Bool. Créons une fonction qui effectue une correspondance de motif sur un CoolBool et renvoie la valeur "hello". CoolBool et renvoie la valeur "hello", que le Bool à l'intérieur du CoolBool est True ou False :
helloMe :: CoolBool -> String
helloMe (CoolBool _) = "hello"
Au lieu d'appliquer cette fonction à un CoolBool normal, nous allons lui lancer une balle courbe et l'appliquer à un indéfini !
ghci> helloMe undefined
"*** Exception: Prelude.undefined
Oups ! Une exception ! Maintenant, pourquoi cette exception s'est-elle produite ? Les types définis avec le mot clé data peuvent avoir plusieurs constructeurs de valeur (même si même si CoolBool n'en a qu'un seul). Donc, afin de voir si la valeur donnée donnée à notre fonction est conforme au pattern (CoolBool _), Haskell doit évaluer la valeur juste assez pour voir quel constructeur de valeur a été utilisé lorsque nous avons créé la valeur. Et quand nous essayons d'évaluer une valeur indéfinie indéfinie, même un peu, une exception est levée.
Au lieu d'utiliser le mot-clé data pour CoolBool, essayons d'utiliser nouveau type :
newtype CoolBool = CoolBool { getCoolBool :: Bool }
Nous n'avons pas besoin de changer notre fonction helloMe, car la syntaxe du filtrage est la même la même si vous utilisez newtype ou data pour définir votre type. Faisons la la même chose ici et appliquons helloMe à une valeur indéfinie :
ghci> helloMe undefined
"hello"
Ça a marché ! Hmmm, pourquoi ça ? Eh bien, comme nous l'avons dit, lorsque nous utilisons newtype, Haskell peut représenter en interne les valeurs du nouveau type de la même manière que les valeurs originales. Il n'a pas besoin d'ajouter un autre boîte autour d'elles, il doit juste être conscient que les valeurs sont de différents types. différents types. Et parce que Haskell sait que les types créés avec le mot-clé newtype ne peuvent avoir qu'un seul constructeur, il n'a pas à d'évaluer la valeur transmise à la fonction pour s'assurer qu'elle est pour s'assurer qu'elle est conforme au modèle (CoolBool _), car les types de type avoir un seul constructeur de valeur possible et un seul champ !
Cette différence de comportement peut sembler insignifiante, mais elle est en fait très importante. importante parce qu'elle nous aide à réaliser que même si les types définis data et newtype se comportent de façon similaire du point de vue du programmeur programmeur, car ils ont tous deux des constructeurs de valeurs et des champs sont en fait deux mécanismes différents. Alors que data peut être utilisé pour créer vos propres types à partir de rien, newtype sert à créer un type complètement nouveau type à partir d'un type existant. La recherche de motifs sur les valeurs de newtype n'est pas n'est pas comme sortir quelque chose d'une boîte (comme c'est le cas avec les données), il s'agit plutôt de faire une conversion directe d'un type à un autre. de faire une conversion directe d'un type à un autre.
Voici une autre source. Selon cet article de Newtype :
Une déclaration newtype crée un nouveau type de la même manière que les données. La syntaxe et l'utilisation des newtypes sont pratiquement identiques à celles des déclarations de données. data - en fait, vous pouvez remplacer le mot-clé newtype par data et cela compilera quand même, il y a même de bonnes chances que votre programme fonctionne toujours. L'inverse n'est pas vrai, cependant - data peut être remplacé par newtype être remplacé par newtype uniquement si le type possède exactement un constructeur avec exactement un champ à l'intérieur.
Quelques exemples :
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)
Cela semble assez limité ! Alors pourquoi quelqu'un utilise-t-il newtype ?
La version courte La restriction à un constructeur avec un champ signifie que le nouveau type et le type du champ sont en relation directe. directe :
State :: (s -> (a, s)) -> State s a runState :: State s a -> (s -> (a, s))
ou, en termes mathématiques, ils sont isomorphes. Cela signifie qu'après le type est vérifié à la compilation, au moment de l'exécution les deux types peuvent être être traités essentiellement de la même manière, sans la surcharge ou l'indirection normalement associés à un constructeur de données. Ainsi, si vous souhaitez déclarer différentes instances de classe de type pour un type particulier, ou si vous voulez rendre un type abstrait, vous pouvez l'envelopper dans un newtype et il sera considéré comme distinct pour le contrôleur de type, mais identique au moment de l'exécution. Vous pouvez alors utiliser toutes sortes d'astuces profondes comme les types fantômes ou récursifs sans se sans s'inquiéter que GHC mélange des paquets d'octets sans raison.
Ver l'article pour les parties désordonnées...
Version simple pour les personnes obsédées par les listes à puces (je n'en ai pas trouvé, je dois donc l'écrire moi-même) :
données - crée un nouveau type algébrique avec des constructeurs de valeurs
- Peut avoir plusieurs constructeurs de valeurs
- Les constructeurs de valeurs sont paresseux
- Les valeurs peuvent comporter plusieurs champs
- Affectent à la fois la compilation et l'exécution, ont une surcharge d'exécution.
- Le type créé est un nouveau type distinct
- Peut avoir ses propres instances de classe de type
- Lors d'un filtrage par rapport à des constructeurs de valeurs, il faut évaluer au moins la forme normale de tête faible (WHNF) *.
- Utilisé pour créer un nouveau type de données (exemple : Adresse { zip : : String, street : : String } )
nouveau type - crée un nouveau type "décoration" avec un constructeur de valeur
- Ne peut avoir qu'un seul constructeur de valeur
- Le constructeur de valeur est strict
- La valeur ne peut avoir qu'un seul champ
- N'affecte que la compilation, pas de frais d'exécution.
- Le type créé est un nouveau type distinct
- Peut avoir ses propres instances de classe de type
- Lors d'un filtrage par rapport à un constructeur de valeur, il peut ne pas être évalué du tout *.
- Utilisé pour créer un concept de niveau supérieur basé sur un type existant avec un ensemble distinct d'opérations prises en charge ou qui n'est pas interchangeable avec le type original (exemple : Mètre, Cm, Pieds est Double)
type - crée un nom alternatif (synonyme) pour un type (comme typedef en C)
- Pas de constructeurs de valeurs
- Aucun champ
- N'affecte que la compilation, pas de frais d'exécution.
- Aucun nouveau type n'est créé (seulement un nouveau nom pour le type existant).
- Ne peut PAS avoir ses propres instances de classe de type
- En cas de correspondance avec le constructeur de données, le comportement est le même que celui du type original.
- Utilisé pour créer un concept de niveau supérieur basé sur un type existant avec le même ensemble d'opérations supportées (exemple : String is [Char]).
[*] Sur la paresse de la correspondance des motifs :
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"
De mémoire, les déclarations de données utilisent l'évaluation paresseuse pour l'accès et le stockage de leurs "membres", alors que le nouveau type ne le fait pas. Newtype supprime également toutes les instances de type précédentes de ses composants, cachant efficacement son implémentation, alors que data laisse l'implémentation ouverte.
J'ai tendance à utiliser les nouveaux types pour éviter le code passe-partout dans les types de données complexes dont je n'ai pas nécessairement besoin d'accéder à l'interne pour les utiliser. Cela accélère à la fois la compilation et l'exécution, et réduit la complexité du code lorsque le nouveau type est utilisé.
Quand j'ai lu la première fois à ce sujet, j'ai trouvé ce chapitre d'une introduction douce à Haskell plutôt intuitive.