Questa @nè una funzionalità avanzata del moderno Haskell, che di solito non è coperta da tutorial come LYAH, né è possibile trovare il Rapporto.
Si chiama un'applicazione di tipo ed è un'estensione del linguaggio GHC. Per capirlo, considera questa semplice funzione polimorfica
dup :: forall a . a -> (a, a)
dup x = (x, x)
La chiamata intuitiva dupfunziona come segue:
- il chiamante sceglie un tipo
a
- il chiamante sceglie un valore
x del tipo precedentemente sceltoa
dup quindi risponde con un valore di tipo (a,a)
In un certo senso, dupaccetta due argomenti: il tipo ae il valore x :: a. Tuttavia, GHC è in genere in grado di inferire il tipo a(ad esempio dal xo dal contesto in cui stiamo usando dup), quindi di solito passiamo solo un argomento dup, vale a dire x. Ad esempio, abbiamo
dup True :: (Bool, Bool)
dup "hello" :: (String, String)
...
E se volessimo passare aesplicitamente? Bene, in quel caso possiamo attivare l' TypeApplicationsestensione e scrivere
dup @Bool True :: (Bool, Bool)
dup @String "hello" :: (String, String)
...
Nota gli @...argomenti che portano tipi (non valori). Quelli sono qualcosa che esiste al momento della compilazione, solo - in fase di esecuzione l'argomento non esiste.
Perché lo vogliamo? Bene, a volte non c'è in xgiro, e vogliamo indurre il compilatore a scegliere il giusto a. Per esempio
dup @Bool :: Bool -> (Bool, Bool)
dup @String :: String -> (String, String)
...
Le applicazioni di tipo sono spesso utili in combinazione con alcune altre estensioni che rendono impossibile l'inferenza di tipo per GHC, come tipi ambigui o famiglie di tipi. Non ne discuterò, ma puoi semplicemente capire che a volte devi davvero aiutare il compilatore, specialmente quando usi potenti funzionalità a livello di tipo.
Ora, riguardo al tuo caso specifico. Non ho tutti i dettagli, non conosco la libreria, ma è molto probabile che tu nrappresenti una sorta di valore del numero naturale a livello di tipo . Qui ci stiamo immergendo in estensioni piuttosto avanzate, come quelle sopra menzionate più DataKinds, forse GADTs, e alcuni macchinari per macchine da scrivere. Anche se non posso spiegare tutto, spero di poter fornire alcune informazioni di base. Intuitivamente,
foo :: forall n . some type using n
prende come argomento @n, una sorta di tempo di compilazione naturale, che non viene passato in fase di esecuzione. Anziché,
foo :: forall n . C n => some type using n
prende @n(tempo di compilazione), insieme a una prova che nsoddisfa il vincolo C n. Quest'ultimo è un argomento di runtime, che potrebbe esporre il valore effettivo di n. In effetti, nel tuo caso, immagino che tu abbia qualcosa di vagamente simile
value :: forall n . Reflects n Int => Int
che essenzialmente consente al codice di portare il livello di tipo naturale al livello di termine, essenzialmente accedendo al "tipo" come "valore". (Il tipo sopra è considerato "ambiguo", tra l'altro - devi davvero @nchiarire le ambiguità.)
Infine: perché dovremmo voler passare na livello di tipo se in seguito lo convertiamo al livello di termine? Non sarebbe più semplice scrivere semplicemente funzioni come
foo :: Int -> ...
foo n ... = ... use n
invece del più ingombrante
foo :: forall n . Reflects n Int => ...
foo ... = ... use (value @n)
La risposta onesta è: sì, sarebbe più facile. Tuttavia, avere na livello di tipo consente al compilatore di eseguire più controlli statici. Ad esempio, potresti voler che un tipo rappresenti "numeri interi n" e consenta di aggiungerli. avere
data Mod = Mod Int -- Int modulo some n
foo :: Int -> Mod -> Mod -> Mod
foo n (Mod x) (Mod y) = Mod ((x+y) `mod` n)
funziona, ma non v'è alcun controllo che xe ysono dello stesso modulo. Potremmo aggiungere mele e arance, se non stiamo attenti. Potremmo invece scrivere
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)
che è meglio, ma consente comunque di chiamare foo 5 x yanche quando nnon lo è 5. Non bene. Anziché,
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))
impedisce che le cose vadano male. Il compilatore controlla staticamente tutto. Il codice è più difficile da usare, sì, ma in un certo senso renderlo più difficile è il punto: vogliamo rendere impossibile per l'utente provare ad aggiungere qualcosa del modulo sbagliato.
Concludendo: si tratta di estensioni molto avanzate. Se sei un principiante, dovrai progredire lentamente verso queste tecniche. Non scoraggiarti se non riesci a coglierli dopo solo un breve studio, ci vuole del tempo. Fai un piccolo passo alla volta, risolvi alcuni esercizi per ogni funzione per capirne il punto. E avrai sempre StackOverflow quando sei bloccato :-)