Ruby a metody z '?' i '!' w nazwie

Wiem, że pewnie sporo z Was ma mój blog w rss, ale ponieważ uważam temat za dosyć ważny to piszę także tu.
http://radarek.jogger.pl/2009/02/21/ruby-a-metody-z-i-w-nazwie/

Sprawa jest o tyle ciekawa, że zrobiłem mały research i wychodzi mi na to, że polskie teksty nt. temat błędnie opisują tą kwestię.
http://www.google.pl/search?q=ruby+metoda+z+wykrzyknikiem&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:pl:official&client=firefox-a

Wystarczy poprzeglądać, nie będę wymieniać, żeby ktoś nie poczuł się urażony. Uważam, że konwencja z ‘!’ to jedna z piękniejszych rzeczy w Ruby i fajnie by było uczyć jej poprawnej (tj. pełnej) formie. Samo “niebezpieczna metoda” niestety oznacza zupełnie co innego niż “bardziej niebezpieczna wersja metody bez ‘!’”.

Czuję się wywołany do tablicy. Zatem piszesz na swoim blogu:

[quote]Zaktualizowałem wpis, dodając cytat matza nt. „bang method”.
Zrobiłem także mały research w polskim internecie i wszędzie jest źle opisana ta konwencja.[/quote]
A to cytat z mojego przewodnika:

Może faktycznie nie jest tutaj napisane, że destrukcyjna powinna być interpretowana jako “modyfikująca” wersja innej metody. Dla mnie, jako autora, wynik a to jednak z przytoczonego przykładu. Myślę, że dla części czytelników również.
Czy znaczy to, że ta konwencja jest u mnie “źle opisana”? To też kwestia interpretacji :slight_smile: Czy źle znaczy błędnie, czy niejednoznacznie?
Jeśli to pierwsze, to się nie zgadzam, jeśli to drugie - to mogę się zgodzić.

Co do samej konwencji - jak to z konwencjami bywa, czasem się je stosuje, a czasem nie, jak widać to z przytoczonych przykładów w Railsach. Oczywiście można odwoływać się do słów Matza, ale języki, również języki programowania są obiektami podlegającymi społecznym interpretacjom, które (o zgrozo!) mogą iść w kierunku przeciwnym do założeń ich twórców. I puryści językowi mogą orzec np. że “w każdym bądź razie” jest błędem językowy, co jednak nie “uchroni” mnóstwa ludzi od stosowania tego zwrotu do skutecznego komunikowania się…

To napisawszy dodam słowa “potencjalnie destrukcyjną wersją metody o tej samej nazwie” - mam nadzieję, że to jest wystarczająco jednoznaczne :slight_smile:

A co do samych metod z wykrzyknikami - to uważam, że jest z nimi większy problem, o którym znacznie rzadziej się pisze.
Otóż (przynajmniej dla Stringa) - jeśli metoda z wykrzyknikiem nie zmodyfikuje oryginalnego łańcucha znaków, to zwracany jest nil!
To jest IMHO zdecydowanie nieintuicyjne (przynajmniej w kontekście par metod z wykrzykniekiem/bez wykrzyknika) i nie pozwala ich stosować “na sposób unikoswych pipów”. W istocie w przetwarzaniu Stringów zazwyczaj stosuję metody bez wykrzyknika.

[quote=apohllo]A to cytat z mojego przewodnika:

Może faktycznie nie jest tutaj napisane, że destrukcyjna powinna być interpretowana jako “modyfikująca” wersja innej metody.[/quote]
Bo i nie chodzi właśnie o to, że ma być wersją modyfikującą. Istotne są dwa fakty:

  1. istnieje “normalna” wersja
  2. istnieje wersja z “!”, którą autor uważa w pewien sposób za bardziej niebezpieczną od tej bez “!”
    Definicja “bardziej niebezpieczna” zostaje w gestii autora. Może to być modyfikacja obiektu w miejscu (map(!)), może to być zwykły zapis i zwrócenie true/false bądź rzucenie wyjątku (ActiveRecord#save(!) - chociaż muszę przyznać, że przy pierwszym zetknięciu z railsami oczekiwałem, że wersja save! nie będzie walidować obiektu, ale autorzy przyjęli inną wersję i jest ok), może to być ominięcie wykonywania jakiś callbacków (Kernel.at_exit(!)),

Bez tego 2 punktu mięlibyśmy więcej metod z ‘!’ niż bez w naszym kodzie i ‘!’ straciłbym swoją moc.

[quote=apohllo]Dla mnie, jako autora, wynik a to jednak z przytoczonego przykładu. Myślę, że dla części czytelników również.
Czy znaczy to, że ta konwencja jest u mnie “źle opisana”? To też kwestia interpretacji :slight_smile: Czy źle znaczy błędnie, czy niejednoznacznie?
Jeśli to pierwsze, to się nie zgadzam, jeśli to drugie - to mogę się zgodzić.[/quote]
Rzekłbym, że nie jest opisana do końca. Twoja wersja nie wyklucza mojej, ale też nie jest konkretna. Brakuje w niej właśnie tej kluczowej drugiej części, o której zapomina (lub po prostu nie wie) większość opisujących, a bez której konwencja nie ma tego samego znaczenia. Ale nie jesteś wyjątkiem :), podobnie jest choćby na pl wikibooks i innych blogach). Chociaż nie wiem czy tam jest najlepsze miejsce na to, bo wiem, że tylko tłumaczysz dlaczego są dwie wersje metod.

Według mnie, railsy nie były nigdy przykładem bardzo dobrze pisanego kodu rubiego, chociaż nie twierdzę, że są bardzo złe. Po części to one uczyły złych nawyków i anty-patternów (alias_method_chain jest tego przykładem).

Ludzie piszą przeważnie tak jak ich się nauczy ;-). A zresztą jak ktoś chce to nie spróbuje stosować się faktycznie do zasady, że każda metoda posiadająca skutku uboczne musi posiadać w nazie ‘!’ :). Kod wyglądałby np tak:

def update @user = User.find(params[:id]) if @user.update_attributes!(params[:user]) #* redirect_to! :action => 'list #* else render! :action => 'edit' #* end end #* bo nie interesuje nas wartość zwracana a skutki uboczne

[quote=apohllo]I puryści językowi mogą orzec np. że “w każdym bądź razie” jest błędem językowy, co jednak nie “uchroni” mnóstwa ludzi od stosowania tego zwrotu do skutecznego komunikowania się…

To napisawszy dodam słowa “potencjalnie destrukcyjną wersją metody o tej samej nazwie” - mam nadzieję, że to jest wystarczająco jednoznaczne :-)[/quote]
Tylko, że mnie chodzi także o zwrócenie uwagi, że sama wersja z ‘!’ nie ma sensu. Chociaż zdaję sobie sprawę, że wymaga to dogłębniejszego wytłumaczenia i nie wiem czy jest sens, żebyś to robił w tak krótkim kursie. Najważniejsze, żeby z Twojego tekstu nie wynikało, że jeśli metoda ma być “niebezpieczna” (destrukcyjna, z efektami ubocznymi) to powinna zawierać ‘!’ bo konwencja o tym nie mówi.

[quote=apohllo]A co do samych metod z wykrzyknikami - to uważam, że jest z nimi większy problem, o którym znacznie rzadziej się pisze.
Otóż (przynajmniej dla Stringa) - jeśli metoda z wykrzyknikiem nie zmodyfikuje oryginalnego łańcucha znaków, to zwracany jest nil!
To jest IMHO zdecydowanie nieintuicyjne (przynajmniej w kontekście par metod z wykrzykniekiem/bez wykrzyknika) i nie pozwala ich stosować “na sposób unikoswych pipów”. W istocie w przetwarzaniu Stringów zazwyczaj stosuję metody bez wykrzyknika.[/quote]
Można zgłosić problem na http://redmine.ruby-lang.org/. Jak coś to mogę to zrobić, już sporo rzeczy im zgłaszałem.

>> [1,2,3].map! {|e| e * 2 }.map! {|e| e * 2 } => [4, 8, 12]
Skoro dla Array#map! działa to nie ma problemu żeby działało dla innych metod z ‘!’.

To nie byłby zły pomysł :slight_smile: Tyle, że pewnie jest sporo kodu, który na tym (niestety) bazuje.

Jeszcze ostatnie słowo w sprawie konwencji “językowych” - taki właśnie z nimi problem, że ze względu na ich konwencjonalność mogą być rozmaicie interpretowane. Widać to w Twoim przykładzie z interpretacją save! - spodziewałeś się jednego (“zgodnie z konwencją”), a autor zaimplementował to inaczej (“zgodnie z konwencją”). No i w zasadzie bez zaglądania do API niewiele z tej konwencji wynikło.

Oczywiście piszę to wszystko trochę z przekory, bo istotnie również podoba mi się to, że mogę dodać znak zapytania lub wykrzyknik na końcu nazwy metody. Niemniej jednak, stoję na stanowisku, że wykorzystanie tych znaków (w szczególności wykrzyknika) może być twórczo rozwijane, nawet trochę “wbrew konwencji”.

Tutaj się nie zgodzę. “save!” jest najbardziej są zgodne z tą konwencją, ponieważ istnieje wersja “zwykła” oraz “bardziej niebezpieczna”. Akurat ta konwencja nie narzuca znaczenia “bardziej niebezpieczna”. Świetnie to opisał David A. Black na swoim blogu: http://dablog.rubypal.com/2007/8/15/bang-methods-or-danger-will-rubyist (ostatni akapit pt. “Forget “intuitive””). Z kolei wspomniana metoda “threadsafe!” już nie jest z tą konwencją zgodna, ale tutaj trochę na siłę się przyczepiłem. Ponieważ ta metoda jest tylko odpalana raz (w pliku konfiguracyjnym) i jest częścią DSLa to można sobie pozwolić na odstępstwo.

Tylko jeśli będzie nadużywane to konwencja traci swoją magię i przydatność (pokazałem wcześniej na przykładzie). DSL na pewno są takim miejscem, gdzie można sobie pozwolić na odstępstwo. W innych miejscach lepiej tego nie robić. W końcu na tym polega konwencja, jest umową między programistami. Jeśli ktoś ją łamie to wprowadza innych w błąd.

A czy ja napisałem, że to jest niezgodne z konwencją? Przecież nie. No ale właśnie problem polega na tym, że:

Tym właśnie konwencje różnią się od zasad wymuszanych przez kompilator, czy interpreter, że nie narzucają jednoznacznej interpretacji.
Ale to sprawia, że nawet jeśli znamy “metodę bez !” powinniśmy zajrzeć do API (przez co przydatność tej konwencji stoi pod znakiem zapytania). “sub!” dla Stringa, czyli metoda wzięta z samej implementacji Rubiego, jest tego najlepszym przykładem. Z drugiej strony dziwi np. w klasie Array brak metod select!, czy reject!. W samym Rubim nie ma 100% konsekwencji w stosowaniu tej konwencji - albo przynajmniej tego czego można by się spodziewać znając tę konwencję.

Summa summarum - staram się tutaj prezentować pragmatyczne stanowisko. Jest pewna konwencja, która jest w jakimś stopniu użyteczna. Nie widzę jednak sensu w jej przesadnym gloryfikowaniu i bardzo dokładnym ustalaniu co jest zgodne z konwencją a co nie.

To z przytoczonego artykułu (wytłuszczenie moje):

To już jest moim zdaniem śmieszne. Gdybym miał metodę, powiedzmy gsub_bla_bla, to od razu wiedziałbym, że muszę zajrzeć do API, żeby wiedzieć co ona robi. Jeśli ma mnie o tym informować wykrzyknik, na końcu nazwy, to nie można mówić o czy innym niż zasada, że dwie metody o innej nazwie, mają (albo mogą mieć) inną semantykę (co jest trywialne i nie wymaga żadnej konwencji).

A czy ja napisałem, że to jest niezgodne z konwencją? Przecież nie.[/quote]
Tak to odebrałem :).

[quote=apohllo]No ale właśnie problem polega na tym, że:

Tym właśnie konwencje różnią się od zasad wymuszanych przez kompilator, czy interpreter, że nie narzucają jednoznacznej interpretacji.[/quote]
Nie wiem czy poprawnie interpretuję słowo “narzucają”. Konwencja jak najbardziej narzuca jednoznaczną interpretację (w tym wypadku jest to “metoda z wykrzyknikiem oznacza bardziej niebezpieczną wersję metody bez wykrzyknika” - czy można niepoprawnie to zinterpretować?), z tą różnicą, że nie jest w stanie kogoś zmuszać do niej. Jeśli ktoś zrobi z ‘!’ własny użytek to nie stosuje się do konwencji i tyle.

Cóż, to wynika z założeń konwencji.

‘reject!’ jest w 1.9, ‘select!’ nie ma, a w sumie IMHO powinna. Zgłoszę i to.

Jakiś przykład? Poprzednia uwaga (select!) się tego nie dotyczy, bo brakowało metod z ‘!’, a istnienia ich nie wymusza konwencja. Wskaż mi metodę, która będzie miała w nazwie ‘!’ i nie będzie miała odpowiednika bez ‘!’ - wtedy będzie to złamanie zasady. Ja nie znam takiej metody.

No to po co w takim układzie takie konwencje wymyślać?:slight_smile: Mój pragmatyzm mówi: starać się pisać maksymalnie zgodnie z konwencją, chyba, że mamy ważny powód by zrobić inaczej. Pragmatyczny nie oznacza “bo mi tak bardziej pasuje” ;-).

[quote=apohllo]To z przytoczonego artykułu (wytłuszczenie moje):

To już jest moim zdaniem śmieszne. Gdybym miał metodę, powiedzmy gsub_bla_bla, to od razu wiedziałbym, że muszę zajrzeć do API, żeby wiedzieć co ona robi. Jeśli ma mnie o tym informować wykrzyknik, na końcu nazwy, to nie można mówić o czy innym niż zasada, że dwie metody o innej nazwie, mają (albo mogą mieć) inną semantykę (co jest trywialne i nie wymaga żadnej konwencji).[/quote]
Czy ja wiem? Jeszcze raz przypomnę, że metoda z ‘!’ powinna być bardziej niebezpieczną wersją metody bez. Czy zmieniając semantyką można mówić o innej wersji? Wtedy będziesz mieć zupełnie 2 inne metody, mające tylko podobne nazwy. Do doca i tak musisz zajrzeć, żeby przeczytać co dla autora oznaczało ‘bardziej niebezpieczna’.

Jakiś przykład?[/quote]
Odniosę się tutaj do “tego czego można by się spodziewać znając tę konwencję”, bo to jest dla mnie wyznacznik jej użyteczności.
Wskazujesz, że w Ruby 1.9 jest metoda reject! Świetnie. Ale w 1.8 była metoda delete_if, której semantyka jest właśnie taka jak brakująca reject!
Oczywiście konwencja, o której tutaj mówimy nie narzuca tego, że delete_if musiała się nazywać reject!, ale tego bym się spodziewał znając ją. Dlatego jest to wg mnie niekonsekwentne użycie tej konwencji.

Tzn. dyskutujemy o trochę innych rzeczach - Ty o samej konwencji i jej literalnym znaczeniu, ja o jej użyteczności. I moim zdaniem użyteczność tej konwencji (ze względu na jej niejednoznaczność - bo owa “niebezpieczna wersja metody” może być właśnie interpretowana na różne sposoby), jest znacznie mniejsza niż np. tej dotyczącej pytajnika.
Czy to, że oryginalny obiekt jest modyfikowany (w przypadku metod działających na Stringach), to jest “niebezpieczne” zachowanie? Dla mnie “niebezpieczne” kojarzy się z czymś czego raczej chcemy unikać, w przypadku metod takich jak “chop!”, znacznie częściej będziemy korzystać z wersji wykrzyknikowej. Gdyby konwencja wyraźnie mówiła, że jest to “wersja metody modyfikująca oryginalny obiekt”, to może rzadziej byłaby stosowana, ale mniej byłoby problemów z jej interpretacją, a zatem jej użyteczność IMO wzrosłaby.

Reasumując - niejednoznaczność słowa “niebezpieczna” powoduje, że ta konwencja nastręcza pewnych problemów interpretacyjnych, a co za tym idzie zmniejsza jej użyteczność (przynajmniej jeśli trzymamy się oficjalnej wykładni tego czym jest ta konwencja). Dlatego moim zdaniem, pewne odstępstwa od tej oficjalnej wykładni nie są wielkim nadużyciem.

Natomiast skoro jesteśmy przy konwencjach, to dla kontrastu wskażę to, gdzie konwencje działają b. dobrze - otóż np. w ActionControllerze: konwencja wg której domyślnie wybierany szablon dla akcji nazywa się tak samo jak ona, jest znakomita. Z jednej strony - jeśli działam zgodnie z konwencją, to jednoznacznie mogę przewidzieć jej konsekwencje. Z drugiej - jeśli nie działam zgodnie z tą konwencją, to muszę to wyraźnie zaznaczyć (przez wywołanie “render” gdzieś w ciele akcji).

Macie o czym dyskutować, chłopaki :wink:

Jakiś przykład?[/quote]
Odniosę się tutaj do “tego czego można by się spodziewać znając tę konwencję”, bo to jest dla mnie wyznacznik jej użyteczności.
Wskazujesz, że w Ruby 1.9 jest metoda reject! Świetnie. Ale w 1.8 była metoda delete_if, której semantyka jest właśnie taka jak brakująca reject!
Oczywiście konwencja, o której tutaj mówimy nie narzuca tego, że delete_if musiała się nazywać reject!, ale tego bym się spodziewał znając ją. Dlatego jest to wg mnie niekonsekwentne użycie tej konwencji.[/quote]
Cóż, tutaj zwracasz uwagę na fakt, że nie wszystko w rubym jest idealne. Jest wiele niekonsekwencji, ale jak w każdym języku. Mnie też rażą takie niedociągnięcia, ale nie wszystko będzie tak jak sobie bym życzył :). Trzeba z tym żyć.

Też odniosłem takie wrażenie :).

Nie przeczę. Sprawa z pytajnikiem jest tak prosta, że ciężko o coś bardziej logiczne i nie budzącego wątpliwości.

Zależy kiedy i gdzie. Jeśli napiszę:

def foo(s) s.downcase! return s end
To fakt modyfikacji obiektu w miejscu jest niebezpieczny (ponieważ wywołujący może nie zdawać sobie z tego sprawę). Mimo wszystko modyfikację w miejscu można uznać za bardziej niebezpieczne od zwrócenia nowej kopii (wystarczy przyglądnąć się językom funkcyjnym takim jak erlang czy haskell, które nie posiadają takiej możliwości w ogóle).

A mi się wydaje, że właśnie dlatego matz nie przyjął takiej konwencji. Większość metod musiałaby wtedy zawierać ‘!’, co powodowałoby większy chaos (noise) w kodzie. Aczkolwiek zgodzę się, że konwencja byłaby prostsza w interpretacji i mniej problemów byłoby z nią (chociaż dla mnie to nie jest problem).

Byłaby prostsza w interpretacji. Z kolei odstępstwo od konwencji (jakakolwiek by to nie była) IMHO wprowadza jeszcze większy chaos.

Dla mnie taka wersja jaką podałem na blogu jest bardziej elastyczna i jestem w stanie zaakceptować jej konsekwencje (tj. niejednoznaczność określenia “bardziej niebezpieczna”). To tak jak z blokami kodu. Niektórzy mówią na nie iteratory, a przecież nie zawsze o to chodzi. Czasem chodzi o pozyskiwanie/zwalnianie zasobów, transakcyjność, iterowanie, callback. I też trzeba zajrzeć do doca co metoda robi z przekazanym blokiem.

Btw, skoro Twoja interpretacja jest inna, to czy zdarzyło Ci się napisać samą metodę destrukcyjną z ‘!’, która nie miała odpowiednika bez ‘!’?

Ja akurat lubię takie dyskusje, chociaż wiem, że wynika to z mojego puryzmu językowego. Dla mnie taka dyskusja jest o wiele ciekawsza niż opisywanie po raz n-ty jak działa walidacja w AR ;-). Nawet jeśli nasze zdania są różne, to cenię sobie czyjeś inne spojrzenie.

Lekko sobie szydzę, bo nie jest tajemnicą ani niczyją odosobnioną opinią że biblioteka standardowa rubiego to bajzel i kopalnia niekonsekwencji w nazewnictwie (czy aliasowaniu) :wink:

Ano nie jest, ale w praktycznie każdym języku znajdziesz różne kwiatki. O tym nie jest jednak dyskusja :P.

Myślę, że temat wyczerpaliśmy do dna. W każdym razie - jak ktoś będzie się pytał o wykrzykniki w Rubim, to odeślę go do tego wątku :slight_smile:

Ja chyba nie zrozumialem calej dyskusji :wink: i zgodzic sie nie moge z tym ze Rails nie lamie konwencji, lamie ja jak najbadziej bo o ile dla przykladu mamy metode save! i save, o tyle obie metody dokonuja takich samych “niebezpiecznych” operacji na obiekcie. Metoda save bez wykrzyknika zmienia dokladnie w ten sam sposob self co metoda save! roznice mamy tylko w dzialaniu i w zasadzie ! ma dla ActiveRecord oznaczac tyle ze jest to metoda NAPRWDE niebezpieczna i w razie problemow z validacja aplikacja zakonczy dzialanie i wyrzuci wyjatek. Takie zachowanie mamy chyba tylko w obrebie ActiveRecord, ale przyklad z threadsafe! to kolejny dowod na lamanie konwencji w jeszcze inny sposob.

Wogole czy istnieje przypadek w ktorym w stdlib Ruby mamy metode zakonczona ‘!’ a nie mamy jej odpowiednika bez ‘!’ ?