Załóżmy, że chcę napisać bibliotekę, która zajmuje się wektorami i macierzami. Czy można upiec wymiary na typy, aby operacje niezgodnych wymiarów generowały błąd w czasie kompilacji?
Na przykład chciałbym, aby podpis produktu kropkowego był podobny
dotprod :: Num a, VecDim d => Vector a d -> Vector a d -> a
gdzie d
typ zawiera jedną wartość całkowitą (reprezentującą wymiar tych wektorów).
Przypuszczam, że można to zrobić, definiując (ręcznie) osobny typ dla każdej liczby całkowitej i grupując je w klasie typów o nazwie VecDim
. Czy istnieje jakiś mechanizm „generowania” takich typów?
A może jakiś lepszy / prostszy sposób na osiągnięcie tego samego?
haskell
type-systems
type-safety
Mitchus
źródło
źródło
tensor
biblioteka osiąga to dość elegancko, używając rekurencyjnejdata
definicji: noaxiom.org/tensor-documentation#ordinalsOdpowiedzi:
Aby rozwinąć odpowiedź @ KarlBielefeldt, oto pełny przykład implementacji wektorów - list ze statycznie znaną liczbą elementów - w Haskell. Trzymaj kapelusz ...
Jak widać z długiej listy
LANGUAGE
dyrektyw, będzie to działać tylko z najnowszą wersją GHC.Potrzebujemy sposobu reprezentowania długości w systemie typów. Z definicji liczba naturalna to zero (
Z
) lub następca innej liczby naturalnej (S n
). Na przykład zapisano by liczbę 3S (S (S Z))
.Wraz z rozszerzeniem DataKinds ta
data
deklaracja wprowadza wywoływany rodzajNat
i konstruktory dwóch typów,S
iZ
- innymi słowy, mamy liczby naturalne na poziomie typu . Zauważ, że typyS
iZ
nie mają żadnych wartości składowych - tylko typy rodzajów*
są zamieszkane przez wartości.Teraz wprowadzamy GADT reprezentujący wektory o znanej długości. Zwróć uwagę na rodzaj podpisu:
Vec
wymaga rodzajuNat
(np. AZ
lubS
typu) do przedstawienia jego długości.Definicja wektorów jest podobna do definicji połączonych list, z pewnymi dodatkowymi informacjami na temat typu o jej długości. Wektor jest albo
VNil
, w którym to przypadku ma długośćZ
(ero), albo jestVCons
komórką dodającą element do innego wektora, w którym to przypadku jego długość jest o jeden większa niż drugiego wektora (S n
). Zauważ, że nie ma argumentu typu konstruktorn
. Jest on po prostu używany w czasie kompilacji do śledzenia długości i zostanie usunięty, zanim kompilator wygeneruje kod maszynowy.Zdefiniowaliśmy typ wektora, który niesie statyczną wiedzę o jego długości. Zapytajmy kilka typów,
Vec
aby dowiedzieć się, jak działają:Produkt kropkowy działa tak samo, jak w przypadku listy:
vap
, która „pospiesznie” stosuje wektor funkcji do wektora argumentów, maVec
zastosowanie<*>
; Nie umieściłem tego wApplicative
instancji, ponieważ robi się bałagan . Zauważ też, że korzystamfoldr
z instancji generowanej przez kompilatorFoldable
.Wypróbujmy to:
Świetny! Podczas kompilacji
dot
wektorów, których długości się nie zgadzają, pojawia się błąd czasu kompilacji .Oto próba funkcji łączenia wektorów razem:
Długość wektora wyjściowego byłaby sumą długości dwóch wektorów wejściowych. Musimy nauczyć moduł sprawdzania typu, jak dodawać
Nat
s razem. W tym celu używamy funkcji na poziomie typu :Ta
type family
deklaracja wprowadza funkcję wywoływanych typów:+:
- innymi słowy, jest to przepis na sprawdzanie typów w celu obliczenia sumy dwóch liczb naturalnych. Jest definiowany rekurencyjnie - ilekroć lewy operand jest większy niżZ
ero, dodajemy go do wyniku i zmniejszamy o jeden w wywołaniu rekurencyjnym. (Dobrym ćwiczeniem jest napisanie funkcji typu, która zwielokrotnia dwieNat
s.) Teraz możemy dokonać+++
kompilacji:Oto jak z niego korzystasz:
Jak dotąd tak proste. A jeśli chcemy zrobić coś przeciwnego do konkatenacji i podzielić wektor na dwie części? Długości wektorów wyjściowych zależą od wartości czasu wykonania argumentów. Chcielibyśmy napisać coś takiego:
ale niestety Haskell nam na to nie pozwoli. Zezwolenie na pojawienie się wartości
n
argumentu w typie zwracanym (jest to zwykle nazywane funkcją zależną lub typem pi ) wymagałoby typów zależnych od „pełnego spektrum”, aDataKinds
jedynie zapewniało nam konstruktory typów promowanych. Innymi słowy, wpisz konstruktoryS
iZ
nie pojawiają się na poziomie wartości. Będziemy musieli zadowolić się wartościami singletonowymi dla reprezentacji określonych w czasie wykonywaniaNat
*.Dla danego typu
n
(z rodzajemNat
) istnieje dokładnie jeden termin typuNatty n
. Możemy wykorzystać wartość singletonu jako świadka w czasie wykonywanian
: zdobycie wiedzy o niejNatty
uczy nasn
i na odwrót.Weźmy to na spin:
W pierwszym przykładzie z powodzeniem podzieliliśmy wektor trzyelementowy na pozycji 2; wtedy wystąpił błąd typu, gdy próbowaliśmy rozdzielić wektor w miejscu za końcem. Singletony to standardowa technika uzależniania typu od wartości w Haskell.
*
singletons
Biblioteka zawiera pomocników Template Haskell do generowania wartości singletonów, takich jakNatty
dla Ciebie.Ostatni przykład A co, jeśli nie znasz statystycznie wymiaru swojego wektora? Na przykład, co jeśli staramy się zbudować wektor z danych wykonawczych w postaci listy? Potrzebujesz typu wektora, aby zależeć od długości listy danych wejściowych. Innymi słowy, nie możemy użyć
foldr VCons VNil
do zbudowania wektora, ponieważ rodzaj wektora wyjściowego zmienia się z każdą iteracją zagięcia. Musimy zachować tajemnicę długości wektora przed kompilatorem.AVec
jest typem egzystencjalnym : zmienna typun
nie pojawia się w typie zwracanymAVec
konstruktora danych. Używamy go do symulacji pary zależnej :fromList
nie potrafimy określić statycznie długości wektora, ale może zwrócić coś, co możesz dopasować do wzorca, aby dowiedzieć się o długości wektora -Natty n
w pierwszym elemencie krotki . Jak Conor McBride odpowiada na to pytanie : „Patrzysz na jedną rzecz, a robiąc to, dowiadujesz się o innej”.Jest to powszechna technika dla egzystencjalnie kwantyfikowanych typów. Ponieważ w rzeczywistości nie możesz nic zrobić z danymi, dla których nie znasz typu - spróbuj napisać funkcję
data Something = forall a. Sth a
- egzystencjalne często są dostarczane w pakiecie z dowodami GADT, które pozwalają odzyskać oryginalny typ przez wykonanie testów dopasowania wzorca. Inne typowe wzorce egzystencjalne obejmują pakowanie funkcji w celu przetworzenia typu (data AWayToGetTo b = forall a. HeresHow a (a -> b)
), co jest zgrabnym sposobem wykonywania modułów pierwszej klasy, lub wbudowywanie słownika klas typów (data AnOrd = forall a. Ord a => AnOrd a
), który może pomóc naśladować polimorfizm podtypu.Pary zależne są przydatne, gdy statyczne właściwości danych zależą od informacji dynamicznych niedostępnych w czasie kompilacji. Oto
filter
wektory:Do
dot
dwóchAVec
sekund musimy udowodnić GHC, że ich długości są równe.Data.Type.Equality
definiuje GADT, który można zbudować tylko wtedy, gdy jego argumenty typu są takie same:Kiedy włączasz dopasowanie wzoru
Refl
, GHC wie o tyma ~ b
. Istnieje również kilka funkcji ułatwiających pracę z tym typem: będziemy używaćgcastWith
do konwersji między równoważnymi typami iTestEquality
do ustalenia, czy dwaNatty
s są równe.Aby przetestować równość dwóch
Natty
s, jedziemy do konieczności skorzystać z faktu, że jeśli dwie liczby są równe, to ich następcy są także równe (:~:
jest przystający nadS
):Dopasowywanie wzorów po
Refl
lewej stronie informuje GHC o tymn ~ m
. Przy tej wiedzy jest to banalneS n ~ S m
, więc GHC pozwala namRefl
od razu zwrócić nowy .Teraz możemy napisać instancję
TestEquality
poprzez prostą rekurencję. Jeśli obie liczby są równe zero, są równe. Jeśli obie liczby mają poprzedników, są one równe, jeśli poprzedniki są równe. (Jeśli nie są równe, po prostu wróćNothing
.)Teraz możemy poskładać elementy
dot
w paręAVec
o nieznanej długości.Po pierwsze, dopasowanie wzorca na
AVec
konstruktorze w celu wyciągnięcia reprezentacji wykonawczej długości wektorów. Teraz użyj,testEquality
aby ustalić, czy te długości są równe. Jeśli tak, będziemy mieliJust Refl
;gcastWith
użyje tego dowodu równości, aby upewnić się, żedot u v
jest dobrze napisany, wypełniając swoje domniemanen ~ m
założenie.Zauważ, że ponieważ wektor bez statycznej wiedzy o jego długości jest w zasadzie listą, skutecznie ponownie wdrożyliśmy wersję listy
dot :: Num a => [a] -> [a] -> Maybe a
. Różnica polega na tym, że ta wersja jest implementowana w kategoriach wektorówdot
. Oto punkt: zanim moduł sprawdzania typu pozwoli ci zadzwonićdot
, musisz przetestować, czy listy wejściowe mają taką samą długośćtestEquality
. Mam skłonność do otrzymywaniaif
-odpowiedzi w niewłaściwy sposób, ale nie w zależności od typowania!Nie można uniknąć używania otoki egzystencjalnej na krawędziach systemu, gdy mamy do czynienia z danymi środowiska wykonawczego, ale można używać typów zależnych w całym systemie i utrzymywać otoki egzystencjalne na krawędziach podczas sprawdzania poprawności danych wejściowych.
Ponieważ
Nothing
nie jest to zbyt pouczające, można dodatkowo sprecyzować rodzajdot'
zwracania dowodu, że długości nie są równe (w postaci dowodu, że ich różnica nie wynosi 0) w przypadku awarii. Jest to dość podobne do standardowej techniki Haskella polegającejEither String a
na ewentualnym zwróceniu komunikatu o błędzie, chociaż termin sprawdzający jest znacznie bardziej użyteczny obliczeniowo niż ciąg znaków!W ten sposób kończy się prezentacja niektórych technik, które są powszechne w programowaniu Haskell o typie zależnym. Programowanie z takimi typami w Haskell jest naprawdę fajne, ale jednocześnie bardzo niewygodne. Podział wszystkich zależnych danych na wiele reprezentacji, co oznacza to samo -
Nat
typ,Nat
rodzaj,Natty n
singleton - jest naprawdę dość kłopotliwy, pomimo istnienia generatorów kodów, które pomagają w tworzeniu podstaw. Obecnie istnieją również ograniczenia dotyczące tego, co można awansować do poziomu typu. Ale to kuszące! Umysł zastanawia się nad możliwościami - w literaturze są przykłady w Haskell silnie typowanychprintf
, interfejsów baz danych, silników układu interfejsu użytkownika ...Jeśli chcesz trochę poczytać, pojawia się coraz więcej literatury na temat Haskell, który jest zależnie wpisany, zarówno opublikowany, jak i na stronach takich jak Stack Overflow. Dobrym punktem wyjścia jest artykuł hasochizm - artykuł ten omawia ten przykład (między innymi), szczegółowo omawiając bolesne części. Singleton'y papieru pokazuje technikę pojedynczych wartości (na przykład ). Więcej informacji na temat pisania zależnego można znaleźć w samouczku Agda ; Ponadto Idris to opracowywany język (z grubsza) zaprojektowany jako „Haskell z typami zależnymi”.
Natty
źródło
Nazywa się to pisaniem zależnym . Gdy poznasz nazwę, możesz znaleźć na niej więcej informacji, niż możesz sobie wyobrazić. Istnieje również interesujący język haskellowy o nazwie Idris, który używa ich natywnie. Jego autor zrobił kilka naprawdę dobrych prezentacji na ten temat, które można znaleźć na youtube.
źródło
newtype Vec2 a = V2 (a,a)
,newtype Vec3 a = V3 (a,a,a)
i tak dalej, ale nie o to pytanie z prośbą.)Pi (x : A). B
, które jest funkcją odA
celuB x
, gdziex
jest argumentem funkcji. W tym przypadku typ zwracanej funkcji zależy od wyrażenia podanego jako argument. Jednak wszystko to można usunąć, to tylko czas kompilacji