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 dup
funziona 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, dup
accetta due argomenti: il tipo a
e il valore x :: a
. Tuttavia, GHC è in genere in grado di inferire il tipo a
(ad esempio dal x
o 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 a
esplicitamente? Bene, in quel caso possiamo attivare l' TypeApplications
estensione 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 x
giro, 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 n
rappresenti 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 n
soddisfa 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 @n
chiarire le ambiguità.)
Infine: perché dovremmo voler passare n
a 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 n
a 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 x
e y
sono 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 y
anche quando n
non 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 :-)