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).