Że @n
to zaawansowana funkcja współczesnego Haskell, która zwykle nie jest objęta samouczkami takimi jak LYAH, ani nie można znaleźć w Raporcie.
Nazywa się to aplikacją typu i jest rozszerzeniem języka GHC. Aby to zrozumieć, rozważ tę prostą funkcję polimorficzną
dup :: forall a . a -> (a, a)
dup x = (x, x)
Intuicyjne wywoływanie dup
działa w następujący sposób:
- rozmówca wybiera typ
a
- wywołujący wybiera wartości
x
uprzednio wybranym typema
dup
następnie odpowiada wartością typu (a,a)
W pewnym sensie dup
przyjmuje dwa argumenty: typ a
i wartość x :: a
. Jednak GHC jest zwykle w stanie wywnioskować typ a
(np. Z x
lub z kontekstu, w którym używamy dup
), więc zwykle przekazujemy tylko jeden argument dup
, mianowicie x
. Na przykład mamy
dup True :: (Bool, Bool)
dup "hello" :: (String, String)
...
Co teraz, jeśli chcemy przekazać a
jawnie? W takim przypadku możemy włączyć TypeApplications
rozszerzenie i napisać
dup @Bool True :: (Bool, Bool)
dup @String "hello" :: (String, String)
...
Zwróć uwagę na @...
argumenty niosące typy (nie wartości). Są to coś, co istnieje tylko w czasie kompilacji - w czasie wykonywania argument nie istnieje.
Dlaczego tego chcemy? Czasami nie ma go w x
pobliżu i chcemy zmusić kompilator do wybrania właściwego a
. Na przykład
dup @Bool :: Bool -> (Bool, Bool)
dup @String :: String -> (String, String)
...
Aplikacje typu są często przydatne w połączeniu z niektórymi innymi rozszerzeniami, które sprawiają, że wnioskowanie typu jest niemożliwe dla GHC, takie jak typy niejednoznaczne lub rodziny typów. Nie będę ich omawiać, ale możesz po prostu zrozumieć, że czasami naprawdę potrzebujesz pomocy kompilatorowi, szczególnie gdy używasz zaawansowanych funkcji na poziomie tekstu.
Teraz o twoim konkretnym przypadku. Nie znam wszystkich szczegółów, nie znam biblioteki, ale jest bardzo prawdopodobne, że n
reprezentujesz rodzaj wartości liczb naturalnych na poziomie typu . Tutaj nurkujemy w raczej zaawansowanych rozszerzeniach, takich jak wyżej wymienione, a DataKinds
może GADTs
i w niektórych maszynach typowych. Chociaż nie potrafię wyjaśnić wszystkiego, mam nadzieję, że mogę zapewnić podstawowy wgląd. Intuicyjnie,
foo :: forall n . some type using n
przyjmuje za argument @n
rodzaj naturalnego czasu kompilacji, który nie jest przekazywany w czasie wykonywania. Zamiast,
foo :: forall n . C n => some type using n
zajmuje @n
(czas kompilacji), wraz z dowodem na ton
spełnia Wiązanie C n
. Ten ostatni argument jest argumentem wykonawczym, który może ujawnić rzeczywistą wartość n
. Rzeczywiście, w twoim przypadku wydaje mi się, że masz coś niejasnego
value :: forall n . Reflects n Int => Int
co zasadniczo umożliwia kodowi przeniesienie poziomu typu naturalnego na poziom terminu, zasadniczo uzyskując dostęp do „typu” jako „wartości”. (Nawiasem mówiąc, powyższy typ jest uważany za „dwuznaczny” - naprawdę potrzebujesz@n
go ujednoznacznić).
Wreszcie: dlaczego warto przejść n
na poziomie typu, jeśli później przekonwertujemy to na poziom terminu? Nie byłoby łatwiej po prostu napisać takie funkcje jak
foo :: Int -> ...
foo n ... = ... use n
zamiast bardziej uciążliwego
foo :: forall n . Reflects n Int => ...
foo ... = ... use (value @n)
Szczera odpowiedź brzmi: tak, byłoby łatwiej. Jednak posiadanie n
na poziomie typu pozwala kompilatorowi przeprowadzać więcej kontroli statycznych. Na przykład możesz chcieć, aby typ reprezentował „liczby całkowite modulon
” i pozwolił na ich dodawanie. Mający
data Mod = Mod Int -- Int modulo some n
foo :: Int -> Mod -> Mod -> Mod
foo n (Mod x) (Mod y) = Mod ((x+y) `mod` n)
działa, ale nie ma wyboru, że x
iy
mają ten sam moduł. Jeśli nie będziemy ostrożni, możemy dodać jabłka i pomarańcze. Zamiast tego moglibyśmy pisać
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)
co jest lepsze, ale nadal pozwala dzwonić, foo 5 x y
nawet jeśli n
nie jest5
. Niedobrze. Zamiast,
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))
zapobiega błędom. Kompilator sprawdza wszystko statycznie. Kod jest trudniejszy w użyciu, tak, ale w pewnym sensie utrudnienie w użyciu jest sednem: chcemy, aby użytkownik nie mógł spróbować dodać czegoś o złym module.
Podsumowując: są to bardzo zaawansowane rozszerzenia. Jeśli jesteś początkujący, musisz powoli robić postępy w kierunku tych technik. Nie zniechęcaj się, jeśli nie możesz ich pojąć po krótkim studiu, zajmuje to trochę czasu. Zrób mały krok po kroku, rozwiąż kilka ćwiczeń dla każdej funkcji, aby zrozumieć jej sens. I zawsze będziesz mieć StackOverflow, gdy utkniesz :-)