Czy kontynuacje pierwszej klasy są przydatne w nowoczesnych obiektowych językach programowania?

10

Kontynuacje są niezwykle przydatne w funkcjonalnych językach programowania (np. ContMonada w Haskell), ponieważ pozwalają na prosty i regularny zapis kodu w stylu rozkazującym. Są one również przydatne w niektórych starszych językach imperatywnych, ponieważ można ich użyć do implementacji brakujących funkcji językowych (np. Wyjątków, coroutines, zielonych wątków). Ale dla współczesnego języka obiektowego z obsługą zbudowany dla tych funkcji, jakie argumenty będzie tam dla także dodanie wsparcia dla kontynuacji pierwszej klasy (czy bardziej nowoczesny styl rozdzielany reseti shiftlub podobny schemat call-with-current-continuation)?

Czy istnieją argumenty przeciwko dodaniu wsparcia innego niż wydajność i złożoność wdrożenia?

Jules
źródło

Odpowiedzi:

15

Pozwólmy, że Eric Lippert odpowie na to pytanie:

Oczywiste pytanie w tym momencie brzmi: jeśli CPS jest tak niesamowity, to dlaczego nie używamy go cały czas? Dlaczego większość profesjonalnych programistów nigdy o nim nie słyszała, a ci, którzy tak myślą, uważają ją za coś, co robią tylko szaleni programiści Scheme?

Po pierwsze, po prostu trudno jest większości ludzi, którzy są przyzwyczajeni do myślenia o podprogramach, pętlach, try-catch-wreszcie itd., Aby mieć powód, dla którego delegaci są wykorzystywani do kontroli przepływu w ten sposób. W tej chwili przeglądam swoje notatki na temat CPS z CS442 i widzę, że w 1995 roku napisałem profQUOTE: „Kontynuując, musisz stać na głowie i wyciągać się na lewą stronę”. Profesor Duggan (*) miał absolutną rację, mówiąc to. Przypomnijmy sobie kilka dni temu, że nasz mały przykład transformacji CPS wyrażenia C # M (B ()? C (): D ()) obejmował cztery lambdy. Nie każdy umie czytać kod korzystający z funkcji wyższego rzędu. Nakłada duże obciążenie poznawcze.

Co więcej: jedną z fajnych rzeczy w przygotowaniu określonych instrukcji przepływu sterowania w języku jest to, że pozwalają Twojemu kodowi jasno wyrażać znaczenie przepływu kontroli, ukrywając mechanizmy - stosy wywołań i adresy zwrotne oraz listy procedur obsługi wyjątków i chronione regiony i tak dalej. Kontynuacje wyjaśniają mechanizmy przepływu kontroli w strukturze kodu. Cały ten nacisk na mechanizm może przytłoczyć znaczenie kodu.

W następnym artykule wyjaśnia, w jaki sposób asynchronia i kontynuacje są dokładnie sobie równoważne, i omawia demonstrację prostego (ale blokującego) synchronicznego działania sieci i przepisania go w stylu asynchronicznym, upewniając się, że obejmuje wszystkie ukryte gotchas, które należy przykryć, aby zrobić to dobrze. Zmienia się w ogromny bałagan kodu. Podsumowanie na końcu:

Święta dobroć, jaki cholerny bałagan popełniamy. Rozszerzyliśmy dwie linie idealnie czystego kodu na dwa tuziny najbardziej cholernego spaghetti, jakie kiedykolwiek widziałeś. I oczywiście nadal się nie kompiluje, ponieważ etykiety nie są objęte zakresem i mamy wyraźny błąd przypisania. Musimy jeszcze przepisać kod, aby rozwiązać te problemy.

Pamiętasz, co mówiłem o zaletach i wadach CPS?

  • PRO: Arbitralnie złożone i interesujące przepływy sterowania można zbudować z prostych części - sprawdź.
  • CON: Reifikacja przepływu kontroli przez kontynuacje jest trudna do odczytania i trudna do uzasadnienia - do sprawdzenia.
  • CON: Kod, który reprezentuje mechanizmy kontroli, całkowicie przytacza znaczenie kodu - sprawdź.
  • CON: Przekształcenie zwykłego przepływu sterowania kodem w CPS jest tym, w czym kompilatory są dobre, a prawie nikim innym nie jest - sprawdź.

To nie jest jakieś ćwiczenie intelektualne. Prawdziwi ludzie w końcu piszą kod moralnie równoważny powyższemu przez cały czas, kiedy mają do czynienia z asynchronią. I jak na ironię, nawet jeśli procesory stały się szybsze i tańsze, spędzamy coraz więcej czasu na czekaniu na rzeczy, które nie są związane z procesorem. W wielu programach większość czasu jest poświęcana procesorowi ustawionemu na zero, czekając na pakiety sieciowe, aby odbyć podróż z Anglii do Japonii iz powrotem, lub aby dyski się obróciły lub cokolwiek innego.

I nawet nie mówiłem o tym, co się stanie, jeśli chcesz dalej komponować operacje asynchroniczne. Załóżmy, że chcesz uczynić ArchiveDocuments operacją asynchroniczną, która wymaga delegata. Teraz cały kod, który go wywołuje, musi być również napisany w CPS. Skaza tylko się rozprzestrzenia.

Prawidłowe ustawienie logiki asynchronicznej jest ważne, będzie tylko ważniejsze w przyszłości, a narzędzia, które ci daliśmy, sprawią, że „staniesz na głowie i wywrócisz się na lewą stronę”, jak mądrze powiedział profesor Duggan.

Styl przekazywania kontynuacji jest potężny, tak, ale musi istnieć lepszy sposób dobrego wykorzystania tej mocy niż powyższy kod.

Zarówno artykuły, jak i następna seria poświęcona nowej funkcji języka C #, która przenosi cały ten bałagan do kompilatora i pozwala pisać kod jako normalny przepływ sterowania za pomocą specjalnego słowa kluczowego oznaczającego niektóre części jako asynchroniczne, warto przeczytać, nawet jeśli: nie jesteś programistą C #. Nie jestem, ale to było całkiem pouczające doświadczenie, kiedy po raz pierwszy na niego natknąłem się.

Mason Wheeler
źródło
5
Warto zauważyć, że jeśli Twój język obsługuje rozszerzenia składni, takie jak makra, kontynuacje stają się znacznie bardziej przydatne, ponieważ możesz ukryć mechanizmy implementacji różnego rodzaju interesujących przepływów kontroli wewnątrz rozszerzeń składni. To w zasadzie działa tak jak „czekaj”, jest po prostu zdefiniowane w języku, ponieważ C # nie zapewnia narzędzi do samodzielnego definiowania tego rodzaju rozszerzeń składni.
Jack
Dobry artykuł, chociaż zauważę, że CPS i pierwszorzędne kontynuacje nie są dokładnie tym samym (CPS to to, co robisz w języku bez wsparcia dla kontynuacji, gdy okaże się, że ich potrzebujesz). Wygląda na to, że C # idzie dokładnie tą samą trasą, o której myślę (chociaż nie mogę zdecydować, czy chcę zautomatyzować przekształcanie opon w CPS lub wdrożyć pełne kontynuacje kopiowania stosów).
Jules
@Jack - właśnie to właśnie rozważam: język, który projektuję, ma funkcję makr, która może automatycznie obsługiwać transformację CPS. Ale pełne natywne kontynuacje mogą dać lepszą wydajność ... pytanie brzmi, jak często byłyby używane w sytuacjach krytycznych dla wydajności?
Jules