Jaka to metoda?
17 komentarzy | Kategorie: Narzędzia, Ruby, Techblog, Tips & tricks | trackbackTagi: ruby tips&tricks tools
Nikt nie jest nieomylny
Praca z Rubym (także Railsami i innymi rubinowymi tworami) to nie tylko pisania kodu, uruchamianie czy testowanie. To także bardzo często zabawa bezpośrednio z kodem w konsoli. Języki dynamiczne, które posiadają taką konsolę (w Rubym oczywiście to irb) dają możliwość poczucia i zrozumienia co tak na prawdę dzieje się z naszym kodem kiedy jest uruchamiany. Dają możliwość szybkiego wskoczenia 'w temat', a także bezpośredniego eksperymentowania z kodem. Namiastką tego w takich językach jak Java czy C# jest debugowanie w IDE, gdzie krok po kroku możemy przyglądać się co się dzieje w kodzie.
Im więcej pracujemy tym bardziej denerwują nas wszelkie literówki ('m' zamiast 'n', 'a' zamiast 'A' i tak dalej). Jeśli mamy włączone rozszerzenie 'readline' wystarczy nacisnąć strzałkę w górę, przesunąć kursor w odpowiednie miejsce i poprawić błąd. Okazuje się, że można jeszcze prościej. Ba! Ruby może zrobić to za nas... może nie dosłownie, ale zaraz zobaczycie o co mi chodzi.
Guess Method na ratunek
Odpowiedzią jest Guess Method, gem który stara się automatycznie wykryć błędnie wpisane nazwy metod oraz stałych. Przykładowo jeśli zamiast 1.to_s napiszemy 1.tos, błąd zostanie wykryty, a zamiast NoMethodError zostanie wywołana poprawna (domniemana) metoda 'to_s' (sic!).
Zanim zaczniemy zabawę - instalacja. Klasycznie za pomocą gem (użytkownicy windows oczywiście pomijają 'sudo').
sudo gem install guessmethod
Zobacz kilka przykładów z sesji irb:
>> require 'guessmethod'
=> true
>> Stirng.tos
attention: replacing non-existant constant Stirng with String for Object
attention: sending to_s instead of tos to String:Class
=> "String"
>> ['1','2','3'].mp {|x| x.tof}
attention: sending map instead of mp to ["1", "2", "3"]:Array
attention: sending to_f instead of tof to "1":String
attention: sending to_f instead of tof to "2":String
attention: sending to_f instead of tof to "3":String
=> [1.0, 2.0, 3.0]
>> 1.tos
attention: sending to_s instead of tos to 1:Fixnum
=> "1"
>> eixt
attention: sending exit instead of eixt to main:Object
(dodatkowo Guess Method dosyć ładnie koloruje wyświetlane komunikaty)
Trzeba od razu sobie powiedzieć jasno: to nie nadaje się do 'produkcyjnego' użycia. Żeby Ci nie przyszło do głowy używać tego w normalnym kodzie źródłowym. Chyba, że lubisz wyzwania i szukasz nowych doznań ;).
Ale jak to działa?
Nie byłbym sobą gdybym nie dowiedział się 'jak oni to zrobili'. Moje podejrzenia się sprawdziły. Algorytm jest następujący:
1. Przechwyć wywołanie nieistniejącej metody
2. Znajdź metodę, która istnieje w aktualnym obiekcie i jest 'najbliższa' metodzie, która została wywołana
3. Wywołaj znalezioną metodę lub rzuć wyjątek jeśli takiej nie ma
Punkt nr 1 jest prosty. Jak wiadomo, w Rubym można przechwycić wywołanie nieistniejącej metody za pomocą 'method_missing':
class MyClass
def method_missing(name, *args)
puts "przechwycono wywołanie: #{name} z argumentami [#{args.join(',')}]"
end
end
MyClass.new.test
MyClass.new.test(1, 2)
Punkt nr 2 wydaje się (i rzeczywiście jest) kluczowym. Po pierwsze, żeby znaleźć jakąś metodę, wpierw trzeba pobrać jakąś listę, która zostanie przeszukana. Czy już pisałem, że Ruby jest w takich sprawach po prostu świetny?
s = "hello"
puts s.methods
Teraz najtrudniejsza rzecz. Jak opisać 'najbliższą metodę'? Mózg człowieka potrafi w jednej chwili skojarzyć, że 'tos' to prawdopodobnie pomyłka i powinno być 'to_s', ale jak ma to zrobić program? Z pomocą przychodzi algorytm wyliczający odległość Levenshteina. Jest to algorytm, który dla zadanych dwóch wejściowych słów s1, s2 wyznacza pewną miarę ich odmienności (odległości). W praktyce algorytm wyznacza ilość kroków, takich jak usunięcie, dodanie i zamiana znaku, tak aby przekształcić słowo s1 na s2. Przykładowo aby przekształcić nasze 'tos' w 'to_s' potrzeba 1 kroku: dodanie '_' na 3 pozycji. Dodatkowo można ustalać pewne wagi dla operacji wstawiania, usuwania i zamiany. Po co? Spójrz na klawiaturę, zrobienie literówki 's' zamiast 'a' jest bardziej prawdopodobne niż 'z' zamiast 'p'!
W tym momencie sprawa powinna być prosta. Mając błędną metodę o nazwie s i zbiór X wszystkich metod obiektu (na którym metoda została wywołana) obliczamy wszystkie odległości dla pary [s, x], gdzie x jest słowem ze zbioru X. Następnie wybieramy najmniejszą odległość i.. gotowe :).
Dodam jeszcze, że algorytm Levenshteina może posłużyć do implementacji własnego spell-checkera. Ale to już wiecie, prawda?
Implementację algorytmu Levenshteina w Rubym znajdziecie pod adresem: http://raa.ruby-lang.org/project/levenshtein/. Dostępne także jako gem (http://rubyforge.org/projects/text).
Tak co problemu „literówki w nazwie metody”, to do eliminowania takich rzeczy używa się IDE, w trakcie pisania kodu. Żeby potem nie tracić czasu.
I jeśli mówimy o namiastkach, to raczej irb nazwałbym namiastką prawdziwego debuggera. Jako interaktywna konsola jest super, ale porównajmy np. czynności potrzebne by ustawić „conditional breakpoint” w Javie pod Eclipsem z tą samą czynnością w irb. Albo nawigowanie po stosie wywołań metod…
Ech, jak ja bym chciał porządny zestaw narzędzi do pisania w rubym/pythonie…
@Hoppke, Nie czytałeś dokładnie, albo nie zrozumiałeś. Chodzi o używanie interaktywnej konsoli irb. Nie nazwałem irb debuggerem.
Czego Ci brakuje?
Porównujesz dwie nieporównywalne rzeczy. Wierszowy interpreter niektórych języków skryptowych służy do zupełnie innych rzeczy niż debugger. Pokaż mi, jak w takim interpreterze chcesz zatrzymać działanie programu po wejściu do jakiejś funkcji, żeby obejrzeć jej argumenty i aktualne wartości zmiennych globalnych, bo to jest nagminna operacja przy debuggowaniu.
Dobra, przykład nie jest może zbyt dobry. Ale chodziło mi o to, że java czy c# nie mają interaktywnej konsoli, gdzie na bieżąco można wpisywać kod i wykonywać go. w Javie natomiast, możesz napisać wpierw kod, a potem go debugować krok po kroku, patrząc np co zwracają metody itp. Pomijam tu sam debugowania, czyli szukania błędów. Irb oczywiście do tego nie służy.
Sorry.
Mówienie o irb i debuggerze w jednym zdaniu mnie zmyliło.
A czego bym chciał? Jakiegoś fajnego IDE, w którym możnaby napisać duży projekt. Z sensownym debuggerem (takim jakie są do Javy). Z uprzyjemniaczami takimi jak np. refaktoryzacje (zmieniam nazwę jakiejś klasy, czy przenoszę metodę z jednej klasy do drugiej i chciałbym, żeby kod automatycznie poprawiło). Albo mam metodę która przyjmuje 5 parametrów, i chcę z nich zrobić jeden obiekt-pojemnik… i żeby mi to automat zrobił w całym kodzie, stworzył nową klasę-pojemnik, dodał jej instancje wszędzie gdzie poprzednio wywoływałem daną metodę... no, takich rzeczy mi brakuje.
Plus wszelkiej maści profilery.
W miarę jak projekt się rozrasta coraz trudniej nad nim zapanować bez odpowiednich narzędzi. I dlatego nadal tak wiele projektów pisze się w językach toporniejszych niż Ruby czy Python… :(
Hoppke, to ty chcesz języka ze statycznym typowaniem. W ogólnym przypadku nie wyobrażam sobie algorytmu refaktoryzacji dla języka z dynamicznym systemem typów.
Próbowałeś Netbeans 6.0? Ma podpowiadanie kodu, robią coś w kwestii refaktoryzacji(*), ma debugowanie wizualne (tak, odpalasz server, ustalasz breakpoint, odpalasz przeglądarkę i jesteś w debuggerze :)). Profilowanie też widziałem, że jest możliwe. Może kiedyś coś napiszę o tym.
*Trzeba zawsze wziąć na poprawkę to, że języki dynamiczne nigdy nie będą tak łatwo dawały się refaktoryzować jak statyczne. To wynika z ich natury. Tutaj raczej trzeba pomyśleć o TDD czy BDD
Bez środowiska developerskiego dorównującego tym używanym do Javy czy .NET Ruby nadal będzie traktowany jak „taki lepszy PHP”.
Bez refaktoryzacji ciężko zarządzać większym projektem. Da się, ale ogranicza to wolność w wprowadzaniu rozleglejszych modyfikacji kodu. I siłą rzeczy koder zaczyna szukać rozwiązań, które da radę wprowadzić. Bo jak wiadomo praca programisty to obok kodowania głównie pałowanie się z narzędziami, a każdy kombinuje jak się najmniej namęczyć ;)
Ale zrozum, że to jest język dynamiczny. Tu nigdy nie będzie pełnej refaktoryzacji. Są ludzie, którzy uważają, że potężne narzędzia (CASE?) załatwią za niego sprawę. Inni z kolei (Ci pragmatyczni) biorą sprawy w swoje ręce i piszą. No i trzeba sobie powiedzieć jasno: Ruby nie ma zamiaru wkraczać tam gdzie Java ma się dobrze, ale raczej tam gdzie użycie Javy jest toporne (np aplikacje webowe).
„Ale zrozum, że to jest język dynamiczny. Tu nigdy nie będzie pełnej refaktoryzacji.”
Smalltalk.
@sztywny, o smalltalku mam blade pojęcie, więc nie wiem co dokładnie chciałeś powiedzieć... ale Ruby nigdy nie będzie miał w pełni funkcjonajlną refaktoryzację (tzn. żadne IDE tego nie da). Jeśli się mylę to rozwiń proszę myśl :).
Pierwsze narzędzia do automatycznej refaktoryzacji powstały właśnie w Smalltalku i z tego co wiem to do dziś wiele innych języków może tych narzędzi Smalltalkowi pozazdrościć. Jest to przy tym język tak dynamiczny jak tylko się da, dynamika Rubego wiąże się zresztą w dużej mierze z inspiracjami z tego języka. Ruby ma jednak dużo bardziej złożoną i elastyczną składnie, co jest dodatkowym problemem przy opracowywaniu tego typu narzędzi. Podsumowując: dynamika nie musi być przeszkodą dla tworzenia takich narzędzi, oraz uważam że mocno przesadziłeś z „nigdy” – dotąd nikt się poważnie rozwojem takich narzędzi dla Ruby nie zajmował, wystarczyło że pare miesięcy temu wziął się za to Sun z NetBeansem i już jakąś tam bazę mają. Gdyby pracował przy tym taki zespół jak np. budował refactoring w Eclipse, to pewnie byliby jeszcze dalej.
Pytanie czy w języku takim jak Ruby te narzędzią wogóle są aż tak bardzo koniecznie niezbędnie potrzebne. Steve Yegge bardzo ładnie to opisał:
http://steve.yegge.googlepages.com/transformation
Obecnie w programowaniu bardzo modne jest podejście „jak najszybciej przygotuj coś, co z grubsza działa, a potem to rozbudowuj” (no i „release early, release often”, model krótkich iteracji i częstego pokazywania aplikacji klientowi do akceptacji itp.).
Refactoring jest tu nieocenioną pomocą. Jasne, da się i bez tego pisać kod (tak samo jak da się i bez podświetlania/dopełniania składni i automatycznego formatowania kodu), ale fajnie gdy ma się takie dodatki.
Porównanie refactoringu do podświetlania kodu i auto-indentu mnie zabiło :] Obawiam się, że nie przeczytałeś linka, więc w skrócie: refactoring to NIE JEST nazwa-parasol dla narzędzi transformujących kod wbudowanych w IDE. To pewien zestaw przekształceń, które są opisane na tyle ściśle, że niekiedy można ja zautomatyzować, ale równie dobrze można aplikować je ręcznie – ma to nawet pewne przewagi, widać wtedy jaki jest ich cel i mechanika działania.
No chyba że pisząc „Refactoring”, miałeś na myśli „Automatyczny refactoring”, wtedy przepraszam za wykład. Chociaż nie do końca rozumiem co same te narzędzia miałby mieć wspólnego z krótkimi iteracjami.
@sztywny: tekst przeczytałem. Widać byłem niedostatecznie wyraźny :) Więc:
Zacznę od fragmentu definicji refactoringu z wikipedii: „In extreme programming and other agile methodologies, refactoring is an integral part of the software development cycle: developers alternate between adding new tests and functionality and refactoring the code to improve its internal consistency and clarity. Automatic unit testing ensures that refactoring does not make the code stop working.”
Refactoring jest po prostu częścią procesu tworzenia kodu – przytoczony wcześniej linkowany tekst opiera się na książce Fowlera i sprowadza refactoring do zamieniania „bad code” w „good code” (spłycając strasznie to, co książka próbuje przekazać) i sugeruje, że jeśli od razu napiszesz good code, to refactoring jest zbędny. Wydaje mi się, że tamten pan nie miał okazji by pracować w środowisku, gdzie refactoring nie jest tylko teorią (inaczej nie odwoływałby się tylko do swojej interpretacji książki – której zresztą praktycznie żaden z jego kolegów inżynierów nie czytał, co chyba najlepiej świadczy o środowisku w którym przyszło mu się obracać).
Bo ja widzę w tekście wnioski, które osobiście uważam za błędne – bo po pierwsze refactoring to nie tylko przerabianie zepsutego kodu na dobry, po drugie nawet pisząc idealny (w danym momencie) kod i tak możesz mieć refactoring wpisany w proces i będzie to tylko świadczyło o tym, że kod/produkt rozwija się prawidłowo. Jak to możliwe? Ano, w Agile to norma.
W agile refactoring to nie jest „ratowanie” kodu ani jego „naprawianie”. To najzupełniej normalna część cyklu tworzenia oprogramowania. Pisanie testów, pisanie kodu, ew. refactoring, pisanie/zmienianie testów, pisanie/zmienianie kodu, ew. refactoring. I to wszystko jeszcze ujęte w ramach iteracji, po których zwiększa się szansa na konieczność zrefaktoryzowania kodu.
Refactoring będzie miał tu na celu podniesienie jakości kodu, ale również po prostu jego przetworzenie tak, by spełnił nowe wymogi. W agile wymogi mogą zmieniać się bardzo szybko, z jednej iteracji na drugą. Ta metodologia wręcz zakłada gotowość do przeprowadzania małych rewolucji w kodzie całkiem regularnie. I niezależnie od użytego języka będzie to wymagało jakiejś pracy.
Automatyczny refactoring oferowany przez IDE to mechanizmy z których koder może skorzystać w swojej pracy. I przydają się tak samo jak dopełnianie składni, formatowanie tekstu czy integracja z frameworkami od testów jednostkowych.
Myśle, że główną myślą artykułu było to, że ludzie zbyt często mylą „Refactoring”, z klikanymi narzędziami w IDE które go automatyzują, a że wpis niżej odniosłeś ten termin do wcinania / podświetlania… Teraz rozumiem co miałeś na myśli, szacun ;)