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
- Szablon Haskell wymieniony w sekcji „Brzydki (ale konieczny)” w odpowiedzi na pytanie Których rozszerzeń Haskell (GHC) powinni używać / których powinni unikać użytkownicy?
- Szablon Haskell rozważał tymczasowe / gorsze rozwiązanie w wątku Unboxed wektorów wartości nowego typu wątku (lista mailingowa bibliotek)
- Yesod jest często krytykowany za zbytnie poleganie na szablonie Haskell (patrz post na blogu w odpowiedzi na ten sentyment)
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?
haskell
template-haskell
Dan Burton
źródło
źródło
Odpowiedzi:
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:
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.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:
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.Istnieją pewne problemy, które sprawiają, że funkcje TH są mniej zabawne w użyciu jako programista biblioteki:
generateLenses [''Foo, ''Bar]
.forM_ [''Foo, ''Bar] generateLens
?Q
jest 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 wQ
monadzie, nawet jeśli nie muszą, co jest jak pisaniebla :: 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:
Q Dec
, może generować absolutnie wszystko na najwyższym poziomie modułu i nie masz absolutnej kontroli nad tym, co zostanie wygenerowane.źródło
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
CPP
ma 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.
źródło
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.
źródło
[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 tegoDec
, nie powoduje, że generator przestaje szukać pliku definicji, co powoduje kompilację. Z tego powodu byłoby miło mieć bardziej restrykcyjną wersjęQ
monady, 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 .Ta odpowiedź jest odpowiedzią na problemy poruszane przez illissius, punkt po punkcie:
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.
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.
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 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.
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.
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).
źródło
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.)
źródło
ghci -XTemplateHaskell <<< '$(do Language.Haskell.TH.runIO $ (System.Random.randomIO :: IO Int) >>= print; [| 1 |] )'
)Dlaczego TH jest zły? Dla mnie sprowadza się to do tego:
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ć ...)
źródło