Das @n
ist eine erweiterte Funktion des modernen Haskell, die in der Regel nicht durch Übungen wie Lyah abgedeckt, noch kann die den Bericht zu finden.
Es ist ein sogenannter Typ - Anwendung und ist eine GHC Spracherweiterung. Um es zu verstehen, betrachten Sie diese einfache polymorphe Funktion
dup :: forall a . a -> (a, a)
dup x = (x, x)
Das intuitive Aufrufen dup
funktioniert wie folgt:
- Der Anrufer wählt einen Typ
a
- Der Anrufer wählt einen Wert
x
des zuvor gewählten Typsa
dup
antwortet dann mit einem Wert vom Typ (a,a)
In gewissem Sinne werden dup
zwei Argumente verwendet: der Typ a
und der Wert x :: a
. GHC ist jedoch normalerweise in der Lage, den Typ abzuleiten a
(z. B. aus x
oder aus dem Kontext, in dem wir ihn verwenden dup
), sodass wir normalerweise nur ein Argument an übergeben dup
, nämlich x
. Zum Beispiel haben wir
dup True :: (Bool, Bool)
dup "hello" :: (String, String)
...
Was ist nun, wenn wir a
explizit weitergeben wollen? In diesem Fall können wir die TypeApplications
Erweiterung einschalten und schreiben
dup @Bool True :: (Bool, Bool)
dup @String "hello" :: (String, String)
...
Beachten Sie die @...
Argumente mit Typen (keine Werte). Dies ist etwas, das nur zur Kompilierungszeit existiert - zur Laufzeit existiert das Argument nicht.
Warum wollen wir das? Nun, manchmal gibt es keine x
, und wir möchten den Compiler dazu bringen, das richtige auszuwählen a
. Z.B
dup @Bool :: Bool -> (Bool, Bool)
dup @String :: String -> (String, String)
...
Typanwendungen sind häufig in Kombination mit einigen anderen Erweiterungen nützlich, die eine Typinferenz für GHC unmöglich machen, z. B. mehrdeutige Typen oder Typfamilien. Ich werde diese nicht diskutieren, aber Sie können einfach verstehen, dass Sie dem Compiler manchmal wirklich helfen müssen, insbesondere wenn Sie leistungsstarke Funktionen auf Typebene verwenden.
Nun zu Ihrem speziellen Fall. Ich habe nicht alle Details, ich kenne die Bibliothek nicht, aber es ist sehr wahrscheinlich, dass Ihr n
Wert eine Art natürlicher Zahl auf Typebene darstellt . Hier tauchen wir in ziemlich fortgeschrittenen Erweiterungen, wie den oben genannten plus DataKinds
, und vielleicht GADTs
einigen Maschinen der Typklasse. Obwohl ich nicht alles erklären kann, kann ich hoffentlich einige grundlegende Einblicke geben. Intuitiv,
foo :: forall n . some type using n
nimmt als Argument @n
eine Art Kompilierungszeit natürlich, die zur Laufzeit nicht übergeben wird. Stattdessen,
foo :: forall n . C n => some type using n
dauert @n
(Kompilierungszeit), zusammen mit einem Beweis , der n
die Bedingung erfüllt C n
. Letzteres ist ein Laufzeitargument, das den tatsächlichen Wert von verfügbar machen kann n
. In Ihrem Fall haben Sie in der Tat etwas, das vage ähnelt
value :: forall n . Reflects n Int => Int
Dies ermöglicht es dem Code im Wesentlichen, die natürliche Textebene auf die Begriffebene zu bringen und im Wesentlichen auf den "Typ" als "Wert" zuzugreifen. (Der obige Typ wird übrigens als "mehrdeutig" angesehen - Sie müssen wirklich @n
klarstellen.)
Schließlich: Warum sollte man auf Typebene bestehen wollen, n
wenn wir das später in die Termstufe umwandeln? Wäre nicht einfacher, einfach Funktionen wie zu schreiben
foo :: Int -> ...
foo n ... = ... use n
statt der umständlicheren
foo :: forall n . Reflects n Int => ...
foo ... = ... use (value @n)
Die ehrliche Antwort lautet: Ja, es wäre einfacher. Auf n
Typebene kann der Compiler jedoch mehr statische Überprüfungen durchführen. Beispielsweise möchten Sie möglicherweise, dass ein Typ "Ganzzahlen modulo n
" darstellt und diese hinzufügt. Haben
data Mod = Mod Int -- Int modulo some n
foo :: Int -> Mod -> Mod -> Mod
foo n (Mod x) (Mod y) = Mod ((x+y) `mod` n)
funktioniert, aber es gibt keine Überprüfung, dass x
und y
haben den gleichen Modul. Wir könnten Äpfel und Orangen hinzufügen, wenn wir nicht vorsichtig sind. Wir könnten stattdessen schreiben
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)
Das ist besser, erlaubt aber trotzdem anzurufen, foo 5 x y
auch wenn n
es nicht so ist 5
. Nicht gut. Stattdessen,
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))
verhindert, dass etwas schief geht. Der Compiler überprüft statisch alles. Der Code ist schwieriger zu verwenden, ja, aber in gewissem Sinne ist es der springende Punkt, die Verwendung zu erschweren: Wir möchten es dem Benutzer unmöglich machen, etwas mit dem falschen Modul hinzuzufügen.
Fazit: Dies sind sehr fortgeschrittene Erweiterungen. Wenn Sie ein Anfänger sind, müssen Sie sich langsam diesen Techniken nähern. Lassen Sie sich nicht entmutigen, wenn Sie sie nach nur einem kurzen Studium nicht erfassen können. Es dauert einige Zeit. Machen Sie jeweils einen kleinen Schritt und lösen Sie einige Übungen für jede Funktion, um den Sinn zu verstehen. Und du wirst immer StackOverflow haben, wenn du feststeckst :-)