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