Haskell Language
Type d'application
Recherche…
Introduction
TypeApplications
est une alternative à la TypeApplications
des annotations lorsque le compilateur a du mal à déduire des types pour une expression donnée.
Cette série d'exemples expliquera le but de l'extension TypeApplications
et son utilisation
N'oubliez pas d'activer l'extension en plaçant {-# LANGUAGE TypeApplications #-}
en haut de votre fichier source.
Éviter les annotations de type
Nous utilisons des annotations de type pour éviter toute ambiguïté. Les applications de type peuvent être utilisées dans le même but. Par exemple
x :: Num a => a
x = 5
main :: IO ()
main = print x
Ce code a une erreur d'ambiguïté. Nous savons que a
a un Num
exemple, et pour l' imprimer nous savons qu'il a besoin d' un Show
par exemple. Cela pourrait fonctionner si a
était, par exemple, un Int
, afin de corriger l'erreur, nous pouvons ajouter une annotation de type
main = print (x :: Int)
Une autre solution utilisant des applications de type ressemblerait à ceci
main = print @Int x
Pour comprendre ce que cela signifie, nous devons examiner le type de signature d’ print
.
print :: Show a => a -> IO ()
La fonction prend un paramètre de type a
, mais une autre façon de le voir est de prendre deux paramètres. Le premier est un paramètre de type , le second est une valeur dont le type est le premier paramètre.
La principale différence entre les paramètres de valeur et les paramètres de type est que ces derniers sont implicitement fournis aux fonctions lorsque nous les appelons. Qui les fournit? L'algorithme d'inférence de type! Ce que TypeApplications
nous permet de faire est de donner explicitement ces paramètres de type. Ceci est particulièrement utile lorsque l'inférence de type ne peut pas déterminer le type correct.
Donc, pour briser l'exemple ci-dessus
print :: Show a => a -> IO ()
print @Int :: Int -> IO ()
print @Int x :: IO ()
Tapez les applications dans d'autres langues
Si vous êtes familier avec des langages comme Java, C # ou C ++ et le concept de génériques / modèles, cette comparaison pourrait vous être utile.
Disons que nous avons une fonction générique en C #
public static T DoNothing<T>(T in) { return in; }
Pour appeler cette fonction avec un float
nous pouvons faire DoNothing(5.0f)
ou si nous voulons être explicites, nous pouvons dire DoNothing<float>(5.0f)
. Cette partie à l'intérieur des crochets est l'application de type.
Dans Haskell, c'est la même chose, sauf que les paramètres de type ne sont pas seulement implicites aux sites d'appel, mais également aux sites de définition.
doNothing :: a -> a
doNothing x = x
Cela peut également être rendu explicite en utilisant soit les ScopedTypeVariables
, Rank2Types
ou RankNTypes
comme ceci.
doNothing :: forall a. a -> a
doNothing x = x
Ensuite, sur le site d’appel, nous pouvons à nouveau écrire doNothing 5.0
ou doNothing @Float 5.0
Ordre des paramètres
Le problème des arguments de type implicites devient évident lorsque nous en avons plus d’un. De quel ordre viennent-ils?
const :: a -> b -> a
Est-ce que l'écriture de const @Int
signifie a
est égal à Int
ou est-ce b
? Dans le cas où nous déclarons explicitement les paramètres de type en utilisant un forall
comme const :: forall a b. a -> b -> a
alors l'ordre est comme écrit: a
, alors b
.
Si nous ne le faisons pas, l'ordre des variables est de gauche à droite. La première variable à mentionner est le premier paramètre de type, le second est le paramètre de deuxième type, etc.
Que faire si nous voulons spécifier la variable de deuxième type, mais pas la première? Nous pouvons utiliser un caractère générique pour la première variable comme celle-ci
const @_ @Int
Le type de cette expression est
const @_ @Int :: a -> Int -> a
Interaction avec des types ambigus
Disons que vous introduisez une classe de types qui ont une taille en octets.
class SizeOf a where
sizeOf :: a -> Int
Le problème est que la taille doit être constante pour chaque valeur de ce type. Nous ne voulons pas réellement que la fonction sizeOf
dépende de a
, mais seulement de son type.
Sans les applications de type, la meilleure solution était le type de Proxy
défini comme ceci
data Proxy a = Proxy
Le but de ce type est de transporter des informations de type, mais aucune information de valeur. Alors notre classe pourrait ressembler à ceci
class SizeOf a where
sizeOf :: Proxy a -> Int
Maintenant, vous vous demandez peut-être pourquoi ne pas laisser tomber le premier argument? Le type de notre fonction serait alors simplement sizeOf :: Int
ou, pour être plus précis, parce que c'est une méthode d'une classe, sizeOf :: SizeOf a => Int
ou pour être encore plus explicite sizeOf :: forall a. SizeOf a => Int
.
Le problème est l'inférence de type. Si j'écris quelque part sizeOf
, l'algorithme d'inférence sait seulement que j'attends un Int
. Il n'a aucune idée de quel type je veux substituer à a
. De ce fait, la définition est rejetée par le compilateur à moins que l'extension {-# LANGUAGE AllowAmbiguousTypes #-}
activée. Dans ce cas, la définition est compilée, elle ne peut être utilisée nulle part sans erreur d'ambiguïté.
Heureusement, l'introduction d'applications de type sauve la journée! Maintenant, nous pouvons écrire sizeOf @Int
, en disant explicitement que a
est Int
. Les applications de type nous permettent de fournir un paramètre de type, même s'il n'apparaît pas dans les paramètres réels de la fonction !