Skip to content

Instantly share code, notes, and snippets.

@jaen
Last active December 21, 2015 15:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jaen/6327210 to your computer and use it in GitHub Desktop.
Save jaen/6327210 to your computer and use it in GitHub Desktop.
Rant. About metaprogramming. In polish.

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, ActiverRecorda, 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

#!/usr/bin/env ruby
require 'pry'
def my_if(pred, &block)
def ifify(bool)
def bool.my_else(&block)
yield if !self
!!self # to return just a final, non-chainable boolean
end
def bool.my_elsif(pred, &block)
if !self && (a = !!pred) then
yield
ifify(a)
else
self
end
end
bool
end
a = !!pred
yield if a
ifify(a)
end
# sample usage
my_if(false) {
puts 'true'
}.my_elsif(false) {
puts 'otherwise'
}.my_elsif(false){
puts 'otherwise2'
}.my_else {
puts 'false'
}.my_else {
puts 'falser'
}
binding.pry
@robuye
Copy link

robuye commented Aug 24, 2013

  1. Własny 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/zachowanie def?

btw. ja bym to zaimplementował taki sposób:

module MyIfy
  extend self

  def if(pred, &block)
    @pred = !!pred
    block.call if @pred
    self
  end

  def else(&block)    
    block.call unless @pred
    self
  end

  def elsif(pred, &block)
    if not @pred
      @pred = !!pred
      block.call if @pred
    end
    self
  end

  def to_bool #to chain with normal if/else
    !!@pred
  end
end


if (MyIfy.if(false) { puts "true" }.
     elsif(false) { puts "otherwise" }.
     elsif(true) { puts "otherwise2" }.
     else { puts "false" }.
     else { puts "falser" }.to_bool)

  puts "classic truthy"
else
  puts "classic falsy"
end

# => otherwise2
# => classic truthy
# => nil
  1. DSL

Bardziej realny przykład - DSL dla wzorca specyfikacji (Fowler):

div_by_3.and(div_by_4).is_satisfied_by?(12) # => true
div_by_3.and(div_by_4).or(div_by_4).is_satisfied_by?(8) # => true

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:

class MyClass
  def invoke_private_method(meth)
    if private_methods(false).include?(meth.to_sym)
      send(meth.to_sym)
    else
      puts "Here be no dragon called #{meth}"
    end
  end

  private

  def puts_one
    puts "one"
    1
  end

  def puts_two
    puts "two"
    2
  end
end

MyClass.new.invoke_private_method('puts_one')

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

  1. kod w stringu? - czy ja mówiłem że to jest fajne? Just because you can doesn't mean you should. Kod w stringu to nie jest definicja metaprogramowania.
  2. AR nie ma dobrego wsparcia dla warunków typu ">", "<", "like", "or" itp. Możesz przejść na Arela lub Sequela - oba ORMy sobie z tym doskonale radzą. Przy czym Sequel może więcej niż Arel ;)

kod Sequela:

table.where(:created_at => (Date.today - 14)..(Date.today - 7))
table.where{(value >= 50) & (value <= 100)}

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

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.

Jestem nieoświeconym hejterem obiektówki więc może oświecisz dlaczego ORM miałby być lepszy od 100% _typechecked SQL_a?

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:

jaki język pozwala Ci traktować np. zapytanie SQL jako pełnoprawną część języka? Nie string, tylko normalnie kod SQLa w treści programu

I moja odpowiedź:

Mapowanie SQL na interfejs języka to ORM(...)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment