Haskell Language
Extensions de langage GHC communes
Recherche…
Remarques
Ces extensions de langage sont généralement disponibles lors de l'utilisation du compilateur Glasgow Haskell (GHC), car elles ne font pas partie du rapport linguistique Haskell 2010 approuvé. Pour utiliser ces extensions, il faut soit informer le compilateur en utilisant un indicateur, soit placer un programme LANGUAGE
avant le mot-clé du module
dans un fichier. La documentation officielle se trouve dans la section 7 du guide de l'utilisateur GCH.
Le format du programme LANGUAGE
est {-# LANGUAGE ExtensionOne, ExtensionTwo ... #-}
. C'est le littéral {-#
suivi de LANGUAGE
suivi d'une liste d'extensions séparées par des virgules, et enfin de la fermeture #-}
. Plusieurs programmes LANGUAGE
peuvent être placés dans un fichier.
MultiParamTypeClasses
C'est une extension très courante qui permet des classes de type avec plusieurs paramètres de type. Vous pouvez considérer MPTC comme une relation entre les types.
{-# LANGUAGE MultiParamTypeClasses #-}
class Convertable a b where
convert :: a -> b
instance Convertable Int Float where
convert i = fromIntegral i
L'ordre des paramètres est important.
Les MPTC peuvent parfois être remplacées par des familles de type.
FlexibleInstances
Les instances régulières nécessitent:
All instance types must be of the form (T a1 ... an)
where a1 ... an are *distinct type variables*,
and each type variable appears at most once in the instance head.
Cela signifie que, par exemple, alors que vous pouvez créer une instance pour [a]
vous ne pouvez pas créer d'instance pour spécifiquement [Int]
. FlexibleInstances
détend que:
class C a where
-- works out of the box
instance C [a] where
-- requires FlexibleInstances
instance C [Int] where
Chaînes surchargées
Normalement, les littéraux de chaîne dans Haskell ont un type de String
(qui est un alias de type pour [Char]
). Bien que cela ne pose pas de problème aux petits programmes éducatifs, les applications du monde réel nécessitent souvent un stockage plus efficace, tel que Text
ou ByteString
.
OverloadedStrings
change simplement le type de littéral en
"test" :: Data.String.IsString a => a
En leur permettant d'être directement transmis aux fonctions qui attendent un tel type. De nombreuses bibliothèques implémentent cette interface pour leurs types de type chaîne, y compris Data.Text et Data.ByteString, qui offrent tous deux des avantages en termes de temps et d'espace sur [Char]
.
Il y a aussi des utilisations uniques de OverloadedStrings
comme celles de la bibliothèque Postgresql-simple qui permet d'écrire des requêtes SQL entre guillemets comme une chaîne normale, mais fournit des protections contre la concaténation incorrecte, une source notoire d'attaques par injection SQL.
Pour créer une instance de la classe IsString
, vous devez fromString
fonction fromString
. Exemple † :
data Foo = A | B | Other String deriving Show
instance IsString Foo where
fromString "A" = A
fromString "B" = B
fromString xs = Other xs
tests :: [ Foo ]
tests = [ "A", "B", "Testing" ]
† Cet exemple est une gracieuseté de Lyndon Maydwell ( sordina
sur GitHub) trouvé ici .
TupleSections
Une extension syntaxique qui permet d'appliquer le constructeur de tuple (qui est un opérateur) de manière sectionnelle:
(a,b) == (,) a b
-- With TupleSections
(a,b) == (,) a b == (a,) b == (,b) a
N-tuples
Il fonctionne également pour les tuples avec une arité supérieure à deux
(,2,) 1 3 == (1,2,3)
Cartographie
Cela peut être utile dans d'autres endroits où des sections sont utilisées:
map (,"tag") [1,2,3] == [(1,"tag"), (2, "tag"), (3, "tag")]
L'exemple ci-dessus sans cette extension ressemblerait à ceci:
map (\a -> (a, "tag")) [1,2,3]
UnicodeSyntax
Une extension qui vous permet d'utiliser des caractères Unicode à la place de certains opérateurs et noms intégrés.
ASCII | Unicode | Les usages) |
---|---|---|
:: | ∷ | a le type |
-> | → | types de fonctions, lambdas, branches de case , etc. |
=> | ⇒ | contraintes de classe |
forall | ∀ | polymorphisme explicite |
<- | ← | do notation |
* | ★ | le type (ou type) des types (par exemple, Int :: ★ ) |
>- | ⤚ | proc pour les Arrows |
-< | ⤙ | proc pour les Arrows |
>>- | ⤜ | proc pour les Arrows |
-<< | ⤛ | proc pour les Arrows |
Par exemple:
runST :: (forall s. ST s a) -> a
deviendrait
runST ∷ (∀ s. ST s a) → a
Notez que l'exemple *
vs. ★
est légèrement différent: puisque *
n'est pas réservé, ★
fonctionne de la même manière que *
pour la multiplication, ou toute autre fonction nommée (*)
, et vice-versa. Par exemple:
ghci> 2 ★ 3
6
ghci> let (*) = (+) in 2 ★ 3
5
ghci> let (★) = (-) in 2 * 3
-1
BinaryLiterals
Standard Haskell vous permet d'écrire des littéraux entiers en décimal (sans préfixe), hexadécimal (précédé de 0x
ou 0X
) et octal (précédé de 0o
ou 0O
). L'extension BinaryLiterals
ajoute l'option de binary (précédé de 0b
ou 0B
).
0b1111 == 15 -- evaluates to: True
ExistentialQuantification
Il s’agit d’une extension de type système qui autorise les types quantifiés de manière existentielle ou, en d’autres termes, qui ont des variables de type qui ne sont instanciées qu’à l’exécution † .
Une valeur de type existentiel est similaire à une référence de classe de base abstraite dans les langages OO: vous ne connaissez pas le type exact dans contient, mais vous pouvez contraindre la classe de types.
data S = forall a. Show a => S a
ou de manière équivalente, avec la syntaxe GADT:
{-# LANGUAGE GADTs #-}
data S where
S :: Show a => a -> S
Types existentiels ouvrent la porte à des choses comme des conteneurs presque hétérogènes: comme dit plus haut, il peut effectivement être différents types dans une S
valeur, mais ils peuvent tous être show
n, vous pouvez donc aussi faire
instance Show S where
show (S a) = show a -- we rely on (Show a) from the above
Maintenant, nous pouvons créer une collection de tels objets:
ss = [S 5, S "test", S 3.0]
Ce qui nous permet également d'utiliser le comportement polymorphe:
mapM_ print ss
Les existentiels peuvent être très puissants, mais notez qu'ils ne sont pas vraiment nécessaires dans Haskell. Dans l'exemple ci-dessus, tout ce que vous pouvez réellement faire avec l'instance Show
est de montrer (duh!) Les valeurs, c.-à-d. Créer une représentation sous forme de chaîne. Le type S
complet contient donc exactement autant d'informations que la chaîne que vous obtenez lorsque vous la montrez. Par conséquent, il est généralement préférable de simplement stocker cette chaîne tout de suite, d'autant plus que Haskell est paresseux et que, par conséquent, la chaîne ne sera d'abord qu'un non-évalué.
D'autre part, les existentiels causent des problèmes uniques. Par exemple, la manière dont les informations de type sont «cachées» dans un existentiel. Si vous faites correspondre un motif sur une valeur S
, vous aurez le type contenu dans la portée (plus précisément son instance Show
), mais cette information ne pourra jamais échapper à sa portée, qui devient donc une «société secrète»: le compilateur ne laisse rien échapper à la portée sauf les valeurs dont le type est déjà connu de l'extérieur. Cela peut conduire à des erreurs étranges comme Couldn't match type 'a0' with '()' 'a0' is untouchable
.
† Comparez ceci avec le polymorphisme paramétrique ordinaire, qui est généralement résolu au moment de la compilation (permettant un effacement complet).
Les types existentiels sont différents des types Rank-N - ces extensions sont, pour ainsi dire, doubles les unes aux autres: pour utiliser réellement des valeurs de type existentiel, il faut une fonction polymorphe (éventuellement contrainte), comme show
dans l'exemple. Une fonction polymorphe est universellement quantifiée, c'est-à-dire qu'elle fonctionne pour tout type dans une classe donnée, alors que la quantification existentielle signifie qu'elle fonctionne pour un type particulier qui est a priori inconnu. Si vous avez une fonction polymorphe, cela suffit, mais pour passer des fonctions polymorphes telles que des arguments, vous avez besoin de {-# LANGUAGE Rank2Types #-}
:
genShowSs :: (∀ x . Show x => x -> String) -> [S] -> [String]
genShowSs f = map (\(S a) -> f a)
LambdaCase
Une extension syntaxique qui vous permet d'écrire \case
à la place de \arg -> case arg of
.
Considérons la définition de fonction suivante:
dayOfTheWeek :: Int -> String
dayOfTheWeek 0 = "Sunday"
dayOfTheWeek 1 = "Monday"
dayOfTheWeek 2 = "Tuesday"
dayOfTheWeek 3 = "Wednesday"
dayOfTheWeek 4 = "Thursday"
dayOfTheWeek 5 = "Friday"
dayOfTheWeek 6 = "Saturday"
Si vous voulez éviter de répéter le nom de la fonction, vous pourriez écrire quelque chose comme:
dayOfTheWeek :: Int -> String
dayOfTheWeek i = case i of
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
À l'aide de l'extension LambdaCase, vous pouvez écrire cela en tant qu'expression de fonction, sans avoir à nommer l'argument:
{-# LANGUAGE LambdaCase #-}
dayOfTheWeek :: Int -> String
dayOfTheWeek = \case
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
RankNTypes
Imaginez la situation suivante:
foo :: Show a => (a -> String) -> String -> Int -> IO ()
foo show' string int = do
putStrLn (show' string)
putStrLn (show' int)
Ici, nous voulons passer une fonction qui convertit une valeur en une chaîne, appliquer cette fonction à la fois à un paramètre de chaîne et à un paramètre int et les imprimer tous les deux. Dans mon esprit, il n'y a aucune raison que cela échoue! Nous avons une fonction qui fonctionne sur les deux types de paramètres que nous transmettons.
Malheureusement, cela ne va pas vérifier GHC déduit a
type basé sur sa première occurrence dans le corps de la fonction. C'est-à-dire dès que nous frappons:
putStrLn (show' string)
GHC déduira que show' :: String -> String
, puisque string
est une String
. Il va exploser en essayant de show' int
.
RankNTypes
vous permet d'écrire la signature de type comme suit, en quantifiant toutes les fonctions qui satisfont le type de show'
:
foo :: (forall a. Show a => (a -> String)) -> String -> Int -> IO ()
C'est le polymorphisme de rang 2: nous affirmons que la fonction show'
doit fonctionner pour tous les a
s de notre fonction, et que l'implémentation précédente fonctionne maintenant.
La RankNTypes
extension permet l' imbrication arbitraire de forall ...
blocs dans les signatures de type. En d'autres termes, il permet le polymorphisme de rang N.
Listes surchargées
ajouté dans GHC 7.8 .
OverloadedLists, similaire à OverloadedStrings , permet de désabuser les littéraux de liste comme suit:
[] -- fromListN 0 []
[x] -- fromListN 1 (x : [])
[x .. ] -- fromList (enumFrom x)
Cela est pratique lorsque vous traitez des types tels que Set
, Vector
et Map
s.
['0' .. '9'] :: Set Char
[1 .. 10] :: Vector Int
[("default",0), (k1,v1)] :: Map String Int
['a' .. 'z'] :: Text
IsList
classe IsList
dans GHC.Exts
est destinée à être utilisée avec cette extension.
IsList
est équipé d'une fonction de type, Item
, et trois fonctions, fromList :: [Item l] -> l
, toList :: l -> [Item l]
et fromListN :: Int -> [Item l] -> l
où fromListN
est facultatif. Les implémentations typiques sont:
instance IsList [a] where
type Item [a] = a
fromList = id
toList = id
instance (Ord a) => IsList (Set a) where
type Item (Set a) = a
fromList = Set.fromList
toList = Set.toList
Exemples tirés de OverloadedLists - GHC .
Dépendances fonctionnelles
Si vous avez une classe de type multi-paramètres avec les arguments a, b, c et x, cette extension vous permet d'exprimer que le type x peut être identifié de manière unique à partir de a, b et c:
class SomeClass a b c x | a b c -> x where ...
Lors de la déclaration d'une instance de cette classe, elle sera vérifiée par rapport à toutes les autres instances pour s'assurer que la dépendance fonctionnelle est maintenue, c'est-à-dire qu'aucune autre instance avec le même abc
mais un x
différent n'existe.
Vous pouvez spécifier plusieurs dépendances dans une liste séparée par des virgules:
class OtherClass a b c d | a b -> c d, a d -> b where ...
Par exemple dans MTL on peut voir:
class MonadReader r m| m -> r where ...
instance MonadReader r ((->) r) where ...
Maintenant, si vous avez une valeur de type MonadReader a ((->) Foo) => a
, le compilateur peut en déduire a ~ Foo
, puisque le second argument détermine complètement le premier et simplifiera le type en conséquence.
La classe SomeClass
peut être considérée comme une fonction des arguments abc
qui donnent x
. De telles classes peuvent être utilisées pour effectuer des calculs dans le système de types.
GADT
Les types de données algébriques conventionnels sont paramétriques dans leurs variables de type. Par exemple, si on définit un ADT comme
data Expr a = IntLit Int
| BoolLit Bool
| If (Expr Bool) (Expr a) (Expr a)
avec l'espoir que cela exclut statiquement les conditions non bien typées, cela ne se comportera pas comme prévu puisque le type d' IntLit :: Int -> Expr a
est quantifié IntLit :: Int -> Expr a
: pour tout choix de a
, il produit une valeur de type Expr a
. En particulier, pour a ~ Bool
, nous avons IntLit :: Int -> Expr Bool
, ce qui nous permet de construire quelque chose comme If (IntLit 1) e1 e2
ce que le type du constructeur If
essayait d'éliminer.
Les types de données algébriques généralisés nous permettent de contrôler le type résultant d'un constructeur de données afin qu'il ne soit pas simplement paramétrique. Nous pouvons réécrire notre Expr
de type comme GADT comme celui - ci:
data Expr a where
IntLit :: Int -> Expr Int
BoolLit :: Bool -> Expr Bool
If :: Expr Bool -> Expr a -> Expr a -> Expr a
Ici, le type du constructeur IntLit
est Int -> Expr Int
, donc IntLit 1 :: Expr Bool
ne va pas taper.
La correspondance de modèle sur une valeur GADT entraîne un affinement du type du terme renvoyé. Par exemple, il est possible d'écrire un évaluateur pour Expr a
comme ceci:
crazyEval :: Expr a -> a
crazyEval (IntLit x) =
-- Here we can use `(+)` because x :: Int
x + 1
crazyEval (BoolLit b) =
-- Here we can use `not` because b :: Bool
not b
crazyEval (If b thn els) =
-- Because b :: Expr Bool, we can use `crazyEval b :: Bool`.
-- Also, because thn :: Expr a and els :: Expr a, we can pass either to
-- the recursive call to `crazyEval` and get an a back
crazyEval $ if crazyEval b then thn else els
Notez que nous pouvons utiliser (+)
dans les définitions ci-dessus parce que, par exemple, si IntLit x
correspond à un motif, nous apprenons aussi a ~ Int
(et de même pour not
et if_then_else_
pour a ~ Bool
).
ScopedTypeVariables
ScopedTypeVariables
vous permet de faire référence à des types universellement quantifiés dans une déclaration. Pour être plus explicite:
import Data.Monoid
foo :: forall a b c. (Monoid b, Monoid c) => (a, b, c) -> (b, c) -> (a, b, c)
foo (a, b, c) (b', c') = (a :: a, b'', c'')
where (b'', c'') = (b <> b', c <> c') :: (b, c)
L'important est que nous puissions utiliser a
, b
et c
pour instruire le compilateur dans des sous-expressions de la déclaration (le tuple dans la clause where
et le premier a
dans le résultat final). En pratique, ScopedTypeVariables
aide à écrire des fonctions complexes en tant que somme de parties, ce qui permet au programmeur d’ajouter des signatures de type à des valeurs intermédiaires qui n’ont pas de types concrets.
PatternSynonymous
Les synonymes de motif sont des abstractions de motifs similaires à la façon dont les fonctions sont des abstractions d'expressions.
Pour cet exemple, examinons l'interface que Data.Sequence
expose et voyons comment il peut être amélioré avec des synonymes de modèle. Le type Seq
est un type de données qui, en interne, utilise une représentation compliquée pour obtenir une bonne complexité asymptotique pour diverses opérations, notamment O (1) (un) consing et (un) snocing.
Mais cette représentation est lourde et certains de ses invariants ne peuvent pas être exprimés dans le système de types de Haskell. De ce fait, le type Seq
est exposé aux utilisateurs en tant que type abstrait, avec des fonctions d’accesseur et de constructeur préservant les invariants, parmi lesquelles:
empty :: Seq a
(<|) :: a -> Seq a -> Seq a
data ViewL a = EmptyL | a :< (Seq a)
viewl :: Seq a -> ViewL a
(|>) :: Seq a -> a -> Seq a
data ViewR a = EmptyR | (Seq a) :> a
viewr :: Seq a -> ViewR a
Mais utiliser cette interface peut être un peu lourd:
uncons :: Seq a -> Maybe (a, Seq a)
uncons xs = case viewl xs of
x :< xs' -> Just (x, xs')
EmptyL -> Nothing
Nous pouvons utiliser des patterns de vue pour le nettoyer quelque peu:
{-# LANGUAGE ViewPatterns #-}
uncons :: Seq a -> Maybe (a, Seq a)
uncons (viewl -> x :< xs) = Just (x, xs)
uncons _ = Nothing
En utilisant l’extension de langage PatternSynonyms
, nous pouvons donner une interface encore plus belle en autorisant l’appariement de motifs à prétendre que nous avons une liste de cons- ou de snoc:
{-# LANGUAGE PatternSynonyms #-}
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
pattern Empty :: Seq a
pattern Empty <- (Seq.viewl -> Seq.EmptyL)
pattern (:<) :: a -> Seq a -> Seq a
pattern x :< xs <- (Seq.viewl -> x Seq.:< xs)
pattern (:>) :: Seq a -> a -> Seq a
pattern xs :> x <- (Seq.viewr -> xs Seq.:> x)
Cela nous permet d’écrire des uncons
dans un style très naturel:
uncons :: Seq a -> Maybe (a, Seq a)
uncons (x :< xs) = Just (x, xs)
uncons _ = Nothing
RecordWildCards
Voir RecordWildCards