Oltre ad as-pattern, cos'altro può significare @ in Haskell?


15

Attualmente sto studiando Haskell e cerco di capire un progetto che utilizza Haskell per implementare algoritmi crittografici. Dopo aver letto Learn You a Haskell for Great Good online, inizio a capire il codice in quel progetto. Poi ho scoperto che sono bloccato al seguente codice con il simbolo "@":

-- | Generate an @n@-dimensional secret key over @rq@.
genKey :: forall rq rnd n . (MonadRandom rnd, Random rq, Reflects n Int)
       => rnd (PRFKey n rq)
genKey = fmap Key $ randomMtx 1 $ value @n

Qui randomMtx è definito come segue:

-- | A random matrix having a given number of rows and columns.
randomMtx :: (MonadRandom rnd, Random a) => Int -> Int -> rnd (Matrix a)
randomMtx r c = M.fromList r c <$> replicateM (r*c) getRandom

E PRFKey è definito di seguito:

-- | A PRF secret key of dimension @n@ over ring @a@.
newtype PRFKey n a = Key { key :: Matrix a }

Tutte le fonti di informazione che trovo dicono che @ è il modello as, ma questo pezzo di codice non è apparentemente così. Ho controllato il tutorial online, i blog e persino il rapporto sulla lingua di Haskell 2010 su https://www.haskell.org/definition/haskell2010.pdf . Semplicemente non c'è risposta a questa domanda.

Più frammenti di codice possono essere trovati in questo progetto usando anche @ in questo modo:

-- | Generate public parameters (\( \mathbf{A}_0 \) and \(
-- \mathbf{A}_1 \)) for @n@-dimensional secret keys over a ring @rq@
-- for gadget indicated by @gad@.
genParams :: forall gad rq rnd n .
            (MonadRandom rnd, Random rq, Reflects n Int, Gadget gad rq)
          => rnd (PRFParams n gad rq)
genParams = let len = length $ gadget @gad @rq
                n   = value @n
            in Params <$> (randomMtx n (n*len)) <*> (randomMtx n (n*len))

Apprezzo molto qualsiasi aiuto in merito.


11
Queste sono applicazioni di tipo . Vedi anche queste domande e risposte . Puoi anche guardare il commit che li ha introdotti nel codice.
MikaelF,

Grazie mille per i link! Questi sono esattamente ciò che sto cercando. Sorprendentemente, identifichi persino il commit del codice! Mille grazie per quello. Solo curioso di come lo trovi? @MikaelF
SigurdW,

2
Github ha la sua interfaccia per git blame , che ti dirà in quale commit ogni ultima riga è stata modificata.
MikaelF,

Grazie mille per questo utile suggerimento :)
SigurdW,

1
@MichaelLitchard Molto felice di poterne beneficiare. Sono grato alle persone gentili che passano il tempo ad aiutarmi. Spero che la risposta possa aiutare anche gli altri.
SigurdW,

Risposte:


17

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 :-)


Grazie mille per la tua spiegazione dettagliata! Risolve davvero il mio problema e immagino che avrei bisogno di molto più tempo per trovare la risposta da solo. Grazie anche per il tuo suggerimento!
SigurdW,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.