Co jest takiego złego w szablonie Haskell?

252

Wygląda na to, że szablon Haskell jest często postrzegany przez społeczność Haskell jako niefortunna wygoda. Trudno wyrazić słowami dokładnie to, co zaobserwowałem w tym względzie, ale rozważ kilka przykładów

Widziałem różne posty na blogu, w których ludzie robią całkiem fajne rzeczy dzięki szablonowi Haskell, umożliwiając ładniejszą składnię, która po prostu nie byłaby możliwa w zwykłym Haskell, a także ogromną redukcję płyt kotłowych. Dlaczego więc tak patrzy się na szablon Haskell? Co sprawia, że ​​jest to niepożądane? W jakich okolicznościach należy unikać szablonu Haskell i dlaczego?

Dan Burton
źródło
56
Nie zgadzam się z głosowaniem za przeprowadzką; Zadaję to pytanie w tym samym duchu, który zadałem. Co jest tak złego w Lazy I / O? i spodziewam się zobaczyć odpowiedzi w ten sam sposób. Jestem otwarty na przeredagowanie pytania, czy to pomogłoby.
Dan Burton
51
@ErikPhilips Dlaczego nie pozwalasz osobom, które często odwiedzają ten tag, decydować, czy należy tutaj? Wygląda na to, że twoją jedyną interakcją ze społecznością Haskell jest stawianie pytań.
Gabriel Gonzalez
5
@GabrielGonzalez jest oczywiste z obecnym pytaniem i odpowiedzią, że nie wynika z FAQ, pod jakim rodzajem pytania nie powinienem tutaj zadawać: nie ma faktycznego problemu do rozwiązania . Pytanie nie rozwiązało żadnego konkretnego problemu i ma jedynie charakter koncepcyjny. I w oparciu o pytanie, czy powinienem unikać haskell szablonu, również wpada on w przepełnienie stosu, nie jest to silnik rekomendacji .
Erik Philips
27
@ErikPhilips Aspekt silnika rekomendacji nie ma znaczenia dla tego pytania, ponieważ zauważysz, że dotyczy on decyzji między różnymi narzędziami (np. „Powiedz mi, którego języka powinienem użyć”). W przeciwieństwie do tego, proszę jedynie o wyjaśnienie dotyczące szablonu Haskell, a FAQ mówi „jeśli twoją motywacją jest„ chciałbym, aby inni wyjaśnili mi [puste] ”, to prawdopodobnie nic ci nie jest”. Porównaj na przykład z GOTO nadal uważanym za szkodliwe?
Dan Burton,
29
Głosowanie w celu ponownego otwarcia. To, że jest to pytanie na wyższym poziomie, nie oznacza, że ​​nie jest dobre.
György Andrasek

Odpowiedzi:

171

Jednym z powodów unikania szablonu Haskell jest to, że jako całość nie jest w ogóle bezpieczny dla typu, a zatem jest sprzeczny z „duchem Haskell”. Oto kilka przykładów:

  • Nie masz kontroli nad tym, jaki rodzaj Haskell AST wygeneruje fragment kodu TH, poza tym, gdzie się pojawi; możesz mieć wartość typu Exp, ale nie wiesz, czy jest to wyrażenie reprezentujące a [Char]lub a (a -> (forall b . b -> c))lub cokolwiek innego. TH byłoby bardziej niezawodne, gdyby można było wyrazić, że funkcja może generować tylko wyrażenia określonego typu lub tylko deklaracje funkcji lub tylko wzorce dopasowania konstruktora danych itp.
  • Możesz generować wyrażenia, które się nie kompilują. Wygenerowałeś wyrażenie, które odwołuje się do wolnej zmiennej foo, która nie istnieje? Pech, zobaczysz to tylko podczas korzystania z generatora kodu i tylko w okolicznościach, które powodują wygenerowanie tego konkretnego kodu. Testowanie jednostkowe jest również bardzo trudne.

TH jest również całkowicie niebezpieczne:

  • Kod działający w czasie kompilacji może wykonywać dowolne czynności IO, w tym wystrzeliwanie pocisków lub kradzież karty kredytowej. Nie musisz przeglądać każdego pakietu cabal, który kiedykolwiek pobierasz w poszukiwaniu exploitów TH.
  • TH może uzyskać dostęp do funkcji i definicji „modułowo-prywatnych”, w niektórych przypadkach całkowicie przerywając enkapsulację.

Istnieją pewne problemy, które sprawiają, że funkcje TH są mniej zabawne w użyciu jako programista biblioteki:

  • Kod TH nie zawsze jest składalny. Powiedzmy, że ktoś tworzy generator soczewek, a najczęściej generator ten jest skonstruowany w taki sposób, że może go wywołać tylko „użytkownik końcowy”, a nie inny kod TH, na przykład biorąc lista konstruktorów typów dla generowania soczewek jako parametr. Wygenerowanie tej listy w kodzie jest trudne, a użytkownik musi tylko pisać generateLenses [''Foo, ''Bar].
  • Programiści nawet nie wiedzą, że można skomponować kod TH. Czy wiesz, że umiesz pisać forM_ [''Foo, ''Bar] generateLens? Qjest tylko monadą, więc możesz korzystać ze wszystkich zwykłych funkcji. Niektóre osoby nie wiedzą o tym i dlatego tworzą wiele przeciążonych wersji zasadniczo tych samych funkcji o tej samej funkcjonalności, a te funkcje prowadzą do pewnego efektu wzdęcia. Ponadto większość ludzi pisze generatory w Qmonadzie, nawet jeśli nie muszą, co jest jak pisanie bla :: IO Int; bla = return 3; dajesz funkcji więcej „środowiska” niż potrzebuje, a klienci funkcji są zobowiązani do zapewnienia tego środowiska w wyniku tego.

Wreszcie, są pewne rzeczy, które sprawiają, że funkcje TH są mniej przyjemne w użyciu jako użytkownik końcowy:

  • Nieprzezroczystość. Gdy funkcja TH ma typ Q Dec, może generować absolutnie wszystko na najwyższym poziomie modułu i nie masz absolutnej kontroli nad tym, co zostanie wygenerowane.
  • Monolit. Nie możesz kontrolować, ile generuje funkcja TH, chyba że programista na to pozwala; jeśli znajdziesz funkcję, która generuje interfejs bazy danych i interfejs serializacji JSON, nie możesz powiedzieć „Nie, chcę tylko interfejs bazy danych, dziękuję; utworzę własny interfejs JSON”
  • Czas pracy Uruchomienie kodu TH zajmuje stosunkowo dużo czasu. Kod jest interpretowany na nowo za każdym razem, gdy plik jest kompilowany, i często działający kod TH wymaga mnóstwa pakietów, które należy załadować. Spowalnia to znacznie czas kompilacji.
dflemstr
źródło
4
Dodaj do tego fakt, że szablon-haskell był historycznie bardzo słabo udokumentowany. (Chociaż właśnie spojrzałem ponownie i wydaje się, że teraz jest co najmniej nieznacznie lepiej). Ponadto, aby zrozumieć szablon-haskell, musisz zasadniczo zrozumieć gramatykę języka Haskell, która nakłada pewną złożoność (to nie jest Schemat). Te dwie rzeczy przyczyniły się do tego, że celowo nie zadałem sobie trudu zrozumienia TH, kiedy byłem początkującym Haskellem.
mightybyte
14
Nie zapominaj, że użycie szablonu Haskell nagle oznacza, że ​​kolejność zgłoszeń ma znaczenie! TH po prostu nie jest tak ściśle zintegrowany, jak można by się spodziewać, biorąc pod uwagę gładki połysk Haskell (czy to 1.4, 98, 2010, czy nawet Glasgow).
Thomas M. DuBuisson
13
Możesz myśleć o Haskell bez większych trudności, nie ma takiej gwarancji na Szablon Haskell.
sierpnia
15
A co stało się z obietnicą Olega dotyczącą bezpiecznej dla typu alternatywy dla TH? Mam na myśli jego pracę opartą na jego artykule „Nareszcie bez tagów, częściowo poddany ocenie” i więcej jego notatek tutaj . Kiedy tak ogłosili, wyglądało to tak obiecująco, a potem nigdy więcej o tym nie słyszałem.
Gabriel Gonzalez
53

To wyłącznie moja własna opinia.

  • Jest brzydki w użyciu. $(fooBar ''Asdf)po prostu nie wygląda ładnie. Na pewno powierzchowne, ale wnosi wkład.

  • Jeszcze brzydsze jest pisanie. Cytowanie czasami działa, ale często trzeba wykonywać ręczne szczepienia i instalacje AST. API jest duży i nieporęczny, zawsze jest dużo przypadków nie zależy, ale nadal potrzebują wysyłki, oraz przypadków dbam o wydają się być obecny w wielu podobnych, ale nie identycznych form (dane vs. newtype, rekord -styl kontra zwykłe konstruktory i tak dalej). Pisanie jest nudne i powtarzalne, a na tyle skomplikowane, że nie jest mechaniczne. Te reformy propozycja adresy niektóre z tych cytatów (making szerzej stosowane).

  • Ograniczeniem scenicznym jest piekło. Niemożność łączenia funkcji zdefiniowanych w tym samym module jest jego mniejszą częścią: drugą konsekwencją jest to, że jeśli masz połączenie najwyższego poziomu, wszystko po nim w module będzie poza zasięgiem czegokolwiek przed nim. Inne języki z tą właściwością (C, C ++) sprawiają, że jest to wykonalne, umożliwiając przekazywanie dalej deklaracji, ale Haskell tego nie robi. Jeśli potrzebujesz cyklicznych odwołań między złożonymi deklaracjami lub ich zależnościami i zależnościami, zazwyczaj po prostu się pieprzysz.

  • Jest niezdyscyplinowany. Rozumiem przez to, że przez większość czasu, kiedy wyrażasz abstrakcję, kryje się za nią jakaś zasada lub koncepcja. W przypadku wielu abstrakcji zasadę leżącą u ich podstaw można wyrazić w ich typach. W przypadku klas typów często można formułować prawa, których instancje powinny przestrzegać, a klienci mogą przyjąć. Jeśli użyjesz nowej funkcji generycznej GHC do wyodrębnienia formy deklaracji instancji na dowolnym typie danych (w granicach), możesz powiedzieć „dla typów sum, działa w ten sposób, dla typów produktów, działa w ten sposób”. Szablon Haskell to z kolei makra. To nie jest abstrakcja na poziomie pomysłów, ale abstrakcja na poziomie AST, co jest lepsze, ale tylko umiarkowanie, niż abstrakcja na poziomie zwykłego tekstu. *

  • Przywiązuje cię do GHC. Teoretycznie inny kompilator mógłby to zaimplementować, ale w praktyce wątpię, żeby to się kiedykolwiek wydarzyło. (Jest to w przeciwieństwie do różnych rozszerzeń systemu, które chociaż mogą być w tej chwili wdrożone tylko przez GHC, łatwo sobie wyobrazić, że zostaną przyjęte przez inne kompilatory i ostatecznie ustandaryzowane.)

  • Interfejs API nie jest stabilny. Gdy nowe funkcje języka są dodawane do GHC i pakiet szablonów haskell jest aktualizowany w celu ich obsługi, często wiąże się to z niezgodnymi wstecznymi zmianami typów danych TH. Jeśli chcesz, aby Twój kod TH był zgodny z więcej niż jedną wersją GHC, musisz być bardzo ostrożny i być może używać CPP.

  • Istnieje ogólna zasada, że ​​powinieneś używać odpowiedniego narzędzia do pracy i najmniejszego, które wystarczy, i w tej analogii Szablon Haskell jest mniej więcej taki . Jeśli istnieje sposób, aby to zrobić, który nie jest szablonem Haskell, zazwyczaj jest to preferowane.

Zaletą szablonu Haskell jest to, że możesz robić z nim rzeczy, których nie można zrobić w żaden inny sposób, i to jest duży. W większości przypadków rzeczy, do których TH jest używane, mogłyby być wykonane tylko wtedy, gdyby zostały zaimplementowane bezpośrednio jako funkcje kompilatora. TH jest niezwykle korzystne, ponieważ pozwala to robić, a także dlatego, że umożliwia prototypowanie potencjalnych rozszerzeń kompilatora w znacznie lżejszy sposób i umożliwia wielokrotne użycie (patrz na przykład różne pakiety soczewek).

Podsumowując, dlaczego uważam, że istnieją negatywne odczucia w stosunku do szablonu Haskell: Rozwiązuje wiele problemów, ale w przypadku każdego problemu, który rozwiązuje, wydaje się, że powinno być lepsze, bardziej eleganckie, zdyscyplinowane rozwiązanie lepiej dostosowane do rozwiązania tego problemu, taki, który nie rozwiązuje problemu poprzez automatyczne generowanie płyty kotłowej, ale poprzez usunięcie konieczności posiadania płyty kotłowej.

* Chociaż często uważam, że CPPma lepszy stosunek mocy do masy w przypadku problemów, które może rozwiązać.

EDYCJA 23-04-14: To, o czym często starałem się zrozumieć powyżej, a dopiero niedawno doszedłem dokładnie, to to, że istnieje ważna różnica między abstrakcją a deduplikacją. Prawidłowa abstrakcja często powoduje deduplikację jako efekt uboczny, a duplikacja jest często znamiennym znakiem nieodpowiedniej abstrakcji, ale nie dlatego jest to cenne. Właściwa abstrakcja sprawia, że ​​kod jest poprawny, zrozumiały i łatwy w utrzymaniu. Deduplikacja powoduje tylko, że jest krótsza. Szablon Haskell, podobnie jak makra, jest narzędziem do deduplikacji.

glaebhoerl
źródło
„CPP ma lepszy stosunek mocy do masy w przypadku problemów, które może rozwiązać”. W rzeczy samej. A w C99 może rozwiązać wszystko, co chcesz. Rozważ te: COS , Chaos . Nie rozumiem też, dlaczego ludzie uważają, że generowanie AST jest lepsze. To po prostu więcej dwuznaczności i mniej ortogonalności w stosunku do innych funkcji językowych
Britton Kerin
31

Chciałbym poruszyć kilka kwestii poruszonych przez dflemstr.

Nie uważam, że fakt, że nie można sprawdzić TH, jest tak niepokojący. Czemu? Ponieważ nawet jeśli wystąpi błąd, nadal będzie czas kompilacji. Nie jestem pewien, czy to wzmacnia mój argument, ale jest to duchowo podobne do błędów, które pojawia się podczas używania szablonów w C ++. Myślę jednak, że te błędy są bardziej zrozumiałe niż błędy C ++, ponieważ otrzymasz całkiem wydrukowaną wersję wygenerowanego kodu.

Jeśli wyrażenie TH / quasi-cudzysłów robi coś tak zaawansowanego, że trudne zakręty mogą się ukryć, to może nie jest to wskazane?

Tę zasadę łamię dość quasi-cytatami, nad którymi ostatnio pracuję (używając haskell-src-exts / meta) - https://github.com/mgsloan/quasi-extras/tree/master/examples . Wiem, że wprowadza to pewne błędy, takie jak niemożność podzielenia uogólnionych list. Myślę jednak, że istnieje duża szansa, że ​​niektóre pomysły zawarte w http://hackage.haskell.org/trac/ghc/blog/Template%20Haskell%20Proposal trafią do kompilatora. Do tego czasu biblioteki do parsowania drzew Haskell do TH są prawie idealnym przybliżeniem.

Jeśli chodzi o szybkość / zależności kompilacji, możemy użyć pakietu zerowego, aby wstawić wygenerowany kod. Jest to co najmniej miłe dla użytkowników danej biblioteki, ale nie możemy zrobić nic lepszego w przypadku edycji biblioteki. Czy zależności TH mogą przesłonić generowane pliki binarne? Pomyślałem, że pominął wszystko, do czego nie odwołuje się skompilowany kod.

Ograniczanie etapów / podział etapów kompilacji modułu Haskell jest do kitu.

RE Nieprzezroczystość: To samo dotyczy każdej funkcji biblioteki, którą wywołujesz. Nie masz kontroli nad tym, co zrobi Data.List.groupBy. Masz tylko rozsądną „gwarancję” / konwencję, że numery wersji mówią ci coś o kompatybilności. Kiedy jest to nieco inna kwestia zmiany.

W tym miejscu opłaca się korzystanie z zera - wersjonujesz już wygenerowane pliki - więc zawsze będziesz wiedzieć, kiedy zmieniła się forma generowanego kodu. Przyglądanie się różnicom może być nieco przesadne w przypadku dużej ilości generowanego kodu, więc jest to jedno miejsce, w którym przydałby się lepszy interfejs programisty.

RE Monolithism: Z pewnością możesz przetworzyć wyniki wyrażenia TH za pomocą własnego kodu czasu kompilacji. Filtrowanie według typu / nazwy deklaracji najwyższego poziomu nie byłoby zbyt dużym kodem. Do licha, można sobie wyobrazić pisanie funkcji, która robi to ogólnie. Aby modyfikować / de-monolityzować quasiquotery, możesz dopasować wzór w „QuasiQuoter” i wyodrębnić zastosowane transformacje lub utworzyć nową pod względem starej.

mgsloan
źródło
1
Odnośnie Krycia / Monolitu: Możesz oczywiście przejść [Dec]i usunąć rzeczy, których nie chcesz, ale powiedzmy, że funkcja odczytuje zewnętrzny plik definicji podczas generowania interfejsu JSON. To, że nie używasz tego Dec, nie powoduje, że generator przestaje szukać pliku definicji, co powoduje kompilację. Z tego powodu byłoby miło mieć bardziej restrykcyjną wersję Qmonady, która pozwoliłaby Ci generować nowe nazwy (i takie rzeczy), ale nie pozwolić IO, aby, jak mówisz, można było filtrować jego wyniki / tworzyć inne kompozycje .
dflemstr
1
Zgadzam się, powinna istnieć wersja zapytania / oferty nie w wersji IO! Byłoby to również pomóc w rozwiązaniu - stackoverflow.com/questions/7107308/... . Gdy upewnisz się, że kod czasu kompilacji nie zrobi nic niebezpiecznego, wydaje się, że możesz po prostu uruchomić sprawdzanie bezpieczeństwa na wynikowym kodzie i upewnić się, że nie odwołuje się do rzeczy prywatnych.
mgsloan
1
Czy napisałeś kiedyś swój kontrargument? Wciąż czekam na obiecany link. Dla tych, którzy szukają starej treści tej odpowiedzi: stackoverflow.com/revisions/10859441/1 , i dla tych, którzy mogą wyświetlać usunięte treści: stackoverflow.com/revisions/10913718/6
Dan Burton
1
czy istnieje bardziej aktualna wersja zera niż wersja hakowania? Ta wersja (i repozytorium darcs) zostały ostatnio zaktualizowane w 2009 roku. Obie kopie nie budują się z obecnym GhC (7.6).
aavogt
1
@ aavogt tgeeky pracował nad naprawą, ale nie sądzę, że skończył: github.com/technogeeky/zeroth Jeśli nikt tego nie zrobi / coś z tym zrobię, w końcu się tym zajmę.
mgsloan
16

Ta odpowiedź jest odpowiedzią na problemy poruszane przez illissius, punkt po punkcie:

  • Jest brzydki w użyciu. $ (fooBar '' Asdf) po prostu nie wygląda ładnie. Na pewno powierzchowne, ale wnosi wkład.

Zgadzam się. Wydaje mi się, że wybrano $ (), aby wyglądała, jakby była częścią języka - za pomocą znanej palety symboli Haskell. Jest to jednak dokładnie to, czego nie chcesz / chcesz w symbolach używanych do łączenia makr. Zdecydowanie wtapiają się w to, a ten aspekt kosmetyczny jest dość ważny. Podoba mi się wygląd {{}} splajnów, ponieważ są dość wizualnie różne.

  • Jeszcze brzydsze jest pisanie. Cytowanie czasami działa, ale często trzeba wykonywać ręczne szczepienia i instalacje AST. [API] [1] jest duży i nieporęczny, zawsze jest wiele przypadków, na których nie masz wpływu, ale nadal musisz je wysłać, a przypadki, na których ci zależy, są zwykle obecne w wielu podobnych, ale nie identycznych postaciach (dane vs. nowy typ, styl zapisu vs. normalne konstruktory itd.). Pisanie jest nudne i powtarzalne, a na tyle skomplikowane, że nie jest mechaniczne. [Propozycja reformy] [2] odnosi się do niektórych z tych kwestii (szersze zastosowanie cytatów).

Zgadzam się z tym jednak, jak zauważają niektóre komentarze w „New Directions for TH”, brak dobrego, gotowego wyceny AST nie jest krytyczną wadą. W tym pakiecie WIP staram się rozwiązać te problemy w formie biblioteki: https://github.com/mgsloan/quasi-extras . Do tej pory zezwalam na łączenie w kilku miejscach więcej niż zwykle i mogę dopasować wzór na AST.

  • Ograniczeniem scenicznym jest piekło. Niemożność łączenia funkcji zdefiniowanych w tym samym module jest jego mniejszą częścią: drugą konsekwencją jest to, że jeśli masz połączenie najwyższego poziomu, wszystko po nim w module będzie poza zasięgiem czegokolwiek przed nim. Inne języki z tą właściwością (C, C ++) sprawiają, że jest to wykonalne, umożliwiając przekazywanie dalej deklaracji, ale Haskell tego nie robi. Jeśli potrzebujesz cyklicznych odwołań między złożonymi deklaracjami lub ich zależnościami i zależnościami, zazwyczaj po prostu się pieprzysz.

Natknąłem się na problem cyklicznych definicji TH, które były niemożliwe przed ... To dość denerwujące. Istnieje rozwiązanie, ale jest brzydkie - zawiń rzeczy związane z cykliczną zależnością w wyrażeniu TH, które łączy wszystkie wygenerowane deklaracje. Jednym z tych generatorów deklaracji może być quasi-cytat, który akceptuje kod Haskell.

  • To jest bezkarne. Rozumiem przez to, że przez większość czasu, kiedy wyrażasz abstrakcję, kryje się za nią jakaś zasada lub koncepcja. W przypadku wielu abstrakcji zasadę leżącą u ich podstaw można wyrazić w ich typach. Podczas definiowania klasy typu często można sformułować prawa, których instancje powinny przestrzegać, a klienci mogą przyjąć. Jeśli użyjesz [nowej funkcji generycznej] [3] GHC do wyodrębnienia formy deklaracji instancji na dowolnym typie danych (w granicach), możesz powiedzieć „dla typów sum, działa w ten sposób, dla typów produktów, działa w ten sposób „. Ale szablon Haskell to tylko głupie makra. To nie jest abstrakcja na poziomie pomysłów, ale abstrakcja na poziomie AST, co jest lepsze, ale tylko skromnie, niż abstrakcja na poziomie zwykłego tekstu.

To jest bezproblemowe tylko wtedy, gdy robisz z nim niepraktyczne rzeczy. Jedyna różnica polega na tym, że dzięki mechanizmom abstrakcyjnym zaimplementowanym w kompilatorze masz większą pewność, że abstrakcja nie jest nieszczelna. Być może projekt demokratyzacji języka brzmi trochę przerażająco! Twórcy bibliotek TH muszą dobrze dokumentować i jasno definiować znaczenie i wyniki udostępnianych narzędzi. Dobrym przykładem zasady opartej na TH jest pakiet wyprowadzający: http://hackage.haskell.org/package/derive - wykorzystuje on DSL taki, że przykład wielu pochodnych / określa / rzeczywistej pochodnej.

  • Przywiązuje cię do GHC. Teoretycznie inny kompilator mógłby to zaimplementować, ale w praktyce wątpię, żeby to się kiedykolwiek wydarzyło. (Jest to w przeciwieństwie do różnych rozszerzeń systemu, które chociaż mogą być w tej chwili wdrożone tylko przez GHC, łatwo sobie wyobrazić, że zostaną przyjęte przez inne kompilatory i ostatecznie ustandaryzowane.)

To całkiem dobra uwaga - interfejs API TH jest dość duży i niezgrabny. Ponowne wdrożenie wydaje się trudne. Istnieje jednak tylko kilka sposobów na rozwiązanie problemu reprezentacji AST Haskell. Wyobrażam sobie, że skopiowanie TH ADT i napisanie konwertera do wewnętrznej reprezentacji AST zapewni ci sporo drogi. Byłoby to równoważne (nie bez znaczenia) wysiłkowi stworzenia haskell-src-meta. Można go również po prostu ponownie wdrożyć, ładnie drukując TH AST i używając wewnętrznego parsera kompilatora.

Chociaż mogę się mylić, nie uważam TH za skomplikowane rozszerzenie kompilatora z punktu widzenia implementacji. Jest to w rzeczywistości jedna z korzyści polegających na „utrzymaniu prostoty” i tym, że podstawową warstwą nie jest teoretycznie atrakcyjny, statycznie weryfikowalny system szablonów.

  • Interfejs API nie jest stabilny. Gdy nowe funkcje języka są dodawane do GHC i pakiet szablonów haskell jest aktualizowany w celu ich obsługi, często wiąże się to z niezgodnymi wstecznymi zmianami typów danych TH. Jeśli chcesz, aby Twój kod TH był zgodny z więcej niż jedną wersją GHC, musisz być bardzo ostrożny i być może używać CPP.

To także dobra uwaga, ale nieco dramatyczna. Chociaż ostatnio pojawiły się dodatki API, nie powodowały one nadmiernego uszkodzenia. Sądzę również, że dzięki lepszemu cytowaniu AST, o którym wspomniałem wcześniej, API, które faktycznie należy zastosować, może zostać znacznie zmniejszone. Jeśli żadna konstrukcja / dopasowanie nie wymaga odrębnych funkcji, a zamiast tego są wyrażone jako literały, wówczas większość interfejsów API znika. Ponadto kod, który piszesz, łatwiej przenosi się do reprezentacji AST dla języków podobnych do Haskell.


Podsumowując, uważam, że TH jest potężnym, częściowo zaniedbanym narzędziem. Mniej nienawiści może prowadzić do żywszego ekosystemu bibliotek, zachęcając do wdrażania większej liczby prototypów funkcji językowych. Zaobserwowano, że TH jest obezwładnionym narzędziem, które pozwala ci / zrobić / prawie wszystko. Anarchia! Cóż, moim zdaniem ta moc może pozwolić ci przezwyciężyć większość jej ograniczeń i zbudować systemy zdolne do dość pryncypialnych podejść do metaprogramowania. Warto użyć brzydkich hacków, aby zasymulować „właściwą” implementację, ponieważ w ten sposób projekt „właściwej” implementacji stopniowo stanie się jasny.

W mojej osobistej idealnej wersji nirwany duża część języka faktycznie wyszedłaby z kompilatora do bibliotek tej różnorodności. Fakt, że funkcje są implementowane jako biblioteki, nie ma dużego wpływu na ich zdolność do wiernego abstrakcji.

Jaka jest typowa odpowiedź firmy Haskell na kod płyty grzewczej? Abstrakcja. Jakie są nasze ulubione abstrakcje? Funkcje i typy!

Klasy typów pozwalają nam zdefiniować zestaw metod, które można następnie wykorzystać we wszystkich funkcjach ogólnych dla tej klasy. Jednak poza tym jedynym sposobem, w jaki klasy pomagają uniknąć bojlera, jest oferowanie „domyślnych definicji”. Oto przykład nieskrępowanej funkcji!

  • Minimalne zestawy powiązań nie są deklarowalne / sprawdzalne przez kompilator. Może to prowadzić do niezamierzonych definicji, które dają dno z powodu wzajemnej rekurencji.

  • Pomimo dużej wygody i mocy, jaką przyniosłoby to, nie można określić wartości domyślnych nadklasy, ze względu na przypadki osierocone http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ Pozwolą nam to naprawić hierarchia liczbowa z wdziękiem!

  • Poszukiwanie możliwości podobnych do TH dla domyślnych metod doprowadziło do http://www.haskell.org/haskellwiki/GHC.Generics . Chociaż jest to fajna sprawa, moje jedyne doświadczenie debugowania kodu przy użyciu tych ogólnych było prawie niemożliwe, ze względu na rozmiar typu indukowanego dla ADT i tak skomplikowanego jak ADT. https://github.com/mgsloan/th-extra/commit/d7784d95d396eb3abdb409a24360beb03731c88c

    Innymi słowy, poszło to za funkcjami zapewnianymi przez TH, ale musiało podnieść całą domenę języka, język konstrukcyjny, do reprezentacji systemu typów. Chociaż widzę, że działa dobrze w przypadku twojego wspólnego problemu, w przypadku złożonych problemów wydaje się, że jest podatny na dostarczanie stosu symboli o wiele bardziej przerażających niż hakowanie TH.

    TH umożliwia obliczenie kodu wyjściowego na poziomie wartości w czasie kompilacji, podczas gdy generics zmusza cię do przeniesienia części kodu / rekurencji kodu do systemu typów. Chociaż ogranicza to użytkownika na kilka dość użytecznych sposobów, nie sądzę, aby złożoność była tego warta.

Myślę, że odrzucenie TH i metaprogramowanie podobne do seplenia doprowadziło do preferencji takich rzeczy jak domyślne metody zamiast bardziej elastycznych makropoleceń, takich jak deklaracje instancji. Dyscyplina unikania rzeczy, które mogłyby prowadzić do nieprzewidzianych wyników, jest mądra, nie powinniśmy jednak ignorować tego, że system typów Haskell umożliwia bardziej niezawodne metaprogramowanie niż w wielu innych środowiskach (poprzez sprawdzenie wygenerowanego kodu).

mgsloan
źródło
4
Ta odpowiedź sama w sobie nie stoi zbyt dobrze: robisz wiele odniesień do innej odpowiedzi, którą muszę znaleźć i znaleźć, zanim będę mógł poprawnie odczytać twoją.
Ben Millwood
To prawda. Starałem się wyjaśnić, o czym mimo wszystko mówiono. Być może zmienię, aby wstawić punkty illisuis.
mgsloan
Powinienem powiedzieć, że być może słowo „pozbawione zasad” jest silniejszym słowem, niż powinienem był używać, z pewnymi konotacjami, których nie zamierzałem - z pewnością nie są pozbawione skrupułów! Ten punkt kuli był tym, z którym miałem najwięcej kłopotów, ponieważ mam to uczucie lub nieformalny pomysł w mojej głowie i kłopoty z wypowiedzeniem go, a słowo „nienazwane” było jednym ze słów, które zrozumiałem, które były gdzieś w pobliżu. „Dyscyplina” jest prawdopodobnie bliższa. Nie mając jasnego i prostego sformułowania, zamiast tego wybrałem kilka przykładów. Jeśli ktoś mógłby mi wyjaśnić, co prawdopodobnie miałem na myśli, byłbym wdzięczny!
glaebhoerl
(Myślę też, że mogłeś
zmienić
1
Doh! Dzięki za zwrócenie na to uwagi! Naprawiony. Tak, niezdyscyplinowanie byłoby prawdopodobnie lepszym sposobem na wyrażenie tego. Myślę, że w tym przypadku rozróżnia się między „wbudowanymi”, a zatem „dobrze zrozumiałymi” mechanizmami abstrakcyjnymi, a „ad-hoc” lub mechanizmami abstrakcyjnymi zdefiniowanymi przez użytkownika. Przypuszczam, że to, co mam na myśli to, że można określić conceivably bibliotekę TH który realizowany coś zupełnie podobnego do typeclass wysyłki (aczkolwiek w czasie kompilacji nie Runtime)
mgsloan
8

Jeden raczej pragmatyczny problem z szablonem Haskell polega na tym, że działa on tylko wtedy, gdy dostępny jest interpreter kodu bajtowego GHC, co nie ma miejsca we wszystkich architekturach. Jeśli więc twój program używa szablonu Haskell lub korzysta z bibliotek, które go używają, nie będzie działał na komputerach z procesorem ARM, MIPS, S390 lub PowerPC.

Jest to istotne w praktyce: git-annex to narzędzie napisane w Haskell, które ma sens, aby uruchamiać się na maszynach martwiących się o pamięć, takie maszyny często mają procesory inne niż i386. Osobiście uruchamiam git- Annex na NSLU 2 (32 MB pamięci RAM, procesor 266 MHz; czy wiesz, że Haskell działa dobrze na takim sprzęcie?) Jeśli używałby szablonu Haskell, nie jest to możliwe.

(Sytuacja dotycząca GHC na ARM bardzo się poprawia w tych dniach i myślę, że 7.4.2 nawet działa, ale i tak jest.)

Joachim Breitner
źródło
1
„Szablon Haskell korzysta z wbudowanego kompilatora i interpretera kodu bajtowego GHC do uruchamiania wyrażeń łączenia”. - haskell.org/ghc/docs/7.6.2/html/users_guide/…
Joachim Breitner
2
ah, ja, że ​​- nie, TH nie będzie działać bez interpretera kodu bajtowego, ale różni się to od (chociaż istotne dla) ghci. Nie zdziwiłbym się, gdyby istniała idealna zależność między dostępnością ghci a dostępnością interpretera kodu bajtowego, biorąc pod uwagę, że ghci zależy od interpretera kodu bajtowego, ale problemem jest brak interpretera kodu bajtowego, a nie brak ghci konkretnie.
muhmuhten
1
(nawiasem mówiąc, ghci ma zabawnie słabe wsparcie dla interaktywnego korzystania z TH. spróbuj ghci -XTemplateHaskell <<< '$(do Language.Haskell.TH.runIO $ (System.Random.randomIO :: IO Int) >>= print; [| 1 |] )')
muhmuhten
Ok, z ghci miałem na myśli zdolność GHC do interpretacji (zamiast kompilacji) kodu, niezależnie od tego, czy kod wchodzi interaktywnie w binarny ghci, czy od splice TH.
Joachim Breitner
6

Dlaczego TH jest zły? Dla mnie sprowadza się to do tego:

Jeśli musisz wygenerować tak wiele powtarzalnych kodów, że próbujesz użyć TH do automatycznego wygenerowania kodu, robisz to źle!

Pomyśl o tym. Połowa atrakcyjności Haskell polega na tym, że jego wysokopoziomowa konstrukcja pozwala uniknąć ogromnej ilości bezużytecznego kodu, który musisz pisać w innych językach. Jeśli potrzebujesz generowania kodu w czasie kompilacji, zasadniczo mówisz, że zawiódł Cię język lub projekt aplikacji. A my programiści nie lubimy zawieść.

Czasami jest to oczywiście konieczne. Ale czasami możesz uniknąć potrzeby TH, po prostu będąc nieco bardziej sprytnym w swoich projektach.

(Inną rzeczą jest to, że TH jest dość niskiego poziomu. Nie ma wielkiego projektu wysokiego poziomu; wiele wewnętrznych szczegółów implementacji GHC jest ujawnionych. To sprawia, że ​​API może się zmieniać ...)

MathematicalOrchid
źródło
Nie sądzę, że to oznacza, że ​​twój język lub aplikacja zawiodły cię, szczególnie jeśli weźmiemy pod uwagę QuasiQuotes. Zawsze są kompromisy, jeśli chodzi o składnię. Niektóre składnie lepiej opisują określoną domenę, więc czasem możesz chcieć przełączyć się na inną składnię. QuasiQuotes pozwala elegancko przełączać się między składniami. Jest to bardzo potężne i jest używane przez Yesod i inne aplikacje. Zdolność pisania kodu generowania HTML przy użyciu składni, która wydaje się być HTMLem, jest niesamowitą funkcją.
CoolCodeBro
@CoolCodeBro Tak, quasi-cytowanie jest całkiem miłe i przypuszczam, że nieco prostopadłe do TH. (Oczywiście jest zaimplementowany na szczycie TH.) Zastanawiałem się więcej nad użyciem TH do generowania instancji klas, budowania funkcji o wielu aracjach lub czegoś podobnego.
MathematicalOrchid