Entries from Październik 2008 ↓

Boolean w Ruby on Rails czyli czy chciałbyś zaoszczędzić 2 dni swego życia?

booleanChciałbym, żeby mi ktoś zaoszczędził 2 dni z mojego życia. Jeden z weekendów mógłby być o 2 dni dłuższy. Wyjazd na deskę mógłby trwać tyle ile by się chciało. Byłoby więcej czasu na czytanie książek, spędzanie czasu z bliskimi itp.
Ten „ktoś” jednak postanowił mi te 2 dni zabrać zamiast sprezentować… A tym „kimś” jest sposób interpretacji kolumny boolean przez Ruby on Rails.

Nie ma co jednak wybiegać do przodu, więc wszystko po kolei.

We wszystkich swoich dotychczasowych projektach używałem bazy MySQL, więc na tym będę bazował swoje rozważania.

Tworzę więc sobie model Person i daję mu atrybut friend, który może być true albo false. W migracjach, zgodnie z konwencją Rails, w modelu Person przy polu :friend daję typ :boolean. Wszystko na razie idzie zgodnie z planem. Po wykonaniu rake:db pojawia się tabela „people” i w niej pole ‘friend’ ma typ tinyint(1). Tu w sumie też wszystko idzie z planem, gdyż kolumna o typie tinyint(1) przez ActiveRecord rozpoznawana jest jako ‘boolean’.

MySQL ciągle się nie dopracował prawdziwego typu ‘boolean’, więc nawet w natywnym MySQL gdy się tworzy kolumnę typu BOOLEAN, to jest to równoznaczne ze stworzeniem kolumny o typie tinyint(1). Wartość zerowa jest traktowana jako false, a każda inna wartość jako true. (MySQL 5.0 Reference Manual – Overview of Numeric Types)

No i gdybym nie był „sprytniejszy”, niż trzeba, albo może gdybym kodował to nie będąc w pełni sił umysłowych, to nie byłoby żadnego problemu. Będąc jednak pomysłowym Dobromirem spojrzałem w strukturę bazy danych, zauważyłem typ tinyint(1), doszedłem do wniosku, że ani true ani false w tinyint się nie zmieści, więc będą tam tylko zera lub jedynki. I dalej szczęśliwie wrzucałem sobie do kodu tu i ówdzie takie oto kwiatki:

if Person.friend == 1 ...
if Person.friend == 0 ...

W kodzie miałem oczywiście zmienne, a nie nazwy modeli, ale podaję dla zobrazowania. W każdym bądź razie te kawałki aż prosiły się o poprawienie, ale nów umysłowy (antonim pełni) był silniejszy i z zadowoleniem pisałem sobie dalej.
Można łatwo sobie dopowiedzieć czym to się skończyło. Absolutnie bezsensownymi dwoma dniami, gdzie aplikacja „dziwnie się zachowywała”.

Jeśli więc ktoś kiedyś również będzie pisał kod nie wkładając w niego za wiele myśli (z braku sił czy też jakiegokolwiek innego powodu), to chciałbym, żeby poniższe przykłady zapadły dostatecznie w pamięć, żeby napisać to dobrze. Jak się na to patrzy, to wszystko to powinno być proste i logiczne, ale rzeczy proste i logiczne daleko nie zawsze są proste.

Znajdujemy więc sobie dwie osoby, jedną która jest moim przyjacielem, a druga – niekoniecznie.

t = Person.find(:first, :conditions => ["..."])     # atrybut friend o typie tinyint(1) ma wartość 1
f = Person.find(:first, :conditions => ["..."])     # atrybut friend o typie tinyint(1) ma wartość 0

Sprawdzamy czy te osoby naprawdę są przyjaciółmi czy też nie.

t.friend    # true
f.friend    # false

t.friend?    # true
f.friend?    # false

I właśnie sposób powyższy (najlepiej ze znakiem zapytania, bo jest bardziej czytelny i jasny w przekazie) jest prawidłowym i najlepszym sposobem na testowanie wartości boolean w Rails. Prawda, że proste?
Warto jednak rozpatrzyć jeszcze kilka przykładów, które czasami dają nam rezultaty, których oczekujemy, a czasem nas zaskakują. I to w niemiły sposób.

t.friend.blank?     # false
f.friend.blank?     # true

t.friend.empty?     # NoMethodError: undefined method `empty?' for true:TrueClass

f.friend.empty?     # NoMethodError: undefined method `empty?' for false:FalseClass

Tutaj blank? działa, natomiast empty? odmawia współpracy. Jednak mimo iż blank? działa poprawnie, to jest to raczej niefortunny i nieklarowny zapis, więc lepiej go unikać.
Poniżej zaś to, co próbowałem robić w swoim kodzie. Dziecinny błąd. Children, don’t try this at home! :)

t.friend == 0     # false
t.friend == 1     # false
t.friend == "0"   # false
t.friend == "1"   # false

t.friend.to_i   # NoMethodError: undefined method `to_i' for true:TrueClass
f.friend.to_i   # NoMethodError: undefined method `to_i' for false:FalseClass

Jak widać ani porównywanie tego do wielkości integer, ani string nie daje oczekiwanych rezultatów. Nawet próba konwersji wyrzuca błąd. Wniosek: nigdy nie porównywać boolean do wielkości liczbowych (i konkretnie do zer i jedynek).
Można porównywać je do wartości true/false (ale koniecznie nie jako string), ale to też jest mało eleganckie.

t.friend == true  # true
f.friend == true  # false

t.friend == "true"    # false
f.friend == "true"    # false
t.friend == "false"   # false
f.friend == "false"   # false

t.friend.to_s   # "true"
f.friend.to_s   # "false"

Jeśli więc ktoś kiedyś dzięki powyższemu użyje formy prawidłowej zamiast niepoprawnej i zaoszczędzi przynajmniej kilka godzin (nie mówiąc już o dwóch dniach), to… przynajmniej nie będzie musiał pisać postów rozpoczynających się słowami „Chciałbym, żeby mi ktoś zaoszczędził 2 dni z mojego życia”.

Ruby on Rails: szlachetny kamień na szynach czy duchowny w pociągu?

Ruby vs RabbiPewnego słonecznego dnia szedł sobie stary i mądry rabin przez pola i łąki. Rozmyślał nad Torą, nad niebezpiecznie rozluźniającymi się obyczajami w swojej gminie (młodzi już nie kłaniają się starszym tak nisko jak drzewiej bywało) i nad nadmiernie wzrastającym autorytetem rabina z sąsiedniej gminy za rzeką. Będąc dotychczas najbardziej szanowaną osobistością w całej okolicy między rzeką a tą miejscowością, dokąd czasem się zapuszczał dokonać bardziej nietypowych zakupów, niż chleb czy mięsiwo (np. kałamarze czy płótno), oczywistością było, że rosnący u boku autorytet jest co najmniej powodem do niespokojnego snu w nocy i podczas siesty. Idąc więc przez łąkę i bezwiednie muskając dłonią czubki wyższych traw, podświadomie poszukiwał „znaku”, który by go wewnętrznie umocnił w przekonaniu, że jego pozycja jest niezagrożona i że nieoczekiwane drgawki nie będą go wyrywały z błogiego stanu siesty.
Tak niepostrzeżenie doszedł aż do nasypu, który biegł wzdłuż rzeki. W swym zamyśleniu i trosce zabrnął dalej, niż zwykle. Wspiął się więc po nasypie (a trzeba zaznaczyć, że nie był to nasyp zwykły, lecz kolejowy) i wzdłuż tego nasypu podążył powoli z powrotem w kierunku swej siedziby.
Aż tu raptem z zadumy wyrwał go nieoczekiwany czerwony blask, strzelający wprost spod nóg. Ku jego bezgranicznemu zdumieniu był to… dorodnej wielkości rubin leżący wprost na torach kolejowych.
- Cóż za wspaniały rubin! – wykrzyknął rabin, podniósł go z torów i już ze spokojnym umysłem i uśmiechem na twarzy dziarsko potruchtał w kierunku unoszącego się dymu, zwiastującego rychły obiad.

A jaki jest morał tej historii?
Otóż apeluję szczerze, żebyśmy nie zamieniali ról w tej historii i nie pozwalali rubinowi znaleźć rabina na torach, tylko pozostawili to w takiej wersji jak powyżej, nawet nie zważając na fakt, że nie jest to najwybitniejsza przypowieść tego roku.

Jak bowiem brzmiałoby kluczowe zdanie przypowieści w języku angielskim?
- What a nice ruby! – exclaimed rabbi, took it off the rails and merrily trotted home. (tłum. wolne)

Ze szczegółami:
[hwuht] [a] [nahys] [roo-bee]! – [ik-skleymd] [rab-ee]
Źródło: Dictionary.com Unabridged (v 1.1)

[roo-bee] (pol. rubi) – Rubin
[rab-ee] (pol. rabi) – Rabin

Jeśli więc jesteśmy w tej branży, programujemy w tym języku, inwestujemy w projekty związane z tą technologią czy wręcz posiadamy firmy, które się zajmują programowaniem m.in. w tym języku, to ja bardzo proszę… nie mieszajmy w to religii i duchownych :)