Polimorfizm wyższego rzędu w porównaniu z typami nieopakowanymi

10

Mam język, w którym typy są domyślnie rozpakowane, a wnioskowanie typu oparte jest na Hindley-Milner. Chciałbym dodać polimorfizm wyższego rzędu, głównie do pracy z typami egzystencjalnymi.

Wydaje mi się, że rozumiem, jak sprawdzić te typy, ale nie jestem pewien, co robić podczas kompilacji. Obecnie kompiluję definicje polimorficzne, generując specjalizacje, podobnie jak szablony C ++, dzięki czemu mogą one pracować z wartościami rozpakowanymi. Na przykład, biorąc pod uwagę definicję f<T>, jeśli wywołuje tylko programowe f<Int32>i f<Char>, następnie pojawiają się tylko te specjalizacje w skompilowanym programie. (Na razie zakładam kompilację całego programu).

Ale przekazując funkcję polimorficzną jako argument, nie widzę, w jaki sposób mogę wygenerować odpowiednią specjalizację statycznie, ponieważ funkcję tę można wybrać w czasie wykonywania. Czy nie mam wyboru, jak użyć reprezentacji w ramkach? Czy istnieje sposób na rozwiązanie tego problemu?

Moją pierwszą myślą było zakodowanie polimorfizmu rang n jako ranga 1, ale nie sądzę, aby było to w ogóle możliwe, ponieważ formuła w logice konstruktywnej niekoniecznie musi mieć wcześniejszą normalną formę.

Jon Purdy
źródło
Alternatywą jest zmniejszenie ilości potrzebnego boksu poprzez przechowywanie map bitowych, dla których argumenty funkcji i słowa w pamięci są wskaźnikami. Wtedy funkcja / struktura polimorficzna jest w rzeczywistości polimorficzna na wskaźniku lub dowolnym słowie danych, a struktury mogą przechowywać w polu swoje ostatnie pole (nawet jeśli jest to polimorficzne). Te mapy bitowe mogą być również używane przez GC, aby uniknąć potrzeby stosowania słów kluczowych dla typów niesumowanych.
fread2281
@ fread2281: Właściwie robiłem coś takiego w starszej wersji języka. Obecnie nie generuję tagów dla typów niesumowanych i nie ma GC. Myślę, że jest to również zgodne z podejściem Neela K.
Jon Purdy,

Odpowiedzi:

6

Myślałem o tym trochę. Głównym problemem jest to, że ogólnie nie wiemy, jak duża jest wartość typu polimorficznego. Jeśli nie masz tych informacji, musisz je jakoś zdobyć. Monomorfizacja otrzymuje te informacje, specjalizując się w polimorfizmie. Boks otrzymuje te informacje, umieszczając wszystko w reprezentacji o znanej wielkości.

Trzecią alternatywą jest śledzenie tego rodzaju informacji. Zasadniczo można wprowadzić inny rodzaj dla każdego rozmiaru danych, a następnie można zdefiniować funkcje polimorficzne dla wszystkich typów określonego rozmiaru. Naszkicuję taki system poniżej.

Kindsκ::=nType constructorsA::=a:κ.A|α|A×B|A+B|AB|refA|Pad(k)|μα:κ.A

Tutaj idea wysokiego poziomu polega na tym, że rodzaj tekstu mówi, ile słów potrzeba, aby ułożyć obiekt w pamięci. Dla każdego danego rozmiaru łatwo jest być polimorficznym we wszystkich typach tego konkretnego rozmiaru. Ponieważ każdy typ - nawet polimorficzny - nadal ma znany rozmiar, kompilacja nie jest trudniejsza niż w przypadku C.

α:nΓΓα:nΓ,α:nA:mΓα:n.A:m
ΓA:nΓB:mΓA×B:n+mΓA:nΓB:nΓA+B:n+1
ΓA:mΓB:nΓAB:1ΓA:nΓrefA:1
ΓPad(k):kΓ,α:nA:nΓμα:n.A:n

A×BAB

Odnośniki są interesujące - wskaźniki są zawsze jednym słowem, ale mogą wskazywać na wartości dowolnej wielkości. Pozwala to programistom na implementację polimorfizmu do dowolnych obiektów przez boksowanie, ale nie wymaga od nich tego. Wreszcie, gdy w grę wchodzą wyraźne rozmiary, często przydatne jest wprowadzenie rodzaju wypełnienia, który wykorzystuje miejsce, ale nic nie robi. (Jeśli więc chcesz wziąć rozłączną sumę liczby całkowitej i pary liczb całkowitych, musisz dodać dopełnienie pierwszej liczby wewnętrznej, aby układ obiektu był jednolity.)

Typy rekurencyjne mają standardową regułę formowania, ale należy pamiętać, że wystąpienia rekurencyjne muszą być tego samego rozmiaru, co oznacza, że ​​zwykle trzeba je nakleić wskaźnikiem, aby można było wykonać sortowanie. Np. Typ danych listy można przedstawić jako

μα:1.ref(Pad(2)+int×α)

Oznacza to więc pustą wartość listy lub parę liczb całkowitych i wskaźnik do innej połączonej listy.

Sprawdzanie typu dla takich systemów również nie jest bardzo trudne; algorytm w mojej pracy ICFP z Joshua Dunfieldem, Pełne i łatwe dwukierunkowe sprawdzanie typów polimorfizmu wyższego stopnia dotyczy tego przypadku bez prawie żadnych zmian.

Neel Krishnaswami
źródło
Fajnie, myślę, że to starannie obejmuje mój przypadek użycia. Byłem świadomy używania rodzajów do uzasadnienia reprezentacji wartości (takich jak GHC *vs. #), ale nie zastanawiałem się nad zrobieniem tego w ten sposób. Wydaje się rozsądne ograniczenie kwantyfikatorów wyższego rzędu do typów o znanej wielkości i myślę, że pozwoliłoby mi to również wygenerować specjalizacje dla poszczególnych wielkości bez konieczności znajomości rzeczywistego typu. Czas ponownie przeczytać ten artykuł. :)
Jon Purdy
1

Wydaje się, że jest to bliższe problemowi kompilacji niż problemowi „teoretycznej informatyki”, więc prawdopodobnie lepiej jest zapytać gdzie indziej.

W ogólnym przypadku myślę, że nie ma innego rozwiązania niż użycie reprezentacji w ramkach. Ale spodziewam się również, że w praktyce istnieje wiele różnych alternatywnych opcji, w zależności od specyfiki twojej sytuacji.

Np. Niskopoziomową reprezentację nieopakowanych argumentów można zazwyczaj podzielić na kilka alternatyw, np. Liczby całkowite lub podobne, zmiennoprzecinkowe lub wskaźnik. Tak więc dla funkcji f<T>może naprawdę potrzebujesz wygenerować tylko 3 różne implementacje rozpakowane i możesz reprezentować polimorficzną jako krotkę tych 3 funkcji, więc utworzenie T do Int32 oznacza po prostu wybranie pierwszego elementu krotki, ...

Stefan
źródło
Dzięki za pomoc. Nie byłem pewien, gdzie zapytać, ponieważ kompilator obejmuje zarówno teorię wysokiego poziomu, jak i inżynierię niskiego poziomu, ale pomyślałem, że ludzie tutaj będą mieli jakieś pomysły. Wygląda na to, że boks może być tutaj najbardziej elastycznym podejściem. Po przeczytaniu odpowiedzi i zastanowieniu się nad nią, jedynym innym rozsądnym rozwiązaniem, jakie udało mi się wymyślić, jest rezygnacja z pewnej elastyczności i wymaganie statycznych argumentów polimorficznych, np. Poprzez przekazanie ich jako samych parametrów typu. To kompromisy aż do samego końca. : P
Jon Purdy,
4
Pytanie OP zawiera doskonale uzasadnione problemy TCS, takie jak sposób wnioskowania o typie, gdy Damas-Hindley-Milner jest rozszerzony o typy wyższych rang. Ogólnie polimorfizm rangi 2 ma decydujące wnioskowanie o typie, ale dla rangi k> 2 wnioskowanie o typie jest nierozstrzygalne. Czy to ograniczenie Damas-Hindley-Milner to zmienia, nie wiem. Wreszcie prawie wszystko, co robią współczesne kompilatory, powinno być częścią TCS, ale zwykle nie dlatego, że implementatory kompilatora wyprzedzają teoretyków.
Martin Berger