Troszkę wykręcasz moje słowa - powiedziałem quote, większość języków, unquote.
Dla przykładu w TIOBE top 20 takie rzeczy potrafią LISP, Ruby no i może Python (nie jestem guru Pythonowym co prawda, ale zawsze ichniejsze meta wydawało mi się słabsze niż Rabiowe).
Z mniej mainstreamowych, ale nadal rozsądnie popularnych języków ostatnio dodali to do Scali, jest Template Haskell, jest Clojure (ale to akurat LISP).
I to chyba tyle, bo tych całkiem niszowych wymieniać teraz nie będę (ale pewnie z 10 na rozsądnym poziomie ewolucji by się znalazło).
Tak czy siak - każ coś takiego zrobić Javie albo C++ to się posra, which is precisely my point. Wróć, przy C++ to programista się posra próbując wykombinować coś szablonami ; D
Tak, efektywnie makra służą do budowania własnej składni, czyli jak powiedziałeś zwykle do tworzenia DSLi.
Tak, Ruby nie ma makr i też ma DSLe, ale spróbuj jak powiedziałem napisać ładnego DSLowego if
-a to nie będzie to takie proste (ponieważ trzeba będzie obejść one block only ot choćby). Mając honest-to-God makra jest to zadanie banalne.
Tak, wiem że języki zwykle mają if
-a ale my point still stands - Ruby'owe DSLe wymagają więcej zachodu.
Niech point-in-case będziet załączony chamski my_if
który napisałem.
Wygląda w miarę ładnie w użytkowaniu, ale czy widzisz w nim błąd? Otóż w Ruby istnieje tylko jeden true
i jeden false
więc nie da się w ten prosty sposób jednocześnie mieć na wartościach boolowskich metody #my_else
i nie mieć jej na wyjściu z tejże metody ; /
Trzeba by było te wszystkie true
/false
opakować w jakieś klasy konwertujące się do tychże w kontekście boolowskim (and I'm not sure if there's #to_bool
in any form) czy coś jeszcze sprytniejszego. A taki niby prosty sposób jaki jest przedstawiony w załączonym pliku też mi do głowu nie wpadł od razu.
Sam piszę w pracy w Rabi (killa powiedziałby pewie, że chujowo i że Ruby) to wiem co ono umożliwia zamiast makr. Wadą Rubiowych alternatyw jest to, że wszystko to dzieje się, że tak powiem, na żywym organizmie - makra, nawet LISPowe mają wyraźnie rozróżnienie między read/compile time a execution time - kod generuje się przed wykonaniem. Jak coś się wychrzani w makrze masz całkiem dobre narzędzia do debugowania tego.
W Ruby w zasadzie każda definicja klasy nie jest definicją klasy (w sensie deklaratywnym, tj. że klasa jest definiowana atomowo jako całość) tylko kodem tworzącym obiekt opisujący tą klasę w locie. Jak ktoś się postara to można zrobić klasę której sama definicja nie jest thread-safe. No bitch please, this is going too far.
Ale nawet i bez tego dość prosto jest przypadkiem rozchrzanić czyjś kod, bo monkeypatching jest absolutnie niehigieniczny (w sensie higieny nazw) i zależny od kolejności wykonywania się monkeypatchów. Ot choćby w Twoim przykładzie RSpeca trzeba zanieczyścić Object
(RSpec robi to pośrednio, poprzez Kernel
) metodą should
eww. A co jak jakaś klasa już ma taką metodę? A co jak coś dziedziczy li tylko po BasicObject
i nie includuje Kernel
? PROBLEMS, PROBLEMS EVERYWHERE.
Z makrami nie miałbyś takiego problemu, wystarczyłoby po prostu zdefiniować je na czas ekspansji makra jako słowo kluczowe i nie byłoby problemu z np. [1,2,3].should(:dance) should have(:danced)
(gdzie jedno should to metoda na podmiocie a jedno to operacja testowania ; F
Ba w takim hipotetycznym rubylike języku z makrami można by nawet zrobić coś takiego:
subject(:a) = [1,2,3]
a.should(:dance).and(:prance).like(:a_pony) should have(:danced_and_pranced_like_a_pony)
gdzie metody should
,and
i like
na a
mogłyby nawet nie zwracać a
a should
w teście i tak wiedziałoby do czego odnieść have(:danced_and_pranced_like_a_pony)
to jest do a
, a nie tego co zwróciło like
.
Show me equivalent ruby code, please.
Co do braku higieny - ot choćby dlatego (z tego co słyszałem) uważa się DSLe oparte na instance_eval
za podejrzane - local
użyte w ciele bloku który posyłamy instance_eval
musi będzie lokalne dla kontekstu ewaluacji a nie dla kontekstu definicji - nagle automagicznie blok przestaje być domknięcie w zależności od tego, gdzie go się użyje ~~' Można to niby rozwiązać definiując na kontekście ewaluacji odpowiednie method_missing
ktore catpure'uje binding
w którym blok był zdefiniowany, ale... shit just gets messy far too fast for my liking.
Dodatkowo pisanie metakodu w Ruby często nie jest możliwe by-example, tylko musisz przejść na define_method
et al. W porządnym systemie makr po prostu cytujesz kod i jesteś zadowolony.
Ech, nic nie mówiłem o tym, że w Ruby nie można tworzyć klas dynamicznie. Ale te klasy są tworzone w trakcie wykonywania programu, a to jest coś, czego - jak wyżej wspomniałem - fanem nie jestem.
(KOD W STRINGU AAAGH)
Sure, ale bez magicznych funkcji z drugiej strony pisałbyś w RSpringu i RHibernate a nie w Railsach i ARze. Myślę, że jakbyś zamiast have_many
miał używać XMLi to byś się dosyć szybko się wkurzył.
Jestem nieoświeconym hejterem obiektówki więc może oświecisz dlaczego ORM miałby być lepszy od 100% typechecked SQLa?
(UWAGA rant-tangent)
Aczkolwiek nie o tym mówiłem nawet, bardziej chodzi mi o to że np. last time I checked w ARze muszę pisać kod w rodzaju (KOD W STRINGU AAAGH):
Table.where('table.field >= ?', some_value)
podczas gdy np. Scalowy squeryl (http://squeryl.org/) pozwala mi napisać coś w rodzaju:
from(table)(t => where(t.field >= some_value))
i mieć pewność w trakcie kompilacji że wszystko z tym zapytaniem jest w porządku i nie dostanę SQL exception w twarz gdzieś w środku wykonywania programu (nie, to nie są makra, chodzi mi tu bardziej o sam fakt, że jeżeli robisz coś w trakcie kompilacji a nie wykonywania programu masz szansę złapać więcej błędów wcześniej).
Powiedziałbyś pewnie, że kod pisany test-first nie miałby takiego problemu, ale jest to przypadek trywialny.
Założe się, że jeżeli jest możliwość popełnienia gdzieś jakiegoś błędu w testach tak, że będą one nadal przechodziły jak ktoś gdzieś w stringu przypadkiem dostawi kropkę, ale aplikacja w jakiś subtelny sposób będzie działać źle, to ktoś taki błąd prędzej czy później popełni.
Jak kompilator sprawdzi poprawność Twojego kodu to jesteś dużo bardziej pewien że jest on poprawny, w końcu to tak jakby jeden wielki test pisany przez dziesiątki mądrych ludzi przez dziesiątki lat.
I tak, przykro mi, moim zdaniem testy nie są wystarczająco dobrym substytutem porządnego, silnego i statycznego systemu typów jak w Scali czy Haskellu, mogą go co najwyżej uzupełniać.
I nie, programista nigdy nie może być smart enough żeby nie popełniać błędów.
I tak to powyżej, to też jest argument przeciwko nadużywaniu metaprogramowani/makr bo, że zacytuję Kernighana:
"Everyone knows that debugging is twice as hard as writing a program in the first place. So if you’re as clever as you can be when you write it, how will you ever debug it?"
Aczkolwiek czasem można i trzeba jak już nauczysz się to wypośrodkowywać.
Tak na marginesie, Ruby i tak ma dosyć dużą dozę funkcyjności - ot choćby wszechobecne bloki i generalnie funkcje wyższego rzędu - w porównaniu do większości języków. Mogę zrobić ot choćby coś takiego (disclaimer: includes crude humour):
possibly_people = [nigga, turk, brit, ruskie, kiwi, pole, murican]
pairings_of_people = possibly_people.drop(2) # some of those may not be people
.filter { |p| p.weight < 100.kg && p.alcohol_percentage < 20.percent } # some of those should not reproduce
.zip [blonde, black, brown, red] # other can get girlfriends
i proszę mamy pary człowiek-dziewczyna o jakimś kolorze włosów postaci [[brit, blonde],[kiwi,black][pole,brown]]
, przy czym ruda zostałą bez pary, bo nie ma duszy.
100% functional in mah Ruby.
@killa
No sure, zgadzam się, czasami jak sam kodzę się orientuję poniewczasie że mogłem coś zrobić dużo prościej i mniej meta, ale jak wcześniej powiedziałem - bez magicznego kodzenia nie byłoby Rails
ów, ActiverRecord
a, tire
, acts_as_something
i wielu innych przydatnych bibliotek w Ruby.
Coś za coś.
PS. DISCLAIMER Nie znam się na niczym i mogę nie mieć absolutnie racji~! ; d
IF
- monkeypatchujesz true i false.Sam
if
jest elementem składni, nie metodą. Czy jakikolwiek język pozwoli "nadpisać" element składni? np. czy w Elixirze makrem możesz zmienić działanie/zachowaniedef
?btw. ja bym to zaimplementował taki sposób:
Bardziej realny przykład - DSL dla wzorca specyfikacji (Fowler):
Zalety: prosty (nie wymagał w ogóle metaprogramowania, instance_eval, monkeypatchowania, whatever), czytelny, działa. (div_by_3, div_by_4 to obiekty).
Monkeypatching jest niebezpieczny - with great power, comes great responsibility. Rails, RSpec, Pry, Facets - stabilne biblioteki które z powodzeniem używają monkeypatchingu.
Przy czym monkeypatching nie ma wiele wspólnego z makrami. Budowanie DSLa nie jest jednoznaczne z whatever_eval i monkeypatchowaniem.
Są różne rodzaje bloków, jedne pamiętają kontekst z momentu definicji, inne z momentu wywołania i nie ma możliwości żeby zmienił swoje zachowanie w zależności gdzie się go użyje.
define_method
definiuje metody, to nie jest żadne ograniczenie. Masz kawałek metapgoramowania bez define_method:Railsy miały dynamic_finders, mogłeś na modelu wywołać np. metodę
model.find_by_name('Rob')
. W tym przypadku metoda została dodana, ale to wynika z intencji, a nie konieczności (kolejne wywołania (a pewnie nastąpią) będą szybsze niż gdyby za każdym razem to musiało przechodzić przez method_missing).kod Sequela:
O rly? A ja mam pewność że straciłeś czas na kompilację i nie masz żadnej gwarancji że nie dostaniesz SQL exception w trakcie wykonywania programu (bo np. ktoś zmienił definicję widoku z którego korzystasz). Kompilator sprawdzi Ci poprawność składni, ale nie poprawność wyniku zapytania. Głupi test sprawdzi jedno i drugie. Jak sam dostawisz gdzieś kropkę w stringu i aplikacja będzie działa w subtelny sposób źle to kompilator też Ci nie pomoże. Kompilator w żadnym przypadku nie jest lepszy od testu.
Masz typechecking dla wspieranej funkcjonalności, spróbuj wywołać funkcję której ORM nie wspiera (teraz będę strzelał - np. zapytanie typu
COPY table_name (column1, column2) FROM 'my_file.csv' CSV
- to jest poprawne zapytanie Postgresa) . Najprawdopodobniej skończysz z RAW SQL zaszytym w stringu i good bye 100% compilation time type checking, good morning SQL exception.Prostszy przykład: dodajesz w zapytaniu SQL typecasting na "customowy" typ (np. PostGIS Point). Zapytanie się skompiluje ale nie masz gwarancji że wykona się poprawnie == SQL exception.
Czy kompilator zagwarantuje Ci że w trakcie działania aplikacji użytkownik nie spróbuje wysłać do kolumny VARCHAR tekstu mającego 1000 znaków? Dang, SQL exception. Błędów w runtime tak czy inaczej nie unikniesz, z kompilatorem czy bez musisz je obsłużyć.
poza tym Twoje pytanie było:
I moja odpowiedź:
A teraz dyskusja sprowadza się do kompilacji i sprawdzania typów. Gdyby języki bez typowania były aż tak koszmarne jak je rysujesz to nikt by ich nie używał, a są używane i cieszą się dużym powodzeniem.
Nie widzę sensu kontynuowania flamewaru o kompilacji, statycznych typach itp. Byłem ciekaw czy coś nowego kryje się pod makrami i dostałem odpowiedź - są spoko i wiem że potrafią rozwiązać dużo problemów. Z punktu widzenia Rubiego to nie jest coś nowego.
Pozdro.