web-dev-qa-db-fra.com

Est-ce une mauvaise forme de créer de nouveaux types / données pour plus de clarté?

Je voudrais savoir si c'est une mauvaise forme de faire quelque chose comme ça:

data Alignment = LeftAl | CenterAl | RightAl
type Delimiter = Char
type Width     = Int

setW :: Width -> Alignment -> Delimiter -> String -> String

Plutôt que quelque chose comme ça:

setW :: Int -> Char -> Char -> String -> String

Je sais que la refonte efficace de ces types ne fait que prendre quelques lignes en échange d'un code plus clair. Cependant, si j'utilise le type Delimiter pour plusieurs fonctions, ce serait beaucoup plus clair pour quelqu'un qui importe ce module ou qui lit le code plus tard.

Je suis relativement nouveau à Haskell, donc je ne sais pas quelle est la bonne pratique pour ce genre de choses. Si ce n'est pas une bonne idée, ou s'il y a quelque chose qui améliorerait la clarté qui est préféré, quel serait-il?

17
BryceTheGrand

Vous utilisez des alias de type, ils ne contribuent que légèrement à la lisibilité du code. Cependant, il est préférable d'utiliser newtype au lieu de type pour une meilleure sécurité de type. Comme ça:

data Alignment = LeftAl | CenterAl | RightAl
newtype Delimiter = Delimiter { unDelimiter :: Char }
newtype Width     = Width { unWidth :: Int }

setW :: Width -> Alignment -> Delimiter -> String -> String

Vous vous occuperez de l'habillage et du déballage supplémentaires de newtype. Mais le code sera plus robuste contre d'autres refactorisations. Ce guide de style suggère d'utiliser type uniquement pour spécialiser les types polymorphes.

20
Shersh

Je ne considérerais pas cette mauvaise forme, mais clairement, je ne parle pas au nom de la communauté Haskell en général. La fonctionnalité de langage existe, pour autant que je sache, dans ce but particulier: rendre le code plus facile à lire.

On peut trouver des exemples d'utilisation d'alias de type dans diverses bibliothèques "de base". Par exemple, la classe Read définit cette méthode:

readList :: ReadS [a]

Le type ReadS n'est qu'un alias de type

type ReadS a = String -> [(a, String)]

Un autre exemple est le type Forest dans Data.Tree :

type Forest a = [Tree a]

Comme le souligne Shersh , vous pouvez également envelopper de nouveaux types dans les déclarations newtype. C'est souvent utile si vous devez d'une manière ou d'une autre contraindre le type d'origine (par exemple avec constructeurs intelligents ) ou si vous souhaitez ajouter des fonctionnalités à un type sans créer d'instances orphelines (un exemple typique est de définir QuickCheck Arbitrary instances vers des types qui ne sont pas fournis avec une telle instance).

14
Mark Seemann

L'utilisation de newtype— qui crée un nouveau type avec la même représentation que le type sous-jacent mais non substituable avec lui - est considérée comme une bonne forme. C'est un moyen bon marché d'éviter obsession primitive , et c'est particulièrement utile pour Haskell car dans Haskell les noms des arguments de fonction ne sont pas visibles dans la signature.

Les nouveaux types peuvent également être un endroit où suspendre des instances de classe de types utiles.

Étant donné que les nouveaux types sont omniprésents dans Haskell, au fil du temps, le langage a acquis des outils et des idiomes pour les manipuler:

  • Coercible Une classe de type "magique" qui simplifie les conversions entre les nouveaux types et leurs types sous-jacents, lorsque le constructeur de nouveau type est dans la portée. Souvent utile pour éviter le passe-partout dans les implémentations de fonctions.

    ghci> coerce (Sum (5::Int)) :: Int

    ghci> coerce [Sum (5::Int)] :: [Int]

    ghci> coerce ((+) :: Int -> Int -> Int) :: Identity Int -> Identity Int -> Identity Int

  • ala . Un idiome (implémenté dans divers packages) qui simplifie la sélection d'un nouveau type que nous pourrions vouloir utiliser avec des fonctions comme foldMap.

    ala Sum foldMap [1,2,3,4 :: Int] :: Int

  • GeneralizedNewtypeDeriving . Une extension pour dériver automatiquement des instances pour votre nouveau type en fonction des instances disponibles dans le type sous-jacent.

  • DerivingVia Une extension plus générale, pour dériver automatiquement des instances pour votre nouveau type basé sur des instances disponibles dans une autre newtype avec le même type sous-jacent.

10
danidiaz

Une chose importante à noter est que Alignment contre Char n'est pas seulement une question de clarté, mais une question d'exactitude. Votre type Alignment exprime le fait qu'il n'y a que trois alignements valides, contrairement au nombre d'habitants Char. En l'utilisant, vous évitez les problèmes avec des valeurs et des opérations non valides, et vous permettez également à GHC de vous informer de manière informée des correspondances de modèle incomplètes si les avertissements sont activés.

Quant aux synonymes, les opinions varient. Personnellement, je pense que type synonymes de petits types comme Int peuvent augmenter la charge cognitive, en vous faisant suivre différents noms pour ce qui est rigoureusement la même chose. Cela dit, à gauche fait un bon point en ce que ce type de synonyme peut être utile dans les premières étapes du prototypage d'une solution, lorsque vous ne voulez pas nécessairement vous soucier des détails de la représentation concrète que vous vont adopter pour vos objets de domaine.

(Il convient de mentionner que les remarques ici à propos de type ne s'appliquent pas en grande partie à newtype. Les cas d'utilisation sont cependant différents: tandis que type introduit simplement un nom différent pour la même chose, newtype introduit une chose différente par fiat. Cela peut être un mouvement étonnamment puissant - voir réponse de danidiaz pour plus de détails.)

6
duplode

C'est certainement bon, et voici un autre exemple, supposez que vous ayez ce type de données avec un op:

data Form = Square Int | Rectangle Int Int | EqTriangle Int

perimeter :: Form -> Int
perimeter (Square s)      = s * 4
perimeter (Rectangle b h) = (b * h) * 2
perimeter (EqTriangle s)  = s * 3

area :: Form -> Int
area (Square s)      = s ^ 2
area (Rectangle b h) = (b * h)
area (EqTriangle s)  = (s ^ 2) `div` 2 

Imaginez maintenant que vous ajoutez le cercle:

data Form = Square Int | Rectangle Int Int | EqTriangle Int | Cicle Int

ajouter ses opérations:

perimeter (Cicle r )      = pi * 2 * r

area (Cicle r)       = pi * r ^ 2

ce n'est pas très bon non? Maintenant, je veux utiliser Float ... Je dois changer chaque Int pour Float

data Form = Square Double | Rectangle Double Double | EqTriangle Double | Cicle Double


area :: Form -> Double

perimeter :: Form -> Double

mais, si, pour plus de clarté et même pour la réutilisation, j'utilise le type?

data Form = Square Side | Rectangle Side Side | EqTriangle Side | Cicle Radius

type Distance = Int
type Side = Distance
type Radius = Distance
type Area = Distance

perimeter :: Form -> Distance
perimeter (Square s)      = s * 4
perimeter (Rectangle b h) = (b * h) * 2
perimeter (EqTriangle s)  = s * 3
perimeter (Cicle r )      = pi * 2 * r

area :: Form -> Area
area (Square s)      = s * s
area (Rectangle b h) = (b * h)
area (EqTriangle s)  = (s * 2) / 2
area (Cicle r)       = pi * r * r

Cela me permet de changer le type en ne changeant qu'une ligne dans le code, supposons que je veux que la distance soit en Int, je ne changerai que

perimeter :: Form -> Distance
...

totalDistance :: [Form] -> Distance
totalDistance = foldr (\x rs -> perimeter x + rs) 0

Je veux que la distance soit flottante, alors je change juste:

type Distance = Float

Si je veux le changer en Int, je dois faire quelques ajustements dans les fonctions, mais c'est un autre problème.

2