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 !



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow