web-dev-qa-db-fra.com

Pourquoi y a-t-il des «données» et des «nouveaux types» à Haskell?

Il semble qu'une définition newtype est juste une définition data qui obéit à certaines restrictions (par exemple, un seul constructeur), et qu'en raison de ces restrictions, le système d'exécution peut gérer newtypes plus efficacement. Et la gestion de la correspondance de motifs pour les valeurs non définies est légèrement différente.

Mais supposons que Haskell ne connaisse que les définitions de data, pas de newtypes: le compilateur ne pourrait-il pas déterminer par lui-même si une définition de données donnée respecte ces restrictions et la traiter automatiquement plus efficacement?

Je suis sûr que je manque quelque chose, il doit y avoir une raison plus profonde à cela.

142
martingw

newtype et le constructeur unique data introduisent un constructeur de valeur unique, 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

Ensuite N undefined est équivalent à undefined et provoque une erreur lors de l'évaluation. Mais D undefined est pas équivalent à undefined, et il peut être évalué tant que vous n'essayez pas de jeter un œil à l'intérieur.

Le compilateur n'a pas pu gérer cela par lui-même.

Non, pas vraiment - c'est un cas où, en tant que programmeur, vous décidez 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 de renommer un type existant, comme avoir plusieurs types de mesures incompatibles différents:

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.

175
Norman Ramsey

Selon Learn You a Haskell :

Au lieu du mot-clé data, le mot-clé newtype est utilisé. Maintenant, pourquoi ça? Eh bien pour un, le nouveau type est plus rapide. Si vous utilisez le mot-clé data pour encapsuler un type, il y a des frais généraux pour tout ce qui encapsule et décompresse lorsque votre programme s'exécute. Mais si vous utilisez newtype, Haskell sait que vous ne l'utilisez que pour envelopper un type existant dans un nouveau type (d'où le nom), car vous voulez qu'il soit le même en interne mais avec un type différent. Dans cet esprit, Haskell peut se débarrasser de l'emballage et du déballage une fois qu'il a résolu quelle valeur est de quel type.

Alors pourquoi ne pas utiliser tout le temps newtype à la place des données? Eh bien, lorsque vous créez un nouveau type à partir d'un type existant à l'aide du mot clé newtype, vous ne pouvez avoir qu'un seul constructeur de 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 qui ont plusieurs constructeurs de valeurs 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érez maintenant le type suivant:

data CoolBool = CoolBool { getCoolBool :: Bool } 

C'est votre type de données algébrique ordinaire qui a été défini avec le mot-clé data. Il a un constructeur de valeur, qui a un champ dont le type est Bool. Faisons une fonction qui correspond au modèle sur un CoolBool et renvoie la valeur "bonjour", que le Bool à l'intérieur du CoolBool soit vrai ou faux:

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

Au lieu d'appliquer cette fonction à un CoolBool normal, jetons-lui une courbe et appliquons-la à undefined!

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

Oui! Une exception! Maintenant, pourquoi cette exception s'est-elle produite? Les types définis avec le mot-clé data peuvent avoir plusieurs constructeurs de valeurs (même si CoolBool n'en a qu'un). Donc, pour voir si la valeur donnée à notre fonction est conforme au modèle (CoolBool _), Haskell doit évaluer la valeur juste assez pour voir quel constructeur de valeur a été utilisé lorsque nous avons fait la valeur. Et lorsque nous essayons d'évaluer une valeur indéfinie, même un peu, une exception est levée.

Au lieu d'utiliser le mot clé data pour CoolBool, essayons d'utiliser newtype:

newtype CoolBool = CoolBool { getCoolBool :: Bool }   

Nous n'avons pas à modifier notre fonction helloMe, car la syntaxe de correspondance de modèle est la même si vous utilisez newtype ou data pour définir votre type. Faisons 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 d'origine. Il n'a pas besoin d'ajouter une autre boîte autour d'eux, il doit juste être conscient que les valeurs sont de différents types. Et comme Haskell sait que les types créés avec le mot clé newtype ne peuvent avoir qu'un seul constructeur, il n'a pas à évaluer la valeur transmise à la fonction pour s'assurer qu'elle est conforme au modèle (CoolBool _) car les types newtype ne peuvent en avoir qu'un constructeur de valeur possible et un champ!

Cette différence de comportement peut sembler triviale, mais elle est en fait assez importante car elle nous aide à réaliser que même si les types définis avec des données et un nouveau type se comportent de la même manière du point de vue du programmeur car ils ont tous deux des constructeurs de valeurs et des champs, ils sont en fait deux mécanismes différents . Alors que les données peuvent être utilisées pour créer vos propres types à partir de zéro, newtype sert à créer un type complètement nouveau à partir d'un type existant. La correspondance de modèles sur les valeurs de nouveau type n'est pas comme retirer quelque chose d'une boîte (comme c'est le cas avec des données), il s'agit plutôt de faire une conversion directe d'un type à un autre.

Voici une autre source. Selon cet article 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 nouveaux types sont pratiquement identiques à celles des déclarations de données - en fait, vous pouvez remplacer le mot-clé nouveau type par des données et il continuera à être compilé, en effet, il y a même de bonnes chances que votre programme fonctionne toujours. L'inverse n'est pas vrai, cependant - les données ne peuvent être remplacées par newtype que si le type a 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 un nouveau type?

La version courte La restriction à un constructeur avec un champ signifie que le nouveau type et le type du champ sont en correspondance 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 vérification du type au moment de la compilation, au moment de l'exécution, les deux types peuvent être traités essentiellement de la même manière, sans la surcharge ou l'indirection normalement associée à un constructeur de données. Donc, si vous voulez déclarer différentes instances de classe de type pour un type particulier, ou faire un type abstrait, vous pouvez l'encapsuler dans un nouveau type et il sera considéré comme distinct pour le vérificateur de type, mais identique lors de l'exécution. Vous pouvez ensuite utiliser toutes sortes de tromperies profondes comme les types fantômes ou récursifs sans vous soucier du brassage des octets par GHC sans raison.

Voir l'article pour les bits en désordre ...

59
Rose Perrone

Version simple pour les personnes obsédées par les listes à puces (impossible d'en trouver une, donc je dois l'écrire moi-même):

data - crée un nouveau type algébrique avec des constructeurs de valeurs

  • Peut avoir plusieurs constructeurs de valeur
  • Les constructeurs de valeur sont paresseux
  • Les valeurs peuvent avoir plusieurs champs
  • Affecte à la fois la compilation et l'exécution, a une surcharge d'exécution
  • Le type créé est un nouveau type distinct
  • Peut avoir ses propres instances de classe de type
  • Lorsque la correspondance de motifs par rapport aux constructeurs de valeurs, sera évaluée au moins à la forme normale de tête faible (WHNF) *
  • Utilisé pour créer un nouveau type de données (exemple: Address {Zip :: String, street :: String})

newtype - crée un nouveau type de "décoration" avec le 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
  • Affecte uniquement la compilation, pas de surcharge d'exécution
  • Le type créé est un nouveau type distinct
  • Peut avoir ses propres instances de classe de type
  • Lorsque le modèle correspond au constructeur de valeurs, NE PEUT PAS être évalué du tout *
  • Utilisé pour créer un concept de niveau supérieur basé sur le type existant avec un ensemble distinct d'opérations prises en charge ou qui n'est pas interchangeable avec le type d'origine (exemple: mètre, cm, les pieds sont doubles)

type - crée un autre nom (synonyme) pour un type (comme typedef en C)

  • Aucun constructeur de valeur
  • Pas de champs
  • Affecte uniquement la compilation, pas de surcharge d'exécution
  • Aucun nouveau type n'est créé (uniquement un nouveau nom pour le type existant)
  • Ne peut PAS avoir ses propres instances de classe de type
  • Lorsque le modèle correspond au constructeur de données, se comporte de la même manière que le type d'origine
  • Utilisé pour créer un concept de niveau supérieur basé sur un type existant avec le même ensemble d'opérations prises en charge (exemple: String is [Char])

[*] Sur la paresse de correspondance de 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"
45
wonder.mice

Du haut de ma tête; les déclarations de données utilisent l'évaluation paresseuse dans l'accès et le stockage de leurs "membres", contrairement à newtype. Newtype supprime également toutes les instances de type précédentes de ses composants, masquant efficacement son implémentation; tandis que les données laissent la mise en œuvre ouverte.

J'ai tendance à utiliser des types nouveaux pour éviter le code passe-partout dans les types de données complexes où je n'ai pas nécessairement besoin d'accéder aux internes lors de leur utilisation. Cela accélère la compilation et l'exécution et réduit la complexité du code lorsque le nouveau type est utilisé.

Lors de la première lecture à ce sujet, j'ai trouvé ce chapitre d'une introduction douce à Haskell plutôt intuitive.

9
Dan