Krótkie i niekompletne omówienie języka Ruby.

Ruby jest wysokopoziomowym, dynamicznym, obiektowo zorientowanym językiem programowania, wzorowanym przede wszystkim na takich językach jak Smalltalk, Perl, Python, czy też Lisp. Niniejszy artykuł zawiera wybiórcze i niepełne przedstawienie cech języka Ruby. Szczegółowy opis języka jest przedmiotem innych, często dość obszernych, opracowań, np. [1].

Historia

Rodowód

Język Ruby został opracowany w Japonii w 1993 roku, przez Yukihiro Matsumoto. Dwa lata później jego pierwszy interpreter (który wcześniej służył autorowi języka jedynie w jego pracy) ujrzał światło dzienne. Przez długi czas Ruby był szerzej znany jedynie w Japonii, gdzie swoją popularnością przerósł takie popularne w Europie i Ameryce języki jak Python, czy też Perl.

Swoją drugą młodość Ruby przeżył, gdy natknęli się na niego David Thomas i Andrew Hunt, autorzy entuzjastycznie odebranej książki Pragmatyczny Programista, w poszukiwaniu języka do prototypowania i specyfikacji. Od tamtej pory, jak twierdzą, używali go w każdym ze swoich projektów [2]. Zaowocowało to wydaniem kolejnej książki [3], która pomogła spopularyzować język poza Japonią.

Jedną z osób, które zaczęły wykorzystywać Ruby w swojej codziennej pracy był David Heinemeier Hansson, który za jego pomocą zrealizował aplikację internetową Basecamp. Z tego samego kodu następnie powstał szkielet aplikacyjny do tworzenia aplikacji internetowych o nazwie Ruby on Rails, którego pierwsza wersja została udostępniona publicznie w 2004 roku na otwartej licencji MIT.

Aktualnie Ruby istnieje w wersji stablinej 1.8.x oraz przygotowywana jest nowa wersja stabilna, oznaczona numerem 1.9.1.

Inspiracje

Ruby nie jest, wbrew opiniom wielu entuzjastów, podejściem rewolucyjnym, lecz ewolucyjnym. Jego siła polega na połączeniu wielu cech obecnych
w innych językach w jeden, spójny i sprawny mechanizm. następne podsekcje zawierają krótką analizę inspiracji twórców języka Ruby.

Smalltalk

Smalltalk był jednym z pierwszych w pełni obiektowych języków programowania. Oprócz obiektowego podejścia, charakteryzuje się w również specyficzną ideą wywoływania metod opartą na koncepcji wiadomości oraz oryginalną składnią, która w zasadzie nie znalazła potem naśladowców (w przeciwieństwie do składni języka C, na której bazują np. C++, C#, Java, D, PHP etc.). Ruby czerpie wiele ze swoich głównych cech właśnie z języka Smalltalk.

Oto niektóre z nich:

  • Kod wykonywany jest przez maszynę wirtualną.
  • Kod jest interpretowany, a nie kompilowany.
  • Typy określane są dynamicznie - brak deklaracji, jakiego typu są zmienne i stałe - jest to określane na etapie wykonania.
  • Wszystkie klasy dziedziczą po bazowej klasie Object. Ilustruje to program przedstawiony poniższym listingu, wypisujący nazwę nadklasy obiektu klasy String.
        puts "napis".class.superclass # => "Object"
  • Ruby posługuje się semantyką referencji - podczas przepisywania jednego obiektu do drugiego, czy też przekazywania obiektów do metod, kopiowane są wskaźniki, a nie wartości. Oznacza to, że w efekcie wykonania programu przedstawionego na poniższym listingu, zarówno zmienna a jak i b będą miały wartość „popis”.
a = "napis"
b = a                 # Przepisanie referencji 
a.gsub!("na", "po")   # Inwazyjna zamiana "na" na "po"
puts b                # => "popis"

Perl

Perl jest bardzo popularnym językiem, najczęściej używanym do pisania skryptów administracyjnych oraz do automatyzacji operacji na tekście. W wymienionych zastosowaniach sprawdza się dzięki zestawom wielu dedykowanych skrótów, operatorów i zmiennych globalnych, nastawionych na operacje na tekście oraz pozyskiwanie wyników działania aplikacji zewnętrznych.

Ruby odziedziczył wiele z możliwości Perla w tym zakresie, jednak większość z istniejących praktyk nawiązujących do składni Perla jest już niezalecanych (wg [1]). Tym niemniej wciąż dostępny jest m.in. znany z Perla operator dopasowania do wyrażeń regularnych, oraz literały samych wyrażeń regularnych. Ilustruje to przykład przedstawiony poniżej.

tablica = ["Kasia", "Basia", "Jola"]
wyrazenie = /(.+)asia/       # literał wyrażenia regularnego
tablica.each do |element|
  if element =~ wyrazenie    # operator dopasowania
    puts $1                  # wypisze "K", potem "B"   
  end
end

Lisp

Lisp jest językiem funkcjonalnym, a nie, jak Ruby, imperatywnym. Mimo to Ruby ma bardzo mocne korzenie w Lispie, co przyznał nawet sam Yukihiro Matsumoto (wg [4]). Główne elementy, jakie można tu wyróżnić, to:

Podobieństwa składni.

Dla porównania na listingu poniższych dwóch listingach przedstawiono analogiczne fragmenty kodu w językach Lisp i Ruby.

(defun foo (n) (lambda (i) (incf n i)))
def foo(n) lambda { |i| n+=i } end

Zastosowanie domknięć.

Najbardziej przejrzysta definicja domknięcia w języku programowania, z jaką się spotkałem ([5]) wymienia trzy cechy, które musi spełniać blok kodu, by stał się domknięciem:

  • Może być przekazywany między kontekstami wykonania,
  • Może być wykonany na żądanie w kontekście, w którym aktualnie się znajduje,
  • Może odnosić się do zmiennych z kontekstów wykonania, w których został utworzony („zamyka” te zmienne).

Niżej zamieszczony listing pokazuje przykład, na którym zmienna lokalna zostaje dowiązana do bloku kodu i każda zmiana tej wartości zostanie również odwzorowana w wykonaniu tego bloku.

#kontekst globalny
 
def utworz_domkniecie
  #lokalny kontekst wykonania funkcji
  a = "jestem "
  # utworzenie domknięcia
  domkniecie = lambda { puts a }
  # modyfikacja 'a' po utworzeniu domknięcia
  a += "z innego zasiegu"
  return domkniecie  
end
 
#powrót do kontekstu globalnego
nowe_domkniecie = utworz_domkniecie
nowe_domkniecie.call #=>"jestem z innego zasiegu"

Python

Python jest jednym z najbardziej popularnych wysokopoziomowych języków dynamicznych. Zawdzięcza to swojej prostocie (a jednocześnie sporej elastyczności), czytelności i dostępności na wiele różnych platform. Python wykorzystywany jest do tworzenia aplikacji internetowych, testowania
interfejsów użytkownika, pisania skryptów rozszerzających istniejące aplikacje (np. Gimp, Blender) oraz w wielu innych zastosowaniach.

Zarówno Ruby jak i Python, mimo różnego podejścia do fundamentalnych koncepcji, są dynamiczne, obiektowe i operują na podobnym poziomie abstrakcji. Dlatego też nic dziwnego, że w obu językach przebijają się te same koncepcje budowy bibliotek i rozwiązywania typowych problemów. Poniżej zamieściłem dwa listingi - pierwszy w języku Python, a drugi w języku Ruby - prezentujące ten sam program. Ma on za zadanie przemierzenie rekursywne struktury katalogów wgłąb i wypisanie nazw elementów tej struktury. Mimo nieznacznych różnic w bibliotekach, konstrukcja programu praktycznie się nie zmieniła.

#!/usr/bin/python
from os import *
from os.path import *
 
def printItems(path):
    for item in listdir(path):
        try:
            itemPath = join(path, item)
            pathIsDir = isdir( itemPath )
            pathIsLink = islink( itemPath )
            pathIsFile = isfile( itemPath )
            pathExists = exists( itemPath )
            if( pathExists ):
                if( pathIsLink ):
                    print "Link:      " + itemPath
                elif( pathIsDir ):        
                    print "Directory: " + itemPath
                    printItems(itemPath)
                elif( pathIsFile ):
                    print "File:      " + itemPath
                else:
                    raise Exception( "Wrong path: " + itemPath )
        except OSError, error:
            if(13 == error.errno):
                print "No permissions to visit " + itemPath
 
printItems("/home/astral/Download")
#!/usr/bin/ruby -w 
require 'pathname'
 
def print_items(path_string)
  path = Pathname.new(path_string)  
  for item in path.children
    begin
      item_path = path.join(item)
      path_is_link = item_path.symlink?
      path_is_dir = item_path.directory?
      path_is_file = item_path.file?
      path_exists = item_path.exist?
      if( path_exists)
        if( path_is_link )
          puts "Link:      " + item_path
        elsif( path_is_dir )        
          puts "Directory: " + item_path
          print_items(item_path)
        elsif( path_is_file )
          puts "File:      " + item_path
        else
          raise Exception.new( "Wrong path: " + item_path )
        end
      end
    rescue Errno::EACCES
      puts "No permissions to visit " + item_path
    end
  end
end
 
print_items("/home/astral/Download")

Cechy języka

Ruby posiada cały szereg cech które wyróżniają go na tle innych języków. Sekcja ta omawia dwie moim zdaniem najciekawsze: bloki i metaprogramowanie.

Bloki

W języku Ruby bloki kodu można zamykać w zmiennych i przekazywać między kontekstami wykonania. Nie jest to nowość - możliwe jest to w takich językach jak C (wskaźniki do funkcji), czy też C\# (delegaty). Jednak Ruby traktuje bloki kodu w specjalny sposób, udostępniając cechy, które zdecydowanie ułatwiają manipulację takimi blokami, np.

  • Każdej metodzie można przekazać jako ostatni argument blok kodu, nawet, jeśli taka metoda w ogóle nie była napisana z myślą o korzystaniu z bloków - wtedy taki blok nie zostanie wykonany. Taką sytuację pokazuje poniższy fragment kodu, w którym zdefiniowana jest metoda nie korzystająca z bloku. Pomimo to, taki blok został metodzie przekazany i nie wywołało to błędów interpretacji.
# Definicja metody nie korzystającej z bloku
def metoda_nie_korzystajaca_z_bloku(numer)
  puts numer
end
 
# Wywołanie metody z przekazaniem bloku
metoda_nie_korzystajaca_z_bloku(5) do
  puts "ten blok nie zostanie wykonany."
end # => 5
  • Można zdefiniować anonimowy blok kodu podczas wywołania funkcji. Służy do tego słowo kluczowe do. Przykład takiej definicji widać na wyżej zamieszczonym listingu, w linijkach od szóstej do ósmej (parę słów kluczowych do i end można zastąpić otwierającą i zamykającą klamrą).
  • Wywoływanie takich bloków w ciele metody odbywa się za pomocą specjalnej metody yield. Ilustruje to następujący kod, gdzie metoda ta wykonana jest dwukrotnie, tym samym dwa razy wykonując przekazany metodzie blok.
def twice         # Metoda wywoła podany blok dwa razy
  yield           # Wykonaj blok
  yield           # Wykonaj blok
  return
end
 
twice do 
  puts "wykonano" 
end               # => "wykonano" (dwa razy)
  • Ułatwione jest przekazywanie przez kontekst wywołujący parametrów do bloku anonimowego za pomocą parametrów metody yield. Przykład:
def zrob_cos_ze_zmienna
  zmienna = 12
  yield zmienna  # Przekazanie zmiennej do bloku 
end
 
zrob_cos_ze_zmienna do |zmienna| # Pobranie zmiennej w bloku
  puts zmienna + 10              # => 22
end

Bloki rozumiane w ten sposób mają szereg zastosowań. Poniżej omówię dwa z nich: bloki transakcyjne oraz wspólna inicjalizacja.

Bloki transakcyjne

Język Ruby nie wspiera znanych z niektórych obiektowych języków programowania destruktorów. Oznacza to, że każdy zasób, taki jak połączenie z bazą danych, należy jawnie zdezaktywować. Można to zrobić za pomocą bloku ensure, który działa identycznie jak np. blok finally w języku Java - wykonuje się zawsze, bez względu na to, czy wykonanie poprzedzającego bloku zostało przerwane, czy zakończyło się pomyślnie. Poniżej przykład otwarcia strumienia do pliku, następnie zapisania do niego oraz w końcu zamknięcia tego strumienia, przy użyciu bloku ensure.

plik = nil 
begin
  plik = File.open("plik.txt", "w") 
  plik.write("...zawartosc pliku...")
ensure
  plik.close unless plik.closed?
end

Ręczne zwalnianie zasobów, szczególnie w przypadkach, gdy nie potrzebujemy przenosić otwartego zasobu pomiędzy kontekstami wykonania (np. otwieramy zasób w funkcji i zamykamy go przed jej zakończeniem) może być niewygodne, albo też wręcz zapomniane. Dlatego też, aby uprościć kod, w języku Ruby można zastosować idiom zwany blokiem transakcyjnym. Idiom ten zazwyczaj implementowany jest w ten sposób, że udostępniana jest metoda, która tworzy zasób, udostępnia go przekazanemu blokowi, po czym sama zajmuje się zamknięciem takiego zasobu. Poniżeszy fragment kodu ilustruje udostępnienie i użycie hipotetycznego zasobu za pomocą ww. idiomu.

class Zasob
  def self.otworz
    zasob = Zasob.new zasob.otworz begin
      yield zasob
    ensure
      zasob.zamknij
    end
  end
 
  def wypisz(argument)
    puts argument
  end
 
  def otworz
    puts "zasob otwarty"
  end
 
  def zamknij
    puts "zasob zamkniety"
  end
end
 
Zasob.otworz do |zasob|    #=> "zasob otwarty"
  zasob.wypisz("operacja") #=> "operacja"
end                        #=> "zasob zamkniety"

Idiom bloku transakcyjnego jest powszechnie stosowany w bibliotekach języka Ruby jak również w Ruby on Rails, gdzie służy m. in. dla zapewnienia finalizacji (tzn. zaakceptowania bądź cofnięcia) transakcji bazodanowych.

Blok jako zapewnienie wspólnego kontekstu

Mechanizm obsługi bloków może posłużyć jako środek do zapewnienia pewnego rodzaju wspólnego kontekstu. Np. podczas implementacji pewnej aplikacji w Ruby on Rails wiele razy spotkałem się z sytuacją, kiedy każda z metod klasy obsługującej żądania HTTP potrzebowała podobnej sekwencji inicjalizacyjnej. Sekwencja ta zależała od parametrów żądania i nie można jej było umieścić w konstruktorze klasy obsługującej te żądania. W celu zaadresowania tego problemu wykorzystałem mechanizm bloków, by zapewnić wspólną inicjalizację dla funkcjonalności każdej z metod. Metody te z kolei przekazywały kod specyficzny dla swojego zadania jako blok do metody odpowiedzialnej za inicjalizację i finalizację wszelkich potrzebnych pól klasy. Poniższy listing przedstawia, pochodzący właśnie z kodu tej aplikacji, przykład takiej metody wspólnej inicjalizacji oraz jednej z metod, która taką inicjalizację wykorzystuje.

# Definicja wspólnej inicjalizacji
def with_common_init
  Event.transaction do
    @locations = Location.find(:all) 
    @parent = parent_object
    yield
  end
end
 
# Wykorzystanie wspólnej inicjalizacji
def index
  with_common_init do
    @events = Event.find(:all)
 
    respond_to do |format|
      format.html # index.html.erb 
      format.xml  { render :xml => @events }
    end
  end
end

Istnieje jeszcze wiele różnych zastosowań dla mechanizmu bloków w języku Ruby. Można je znaleźć na technicznych blogach bądź też w literaturze.

Metaprogramowanie

Metaprogramowanie najczęściej określane jest jako pisanie kodu, który generuje nowy lub modyfikuje istniejący kod innych programów (lub też swój własny) [6]. Możliwość pisania takiego kodu posiada większość znanych autorowi języków interpretowanych (m. in. Perl, Python, PHP, ActionScript, JavaScript), a także niektóre języki kompilowane (przykładem mogą być szablony języka C++, które podczas kompilacji służą do wygenerowania wielu różnych wersji konkretnych danego szablonu np. w zależności od tego, dla ilu typów zostały zastosowane).

Możliwość metaprogramowania najczęściej udostępniana jest na dwa sposoby:

  • Udostępnianie interfejsu programistycznego pozwalającego programiście dostać się do wewnętrznych danych programu i środowiska wykonania (metadanych) oraz zmodyfikować te dane.
  • Generowanie fragmentów kodu w trakcie wykonania i następnie wykonywanie tych wygenerowanych fragmentów (często przez tę samą aplikację, która ten kod wygenerowała).

Język Ruby oferuje obie te możliwości.

Metadane

Ruby pozwala sięgnąć prosto do tablicy symboli interpretera, skąd można pobrać informacje o wszystkich zmiennych znajdujących się w danym kontekście, listę klas, które dziedziczą po wybranej klasie, czy też wszystkich instancji danej klasy. Przykład możliwości metaprogramowania posłuży ilustruje poniższy fragment kodu, który przedstawia krótki program sięgający do tablicy symboli i wypisujący wszystkie znane podklasy podanej klasy. Kod ten ma na celu jedynie zademonstrowanie idei wykorzystania metaprogramowania w języku Ruby. Identyczny efekt można osiągnąć za pomocą jednego wywołania metody subclasses na danej klasie.

def wypisz_podklasy(klasa)
  Object.constants.each do |nazwa_z_tablicy| # wszystkie stałe, w tym nazwy klas
    potencjalna_podklasa = Object.const_get nazwa_z_tablicy
    if potencjalna_podklasa.respond_to? "ancestors" #jeśli pobrany obiekt ma metodę ancestors()
      if potencjalna_podklasa.ancestors.include? klasa #jeśli lista nadklas zawiera nazwę naszej klasy
        puts potencjalna_podklasa
      end      
    end
  end  
end
 
wypisz_podklasy(Numeric)

Tę właściwość wykorzystałem swojego czasu w aplikacji opartej o Ruby on Rails podczas sprawdzania przywilejów do danego zasobu bez wiedzy o tym, jakiej klasy jest ten zasób. Rozwiązanie to polega na pobraniu z parametrów żądania HTTP nazwy kontrolera (który jest prawie zawsze nazwą zasobu w liczbie mnogiej i z zmienioną wielkością liter - tak to działa w Ruby on Rails), przetworzeniu ją na nazwę klasy i odpytaniu tablicy symboli o klasę o danej nazwie. Mając tę klasę, kod aplikacji wykonuje na niej statyczną metodę do znajdowania instancji danego zasobu za pomocą identyfikatora (również pobranego z parametrów żądania HTTP). W ten sposób otrzymuje dokładnie tę instancję, do której adresowane było żądanie i może wykonać na niej metodę sprawdzającą przywileje. Poniżej fragment kodu ilustrujący te czynności:

# Przekształcenie parametru żądania w nazwę klasy
# poprzez sprowadzenie do liczby pojedynczej metodą singularize i zmiany 
# na konwencję nazewniczą obowiązującą dla klas za pomocą metody classify.
# Np. ta linijka zmieni napis "subversion_repositories" na "SubversionRepository"
model_class_name = Inflector.singularize(controller_name).classify
 
# Odnalezienie klasy zasobu na podstawie nazwy
model_class = Object.const_get(model_class_name)
 
# pobranie identyfikatora instancji z parametrów żądania
id = params[:id]
 
# Odnalezienie w danej klasie zasobu odpowiedniej instancji
#Poprzez wywołanie metody find(), którą w Ruby on Rails posiadają klasy modelu.
current_resource = model_class.find(id)
 
# Sprawdzenie przywilejów aktualnego 
# użytkownika do żądanej instancji
if current_participant.not_allowed_to_read?(current_resource)
  # Wysłanie strony z odmową dostępu
  render(:template => "shared/no_privileges")
end

Dodawanie kodu w trakcie wykonania

Ruby wyróżnia się szczególnie na tym polu. Niemalże wszystko można zparametryzować, wygenerować i dodać w czasie wykonania do danego kontekstu wykonania. Często pozwala to oszczędzić czas spędzony na modyfikowaniu kodu. W swoim kodzie wykorzystałem tę cechę języka Ruby wielokrotnie. Przykład zaczerpnięty z kodu jednej z moich aplikacji przedstawiają dwa niżej zamieszczone listingi. Pierwszy przedstawia fragment kodu z mechanizmu sprawdzania przywilejów przed zastosowaniem metaprogramowania:

def self.reader_role_name
  table_name() + READER_SUFFIX
end
 
def self.creator_role_name
  table_name() + CREATOR_SUFFIX
end
 
def self.editor_role_name
  table_name() + EDITOR_SUFFIX
end
 
def self.destroyer_role_name
  table_name() + DESTROYER_SUFFIX
end
 
def reader_instance_role_name
  self.class.reader_role_name + "_" + self.id.to_s
end
 
def creator_instance_role_name
  self.class.creator_role_name + "_" + self.id.to_s
end
 
def editor_instance_role_name
  self.class.editor_role_name + "_" + self.id.to_s
end
 
def destroyer_instance_role_name
  self.class.destroyer_role_name + "_" + self.id.to_s
end
 
def reader_instance_role
  Role.find(:first,
    :conditions => ["name = ?", self.reader_instance_role_name])
end
 
def creator_instance_role
  Role.find(:first,
    :conditions => ["name = ?", self.creator_instance_role_name])
end
 
def editor_instance_role
  Role.find(:first,
    :conditions => ["name = ?", self.editor_instance_role_name])
end
 
def destroyer_instance_role
  Role.find(:first,
    :conditions => ["name = ?", self.destroyer_instance_role_name])
end

Drugi listing zawiera kod, który ma identyczny efekt końcowy, lecz jest zrefaktoryzowany przy pomocy techniki generowania i dodawania kodu w trakcie wykonania. rozwiązanie to sprowadza się do zauważenia, że fragmenty kodu dla każdego przywileju różnią się tylko pewnymi konkretnymi słowami. Dlatego też słowa te wyabstrahowano i uczyniono w nich parametry dla fragmentu kodu. Po sparametryzowaniu, kod taki włączany jest do aplikacji.

# Dla kazdej z czterech nazw wykonaj szablon
%w(reader creator editor destroyer).each do |privilege|
  # Utworz fragment kodu w oparciu o szablon i wartość zmiennej privilege
  method_source = <<-END_SRC
    def self.#{privilege}_role_name
      table_name() + #{privilege.upcase}_SUFFIX
    end
 
    def #{privilege}_instance_role_name
      self.class.#{privilege}_role_name + "_" + self.id.to_s
    end
 
    def #{privilege}_instance_role
      Role.find(:first, 
        :conditions => ["name = ?", 
          self.#{privilege}_instance_role_name])
    end
 
    END_SRC
    # Dodaj utworzony fragment kodu do klasy
    class_eval method_source, __FILE__, __LINE__
  end  
end

Jeśli generowany kod nie musi być parametryzowalny, są prostsze sposoby na dodawanie go do klas. Sposobem bardzo często wykorzystywanym w języku Ruby jest definiowanie metod przez inne metody. Innymi słowy - w ciele metody można zawrzeć definicje innych metod. Jeśli ta metoda zawierająca definicje zostanie wykonana, doda kod nowych metod do klasy. Przykład podany poniżej (pochodzący z jednej z moich aplikacji) ilustruje dodanie metod do klasy, która ma bezpośrednio do czynienia z danymi dotyczącymi uczestnictwa w projektach.

class ActiveRecord::Base
  # ...
  def self.has_participations
    def current_participations(options = {})
      return self.participations.find(:all, 
          :conditions => {:end_date => nil} )
    end
  end
  # ...
end

Jak widać, metoda statyczna has_participations definiuje metodę egzemplarza o nazwie current_participations. Jeśli klasa dziedzicząca chce włączyć definicję metody current_participations, wystarczy wywołać zdefiniowaną metodę statycznąw następujący sposób:

class Project < ActiveRecord::Base
  has_participations
  # ... 
end

Ruby i języki specyficzne dla domeny

Język Ruby słynie ze swej elastycznej składni, ilości opcjonalnych elementów gramatyki i z siły wyrazu. Ostatnie linie przedstawionego poniżej kodu (który sam w sobie nie robi nic użytecznego) ilustrują, jak dalece można manipulować składnią języka Ruby, by udawać inne istniejące języki.

#!/usr/bin/ruby -w
 
temperature = 1 eggs = 1 it = 1
 
def add(*ingredients) "" end 
def mix(arg) "" end 
def ready() "" end 
def equals(num) true end 
def everything(arg) "" end 
def blows!() "" end
 
# Całkowicie poprawny fragment kodu w języku Ruby
add eggs and mix it until ready unless 
temperature.equal? 100 or
everything blows!

Dzięki temu Ruby nadaje się doskonale do pisania tzw. języków specyficznych dla domeny (ang. Domain Specific Languages, często używa się akronimu DSL). Języki te są szeroko wykorzystywane w Ruby on Rails, a także innych bibliotekach.

Języki DSL są pewnymi ograniczonymi formami języków komputerowych zaprojektowanych w celu zaadresowania specyficznych rodzin problemów. Języki DSL nie muszą być koniecznie językami wykonywalnymi - mogą to być języki konfiguracji, opisu metadanych itp. Za przykłady mogą posłużyć doskonale znane języki, takie jak:

  • język wyrażeń regularnych (opracowany w celu dopasowywania tekstu do wzorców),
  • make (opracowany w celu automatyzacji procesu budowania i śledzenia zależności),
  • bison/yacc (opracowany w celu ułatwienia pisania parserów dla gramatyk formalnych).

Składnia tych języków odróżnia się znacząco od składni języków ogólnego przeznaczenia - zarówno słowa kluczowe jak i konstrukcje mają na celu jak najlepsze odzwierciedlenie sposobu myślenia specjalisty z danej domeny wiedzy oraz umożliwienie mu jak najbardziej bezpośrednie przelanie swoich myśli na kod.

Kod napisany w języku DSL jest przetwarzany różnorako. Czasami jest tłumaczony na inny rodzaj kodu (czyli stanowi formę kodu pośredniego), czasami jest bezpośrednio interpretowany. Czasami też język ogólnego przeznaczenia jest na tyle elastyczny, że, wykorzystując istniejące w nim mechanizmy, można przystosować go do danej dziedziny, pisząc odpowiednie biblioteki (wtedy mówimy w tzw. wewnętrznym języku domenowym, ang. Internal DSL). Tak jest właśnie w przypadku języka Ruby, który doczekał się już kilku wewnętrznych języków domenowych. Omówię teraz niektóre z nich.

Rake

Rake jest narzędziem napisanym w celu wykonywania zadań i podzadań oraz śledzenia zależności między nimi. Może on być wykorzystywany w podobny sposób jak posiksowe narzędzie make, ale częściej jest używany do automatyzacji pewnych zadań związanych z utrzymaniem aplikacji. Przykładami mogą być:

  • Wypisywanie z plików źródłowych wszystkich odpowiednio oznakowanych notatek pozostawionych w kodzie źródłowym.
  • Tworzenie kopii zapasowej bazy danych.
  • Czyszczenie dzienników zdarzeń aplikacji.
  • Uruchomienie testów jednostkowych.

Rake wykorzystuje specjalny format pliku będący w istocie zakamuflowanym programem w języku Ruby. Poniżej hipotetyczny skrypt Rake służący do przygotowania oficjalnego pakietu źródłowego i skopiowania go na serwer projektu:

task :default => [:package] do
  puts "Done."
end
 
task :package => [:clean_bin, :zip_source, :copy_to_server ] do
   puts "Package preparation finished."
end
 
task :clean_bin do
  puts "cleaning binary files..."
  #...
end
 
task :zip_source do
  puts "making zip archive..."
  #...
end
 
task :copy_to_server do
  puts "copying..." 
end

Uruchomienie na pliku zawierającym ten kod narzędzia Rake wyprodukuje następujący efekt:

    (in /home/astral/ganymede/m7workspace/PlaygroundForRuby)
    cleaning binary files...
    making zip archive...
    copying...
    Package preparation finished.
    Done.

Narzędzie Rake odgrywa istotną rolę w pracy programisty Ruby on Rails. Ruby on Rails zawiera wiele predefiniowanych zadań dla aplikacji Rake, np. do migracji schematu bazy danych, automatycznego dokumentowania całej aplikacji oraz wypisywania komentarzy pozostawionych w kodzie.

Migracje ActiveRecord

Migracje ActiveRecord są przykładem na to, jak można wykorzystać język Ruby, by opracować niezależny od systemu zarządzania bazą danych szkielet do migracji schematu bazy. Ruby został tu użyty do implementacji języka podobnego do DDL (Data Definition Language - języka definiowania danych) znanego z rodziny języków SQL. Za pomocą migracji można tworzyć, usuwać i zmieniać tabele w bazie danych, jak również cofać dokonane zmiany.

Poniżej przykładowa migracja ActiveRecord tworzącą tabelę events z kilkoma kolumnami.

class CreateEvents < ActiveRecord::Migration
  def self.up
    create_table :events do |t|
      t.integer :location_id
      t.integer :creator_id
      t.string :topic, :limit => 100
      t.string :summary, :limit => 600
      t.datetime :start_date, :default => Time.now
      t.datetime :end_date
      t.integer :version
      t.timestamps
    end
  end
 
  def self.down
    drop_table :events
  end
end

Migracje ActiveRecord mogą użyte do utworzenia wszystkich tabel w bazie danych, wypełnienia ich danymi początkowymi oraz wszelkich zmian schematu.

Mechanizm Routes

Mechanizm Routes, zastosowany w Ruby on Rails, służy do tłumaczenia URLi wpisanych w okno adresowe przeglądarki internetowej na konkretne żądanie do konkretnego obiektu w aplikacji. Umożliwia on między innymi zdefiniowanie formatu dla każdego elementu takiegu URLa. Ilustruje to poniższy fragment kodu:

# Przykład wykorzystania: 
# http://aplikacja.com/repositories/12/AtmaGroupware/db/db.config
map.connect ':controller/:revision/:path/:current_item',
 :controller => 'repositories',
   :requirements => {
     :revision => /[[:digit:]]+/, 
     :path => /.*/, 
     :current_item => /.*/ },
   :action => 'print'

Mechanizm Routes pozwala również budować ścieżki poprzez zagnieżdżanie zasobów (szerzej o zasobach w innym artykule). Przykład takiego zagnieżdżenia przedstawiłem poniżej.
# Przykład wykorzystania:
# http://aplikacja.com/projects/1/issues/2/issue_comments 
# czyli wszystkie komentarze dla jednego z zagadnień dla projektu 
map.resources
:projects do |project|
  project.resources :issues do |issue|
    issue.resources :issue_comments
  end
end

Routes jest rdzennym mechanizmem Ruby on Rails do tłumaczenia URLi.

Klasa Builder::XmlMarkup

Klasa ta, będąca częścią komponentu ActiveResource należącego do Ruby on Rails, udostępnia przyjazny sposób generowania kodu XML. Fragment kodu przedstawiony na zamieszczonym poniżej fragmencie kodu, zaczerpnięty z dokumentacji ActiveResource (dołączonej do Ruby on Rails), obrazuje bezpośredni związek kodu pisanego w Ruby z generowanym kodem XML.

require 'rubygems'
require 'active_resource'
 
buffer = ""
xm = Builder::XmlMarkup.new(:target=>buffer, :indent=>2)
 
xm.instruct!             # <?xml version="1.0" encoding="UTF-8"?>
xm.html {                # <html>
  xm.head {              #   <head>
    xm.title("History")  #     <title>History</title>
  }                      #   </head>
  xm.body {              #   <body>
    xm.comment! "HI"     #     <! -- HI -->
    xm.h1("Header")      #     <h1>Header</h1>
    xm.p("paragraph")    #     <p>paragraph</p>
  }                      #   </body>
}                        # </html>
 
puts buffer

Biblioteka ta może być np. używana w aplikacji opartej o Ruby on Rails do pisania tymczasowego kodu HTML, kiedy zajdzie potrzeba szybkiego przetestowania idei. Potem ten kod może zostać zastąpiony bardziej odpowiednimi mechanizmami (np. widokami częściowymi Rails).

RSpec

RSpec jest szkieletem testowym pozwalającym na definiowanie scenariuszy testowych w języku bliskim naturalnemu. Wspomiman tu tylko o nim jako o dobrym przykładzie języka specyficznego dla domeny (więcej na jego temat można znaleźć na
stronie domowej projektu [7]).


Bibliography
1. D. Thomas, C. Fowler, A. Hunt - Programowanie w języku Ruby (wydanie II), helion, 2007
3. D. Thomas, C. Fowler, A. Hunt - Programming Ruby, The Pragmatic Programmers, LLC, 2000
O ile nie zaznaczono inaczej, treść tej strony objęta jest licencją Creative Commons Attribution-Share Alike 2.5 License.