C'est @n
une fonctionnalité avancée de Haskell moderne, qui n'est généralement pas couverte par des didacticiels comme LYAH, ni le rapport.
C'est ce qu'on appelle une application de type et est une extension de langage GHC. Pour le comprendre, considérons cette simple fonction polymorphe
dup :: forall a . a -> (a, a)
dup x = (x, x)
L'appel intuitif dup
fonctionne comme suit:
- l' appelant choisit un type
a
- l' appelant choisit une valeur
x
du type précédemment choisia
dup
répond ensuite avec une valeur de type (a,a)
Dans un sens, dup
prend deux arguments: le type a
et la valeur x :: a
. Cependant, GHC est généralement capable d'inférer le type a
(par exemple à x
partir du contexte où nous l'utilisons dup
), donc nous ne transmettons généralement qu'un seul argument dup
, à savoir x
. Par exemple, nous avons
dup True :: (Bool, Bool)
dup "hello" :: (String, String)
...
Maintenant, que se passe- a
t- il si nous voulons passer explicitement? Eh bien, dans ce cas, nous pouvons activer l' TypeApplications
extension et écrire
dup @Bool True :: (Bool, Bool)
dup @String "hello" :: (String, String)
...
Notez les @...
arguments portant des types (pas des valeurs). C'est quelque chose qui existe au moment de la compilation, seulement - au moment de l'exécution, l'argument n'existe pas.
Pourquoi voulons-nous cela? Eh bien, parfois il n'y en a pas x
et nous voulons inciter le compilateur à choisir le bon a
. Par exemple
dup @Bool :: Bool -> (Bool, Bool)
dup @String :: String -> (String, String)
...
Les applications de type sont souvent utiles en combinaison avec d'autres extensions qui rendent l'inférence de type impossible pour GHC, comme les types ambigus ou les familles de types. Je n'en discuterai pas, mais vous pouvez simplement comprendre que parfois vous avez vraiment besoin d'aider le compilateur, en particulier lorsque vous utilisez de puissantes fonctionnalités au niveau du type.
Maintenant, à propos de votre cas spécifique. Je n'ai pas tous les détails, je ne connais pas la bibliothèque, mais il est très probable que votre n
représente une sorte de valeur de nombre naturel au niveau du type . Ici, nous plongons dans des extensions assez avancées, comme celles mentionnées ci-dessus plus DataKinds
, peut GADTs
- être , et certaines machines de classe. Bien que je ne puisse pas tout expliquer, j'espère pouvoir vous fournir quelques informations de base. Intuitivement,
foo :: forall n . some type using n
prend comme argument @n
, une sorte de naturel au moment de la compilation, qui n'est pas transmis au moment de l'exécution. Au lieu,
foo :: forall n . C n => some type using n
prend @n
(au moment de la compilation), avec une preuve qui n
satisfait la contrainte C n
. Ce dernier est un argument d'exécution, qui peut révéler la valeur réelle de n
. En effet, dans votre cas, je suppose que vous avez quelque chose qui ressemble vaguement
value :: forall n . Reflects n Int => Int
ce qui permet essentiellement au code d'apporter le niveau de type naturel au niveau de terme, en accédant essentiellement au "type" en tant que "valeur". (Le type ci-dessus est considéré comme "ambigu", soit dit en passant - vous devez vraiment @n
lever l' ambiguïté .)
Enfin: pourquoi devrait-on vouloir passer n
au niveau du type si nous convertissons ensuite cela au niveau du terme? Il ne serait pas plus facile d'écrire simplement des fonctions comme
foo :: Int -> ...
foo n ... = ... use n
au lieu du plus lourd
foo :: forall n . Reflects n Int => ...
foo ... = ... use (value @n)
La réponse honnête est: oui, ce serait plus facile. Cependant, avoir n
au niveau du type permet au compilateur d'effectuer des vérifications plus statiques. Par exemple, vous voudrez peut-être qu'un type représente "des entiers modulo n
" et permette d'en ajouter. Ayant
data Mod = Mod Int -- Int modulo some n
foo :: Int -> Mod -> Mod -> Mod
foo n (Mod x) (Mod y) = Mod ((x+y) `mod` n)
fonctionne, mais il n'y a pas de vérification x
et y
sont du même module. Nous pouvons ajouter des pommes et des oranges, si nous ne faisons pas attention. On pourrait plutôt écrire
data Mod n = Mod Int -- Int modulo n
foo :: Int -> Mod n -> Mod n -> Mod n
foo n (Mod x) (Mod y) = Mod ((x+y) `mod` n)
ce qui est mieux, mais permet toujours d'appeler foo 5 x y
même quand ce n
n'est pas le cas 5
. Pas bon. Au lieu,
data Mod n = Mod Int -- Int modulo n
-- a lot of type machinery omitted here
foo :: forall n . SomeConstraint n => Mod n -> Mod n -> Mod n
foo (Mod x) (Mod y) = Mod ((x+y) `mod` (value @n))
empêche les choses de mal tourner. Le compilateur vérifie statiquement tout. Le code est plus difficile à utiliser, oui, mais dans un sens, le rendre plus difficile à utiliser est tout l'intérêt: nous voulons qu'il soit impossible pour l'utilisateur d'essayer d'ajouter quelque chose du mauvais module.
Conclusion: ce sont des extensions très avancées. Si vous êtes débutant, vous devrez progresser lentement vers ces techniques. Ne vous découragez pas si vous ne pouvez pas les saisir après seulement une courte étude, cela prend du temps. Faites un petit pas à la fois, résolvez quelques exercices pour chaque fonctionnalité pour en comprendre l'intérêt. Et vous aurez toujours StackOverflow lorsque vous serez coincé :-)