Haskell Language
Wpisz aplikację
Szukaj…
Wprowadzenie
TypeApplications
to alternatywa dla adnotacji typu, gdy kompilator ma problemy z wnioskowaniem typów dla danego wyrażenia.
Ta seria przykładów wyjaśni cel rozszerzenia TypeApplications
i sposób jego użycia
Nie zapomnij włączyć rozszerzenia, umieszczając {-# LANGUAGE TypeApplications #-}
u góry pliku źródłowego.
Unikanie adnotacji typu
Używamy adnotacji typu, aby uniknąć dwuznaczności. Aplikacje typu mogą być używane w tym samym celu. Na przykład
x :: Num a => a
x = 5
main :: IO ()
main = print x
W kodzie występuje błąd niejednoznaczności. Wiemy, że a
ma instancję Num
, i aby ją wydrukować, wiemy, że potrzebuje instancji Show
. To może działać, jeśli było, na przykład a
Int
, tak aby naprawić błąd możemy dodać typ adnotacji
main = print (x :: Int)
Inne rozwiązanie wykorzystujące aplikacje typu wyglądałoby tak
main = print @Int x
Aby zrozumieć, co to oznacza, musimy spojrzeć na podpis typu print
.
print :: Show a => a -> IO ()
Funkcja przyjmuje jeden parametr typu a
, ale można na to spojrzeć inaczej, ponieważ w rzeczywistości zajmuje dwa parametry. Pierwszy to parametr typu , drugi to wartość, której typ jest pierwszym parametrem.
Główną różnicą między parametrami wartości a parametrami typu jest to, że te ostatnie są domyślnie dostarczane do funkcji, gdy je wywołujemy. Kto je zapewnia? Algorytm wnioskowania typu! To, TypeApplications
pozwalają nam TypeApplications
, to jawne podanie tych parametrów typu. Jest to szczególnie przydatne, gdy wnioskowanie o typie nie może określić poprawnego typu.
Aby rozbić powyższy przykład
print :: Show a => a -> IO ()
print @Int :: Int -> IO ()
print @Int x :: IO ()
Wpisz aplikacje w innych językach
Jeśli znasz języki takie jak Java, C # lub C ++ i pojęcie ogólne / szablony, to porównanie może być dla Ciebie przydatne.
Powiedzmy, że mamy ogólną funkcję w C #
public static T DoNothing<T>(T in) { return in; }
Aby wywołać tę funkcję za pomocą DoNothing(5.0f)
float
, możemy zrobić DoNothing(5.0f)
lub, jeśli chcemy być DoNothing(5.0f)
, możemy powiedzieć DoNothing<float>(5.0f)
. Ta część wewnątrz nawiasów kątowych to aplikacja typu.
W Haskell jest tak samo, z tym wyjątkiem, że parametry typu są niejawne nie tylko w witrynach wywołań, ale także w witrynach definicji.
doNothing :: a -> a
doNothing x = x
Można to również jednoznacznie określić za pomocą takich rozszerzeń ScopedTypeVariables
, Rank2Types
lub RankNTypes
.
doNothing :: forall a. a -> a
doNothing x = x
Następnie na stronie połączeń możemy ponownie napisać doNothing 5.0
lub doNothing @Float 5.0
Kolejność parametrów
Problem z niejawnymi argumentami typu staje się oczywisty, gdy mamy więcej niż jeden. W jakiej kolejności są?
const :: a -> b -> a
Czy zapisanie const @Int
oznacza, że a
jest równe Int
, czy jest to b
? W przypadku, gdy jawnie określamy parametry typu za pomocą forall
takiego jak const :: forall a b. a -> b -> a
a następnie kolejność jest zapisana: a
, a następnie b
.
Jeśli nie, to kolejność zmiennych jest od lewej do prawej. Pierwsza zmienna, o której należy wspomnieć, to parametr pierwszego typu, druga to parametr drugiego typu i tak dalej.
Co jeśli chcemy określić zmienną drugiego typu, ale nie pierwszą? Możemy użyć symbolu wieloznacznego dla pierwszej takiej zmiennej
const @_ @Int
Typ tego wyrażenia to
const @_ @Int :: a -> Int -> a
Interakcja z niejednoznacznymi typami
Załóżmy, że wprowadzasz klasę typów o rozmiarze w bajtach.
class SizeOf a where
sizeOf :: a -> Int
Problem polega na tym, że rozmiar powinien być stały dla każdej wartości tego typu. Nie faktycznie chcesz sizeOf
funkcja zależy od , ale tylko na jego rodzaj. a
Bez aplikacji typu najlepszym rozwiązaniem, jakie mieliśmy, był typ Proxy
zdefiniowany w ten sposób
data Proxy a = Proxy
Celem tego typu jest przenoszenie informacji o typie, ale bez informacji o wartości. Wtedy nasza klasa mogłaby wyglądać tak
class SizeOf a where
sizeOf :: Proxy a -> Int
Teraz możesz się zastanawiać, dlaczego nie porzucić pierwszego argumentu? Typem naszej funkcji byłby więc po prostu sizeOf :: Int
lub, ściślej mówiąc, ponieważ jest to metoda klasy, sizeOf :: SizeOf a => Int
lub jeszcze bardziej sizeOf :: forall a. SizeOf a => Int
.
Problemem jest wnioskowanie typu. Jeśli piszę gdzieś sizeOf
, algorytm wnioskowania wie tylko, że oczekuję Int
. To nie ma pojęcia jaki typ chcę zastąpić . a
Z tego powodu kompilator odrzuca definicję, chyba że włączone jest rozszerzenie {-# LANGUAGE AllowAmbiguousTypes #-}
. W takim przypadku definicja się kompiluje, po prostu nie można jej użyć nigdzie bez błędu niejednoznaczności.
Na szczęście wprowadzenie aplikacji typu oszczędza dzień! Teraz możemy napisać sizeOf @Int
, wyraźnie mówiąc, że a
jest Int
. Aplikacje typu pozwalają nam podać parametr typu, nawet jeśli nie pojawia się on w rzeczywistych parametrach funkcji !