Czy ktoś może mi wyjaśnić pisanie na klawiaturze zależnej? Mam niewielkie doświadczenie w języku Haskell, Cayenne, Epigram lub innych językach funkcjonalnych, więc im prostszych terminów użyjesz, tym bardziej to docenię!
functional-programming
dependent-type
Nacięcie
źródło
źródło
Odpowiedzi:
Rozważ to: we wszystkich przyzwoitych językach programowania możesz pisać funkcje, np
Tutaj
f
przyjmuje wartośćarg
i oblicza wartośćresult
. Jest to funkcja od wartości do wartości.Teraz niektóre języki umożliwiają definiowanie wartości polimorficznych (czyli ogólnych):
Tutaj
empty
przyjmuje typT
i oblicza wartość. Jest to funkcja od typów do wartości.Zwykle możesz również mieć definicje typów ogólnych:
Ta definicja przyjmuje typ i zwraca typ. Można go postrzegać jako funkcję od typów do typów.
To tyle, jeśli chodzi o to, co oferują zwykłe języki. Język nazywany jest typem zależnym, jeśli oferuje również czwartą możliwość, mianowicie definiowanie funkcji od wartości do typów. Innymi słowy, parametryzacja definicji typu względem wartości:
Niektóre języki głównego nurtu mają jakąś fałszywą formę, której nie należy mylić. Np. W C ++ szablony mogą przyjmować wartości jako parametry, ale po zastosowaniu muszą być stałymi czasu kompilacji. Inaczej jest w przypadku prawdziwie zależnego języka. Na przykład mógłbym użyć powyższego typu w ten sposób:
W tym przypadku typ wyniku funkcji zależy od rzeczywistej wartości argumentu
j
, a więc od terminologii.źródło
BoundedInt
przykład nie jest w rzeczywistości typem uściślenia? To jest „całkiem blisko”, ale nie jest to dokładnie ten rodzaj „typów zależnych”, o których np. Idris wspomina jako pierwszy w samouczku dotyczącym składania.Zależne typy umożliwiają wyeliminowanie większego zestawu błędów logicznych w czasie kompilacji . Aby to zilustrować, rozważ następującą specyfikację funkcji
f
:Bez typów zależnych możesz zrobić coś takiego:
W tym przypadku kompilator nie może wykryć, czy
n
rzeczywiście jest parzysty, to znaczy z punktu widzenia kompilatora następujące wyrażenie jest w porządku:Ten program uruchomi się, a następnie zgłosi wyjątek w czasie wykonywania, co oznacza, że program ma błąd logiczny.
Teraz typy zależne pozwalają na znacznie większą ekspresję i pozwolą napisać coś takiego:
Tutaj
n
jest zależny typ{n: Integer | n mod 2 == 0}
. Pomocne może być przeczytanie tego na głos jakoW tym przypadku kompilator wykryłby w czasie kompilacji błąd logiczny, do którego przekazałeś nieparzystą liczbę
f
i uniemożliwiłby wykonanie programu w pierwszej kolejności:Oto ilustrujący przykład wykorzystujący typy zależne od ścieżki Scala, w jaki sposób możemy próbować zaimplementować funkcję
f
spełniającą takie wymaganie:case class Integer(v: Int) { object IsEven { require(v % 2 == 0) } object IsOdd { require(v % 2 != 0) } } def f(n: Integer)(implicit proof: n.IsEven.type) = { // do something with n safe in the knowledge it is even } val `42` = Integer(42) implicit val proof42IsEven = `42`.IsEven val `1` = Integer(1) implicit val proof1IsOdd = `1`.IsOdd f(`42`) // OK f(`1`) // compile-time error
Kluczem jest zwrócenie uwagi na to, jak wartość
n
pojawia się w typie wartości,proof
a mianowicien.IsEven.type
:Mówimy, że typ
n.IsEven.type
zależy od wartości,n
stąd termin typy zależne .źródło
f(random())
błąd kompilacji?f
do jakiegoś wyrażenia wymagałoby od kompilatora (z twoją pomocą lub bez) zapewnienia, że wyrażenie jest zawsze parzyste i nie ma takiego dowodurandom()
(ponieważ w rzeczywistości może być dziwne), dlategof(random())
kompilacja nie powiodłaby się.Jeśli znasz C ++, możesz łatwo podać motywujący przykład:
Powiedzmy, że mamy jakiś typ kontenera i jego dwie instancje
typedef std::map<int,int> IIMap; IIMap foo; IIMap bar;
i rozważ ten fragment kodu (możesz założyć, że foo nie jest puste):
Jest to oczywiste śmieci (i prawdopodobnie psuje struktury danych), ale sprawdzanie typu będzie dobre, ponieważ „iterator do foo” i „iterator do baru” są tego samego typu
IIMap::iterator
, nawet jeśli są całkowicie niekompatybilne semantycznie.Problem w tym, że typ iteratora nie powinien zależeć tylko od typu kontenera, ale w rzeczywistości od obiektu kontenera , czyli powinien być „niestatycznym typem składowym”:
foo.iterator i = foo.begin(); bar.erase(i); // ERROR: bar.iterator argument expected
Taka cecha, możliwość wyrażenia typu (foo.iterator) zależnego od terminu (foo), jest dokładnie tym, co oznacza typ zależny.
Powodem, dla którego rzadko widzisz tę funkcję, jest to, że otwiera ona dużą puszkę robaków: nagle kończysz w sytuacjach, w których, aby sprawdzić w czasie kompilacji, czy dwa typy są takie same, musisz udowodnić dwa wyrażenia są równoważne (zawsze dają tę samą wartość w czasie wykonywania). W rezultacie, jeśli porównasz listę języków zależnie wpisywanych w Wikipedii z listą potwierdzających twierdzenia , możesz zauważyć podejrzane podobieństwo. ;-)
źródło
Cytując książkę Typy i języki programowania (30.5):
źródło