Let's Do It Romania - 24 Septembrie 2011



   

Ruby

   

Un limbaj cu ochii oblici


   

Inspirându-se din Smalltalk, limbajul Ruby aduce în domeniul scripting-ului esenţa programării obiectuale.

Mircea Sârbu


Trebuie să mărturisesc de la bun început că sunt un începător în acest limbaj, că nu am scris nici o aplicaţie în Ruby. Mi-a fost semnalat de un prieten (mulţumesc, Teo), aşa că am răsfoit nişte prezentări pe Web, m-am jucat cu un interpretor, am citit o jumătate de carte... Lucruri pe care le face oricine când primeşte o jucărie nouă. Aşadar, aceasta prezentare este doar un semnal, o gustare (digerabilă într-o jumătate de oră) menită să vă transmită ceva din aroma acestui nou limbaj în vogă. Dacă vă place, o căutare pe Web vă oferă informaţii suplimentare. Punctele de plecare sunt www.ruby-lang.org şi www.rubycentral.com.

Ruby a fost creat de un programator japonez, pe nume Yukihiro Matsumoto (zis Matz), care, nemulţumit de toate celelalte alternative, s-a hotărât să-şi creeze un limbaj de programare propriu. Aceasta se întâmpla prin 1993, iar prima versiune a fost gata în 1995. Cu timpul, limbajul a început să capete popularitate. Mai întâi în Japonia natală (unde se pare că a depăşit Python-ul), apoi a început să se răspândească în lume. De curând a apărut prima carte despre Ruby în limba engleză, ceea ce, având în vedere calităţile limbajului, creează premisele unei răspândiri mult mai accelerate.

În general

O caracterizare minimală (repede, că ne grăbim la cod!) ar suna aşa:

  • Ruby este un limbaj de scripting;

  • Ruby este un limbaj pur obiectual;

  • Ruby este liber (open source).

Ar mai trebui spus că Ruby este un limbaj eclectic, în sensul că a preluat o multitudine de caracteristici preţioase din numeroase alte limbaje, dintre care două se disting în mod special: Smalltalk şi Perl. În ciuda acestui fapt, limbajul este simplu, consistent şi succint în exprimare.

Ca şi în Smalltalk (dar spre deosebire de Python), în Ruby nu există nimic altceva decât obiecte. Orice există este instanţă a unei clase. Ca şi în Smalltalk, orice acţiune se face trimiţând un mesaj unui obiect. Spre deosebire de Smalltalk, Ruby nu produce bytecode ci se interpretează.

În plus, sintaxa lui Ruby este mult mai familiară decât cea utilizată de Smalltalk, deşi este inspirată din Eiffel şi Ada, cu influenţe din Perl. Spre deosebire de acesta din urmă, Ruby este lizibil.

În fine, definiţia mea: Ruby este Smalltalk adaptat pentru scripting. Aceasta implică o sintaxa familiară, interpretare directă şi utilizarea fişierelor.

Salutare, lume

Caracterizări de ordin general găsiţi câte vreţi pe Web. Aşadar:

      def salut(nume)
        rez = "Salutare, " + nume
        return rez
      end

Prin convenţie, rezultatul evaluării va fi introdus în acest articol prin simbolul ==>>. Deci:

      salut("lume") ==>> "Salutare, lume"
      salut("Mircea").length ==>> 16

Nici o surpriză. De fapt, Ruby a fost construit pe baza unui principiu cât se poate de sănătos, cunoscut sub numele POLS (Principle of Least Surprise).

Aşadar, am definit o funcţie numită salut. De fapt, nu este o funcţie, ci o metodă. Atunci când receptorul nu este specificat este considerat implicit self, iar în cazul în care codul nu apare în cadrul unei clase, acesta este o instanţă generică a clasei Object, rădăcina ierarhiei de clase din Ruby. Aceasta este o veste bună pentru cei care nu se simt în largul lor în lumea obiectuală: se poate programa fără clase! De fapt, se lucrează în cadrul clasei Object, dar acest lucru este transparent.

Sintaxa este foarte liberală şi există mereu alternative. Pot scrie, de exemplu:

      salut "lume" ==>> "Salutare, lume"
      (salut 'Mircea').length ==>> 16

Pe de altă parte, metoda salut poate fi scrisă mult mai simplu (aici însă ghilimelele sunt importante, nu pot fi apostrofuri):

      def salut(nume) "Salutare, #{nume}" end

Deci, nimic special până aici. Ruby dispune, desigur, de structurile de date tipice din limbajele de scripting: liste (array) şi dicţionare (hash), precum şi de structurile tipice de control (if, while, for etc.):

      revista = 'NetReport'
      a = ['Agora', 2000, "#{revista}"]
      b = Hash.new
      b['editura'] = a[0]
      b[:an] = a[1] += 1  
      b['revista'] = revista
      for k in b.keys
        if k.kind_of? String
          puts b[k]
        end
      end

Fără surprize, codul de mai sus va afişa:

      Agora
      NetReport

Codul arată cât de poate de tipic pentru scripting. Aproape totul este însă syntactic sugar, (adică "înlocuitor") pentru clasica tripletă obiectuală receptor.mesaj(argumente). Smalltalk deghizat. Listele, dicţionarele, variabilele, toate sunt instanţe ale unor clase. Până şi operatorii sunt de fapt mesaje. Ba chiar şi structurile de control! Iar enigmaticul :an este (ca şi în Smalltalk) un simbol (adică o instanţă a clasei Symbol).

Clase

Oricât de bogată ar fi ierarhia de clase de care dispune în mod nativ Ruby, aplicaţiile noastre vor avea nevoie de clase specifice. Desigur, avem posibilitatea să le definim. Iată un exemplu:

      class Persoana
        def initialize(prenume, nume, sex, loc)
          @nume = nume + ' ' + prenume
          @sex, @loc = sex, loc
        end
      end

Clasa pe care am definit-o se numeşte Persoana. Ca şi în Smalltalk, există convenţii în privinţa identificatorilor, prin care se desemnează de fapt domeniul lor de vizibilitate (nu tipul, ca în Perl). Variabilele locale încep cu minusculă, cele globale sunt prefixate cu caracterul $, numele claselor şi al constantelor încep cu majusculă, variabilele instanţelor se prefixează cu @ iar variabilele de clasă cu @@. Se poate observa în cod faptul că datorită acestei convenţii, Ruby distinge variabilele corespunzând argumentelor de variabilele instanţei (deci nu este nevoie de self).

Singura metodă definită, initialize, are o semnificaţie specială, fiind ceea ce s-ar putea numi, printr-un uşor abuz de limbaj, un constructor. Mai precis, este metoda care răspunde la mesajul new adresat clasei (desigur, clasele sunt şi ele obiecte) şi creează o instanţă:

      pers = Persoana.new('Mircea', 'Sarbu', :m, "Tg-Mures")

Dacă vom încerca să afişăm persoana pers, vom obţine ceva de genul #<Persoana:0x46222e8>, ceea ce nu ne edifică prea mult. O soluţie ar fi să definim o metodă care să afişeze nişte informaţii inteligibile despre persoană. Dar putem face şi altceva: clasa Object dispune de o metodă numită to_s care produce o reprezentare textuală a obiectelor. Această metodă este moştenită de toate clasele, inclusiv de Persoana (care, în mod implicit, este o subclasă a clasei Object). De fapt, această metodă a produs şirul de caractere de mai sus. Avem posibilitatea să o redefinim:

      class Persoana
        def to_s
          form = 'Dl'
          form = 'Dna' if @sex == :f
          "#{form}. #{@nume} (#{@loc})"
        end
      end

Sunt sigur că iubitorii de Perl au o tresărire de simpatie la vederea acestui cod. Pentru iubitorii de Python, o consolare: se poate scrie şi mai frumos...

În primul rând, să remarcăm că o clasă este mereu deschisă, în sensul că putem oricând să o modificăm (în cazul acesta i-am adăugat o metodă). Apoi să observăm forma alternativă pentru if când există o singură ramură.

Ce facem însă dacă dorim să obţinem doar numele unei anumite persoane? Dar dacă persoana se mută în altă localitate? Avem nevoie de metode care să permită accesul la anumite variabile ale instanţelor. În mod normal acestea sunt destul de simple. De pildă:

      class Persoana
        def loc= (loc)
          @loc = loc
        end
      end

Simple, dar multe. Observaţi că de fapt se redefineşte operatorul de atribuire.

Din fericire, avem o cale simplă:

      class Persoana
        attr_reader :nume, :sex
        attr_accessor :loc
        def numeNou(prenume, nume)
          @nume = nume + ' ' + prenume
        end
      end

Am declarat astfel că variabilele de instanţă nume şi sex pot fi citite direct, iar atributul loc poate fi scris şi citit direct. Există şi varianta attr_writer, pe care nu am folosit-o. În schimb am declarat o metodă proprie pentru schimbarea numelui. Acum putem face atribuiri de genul:

      pers2 = pers.clone
      pers2.loc = "Bucuresti"
      pers2.numeNou('Ion', 'Ionescu')
      n = pers.nume

Dacă intenţionaţi să evitaţi crizele de nervi, reţineţi că toate aceste aşa-zise "declaraţii" sunt de fapt comenzi executabile, aşa că în cadrul aceleiaşi sesiuni o schimbare în structura unei clase poate genera conflicte cu declaraţiile anterioare. Ruby e un interpteror, nu un compilator!

Iată şi moştenirea:

      class Angajat < Persoana
        @@nr = 0
        Limita = 100
        def initialize(p, salar)
          @salar, @loc = salar, p.loc
          @marca = @@nr += 1
          np = p.nume.split(/\W+/)
          @nume = np[0] + ' ' + np[1]
        end
        def to_s
          super.to_s + " marca= #{@marca}"
          # salariul e confidential
        end
        def Angajat.preaMulti?
          return @@nr > Limita
        end
      end

Desigur, am forţat puţin nota pentru a ilustra pe scurt câteva aspecte:

  • Variabila de clasă @@nr va fi incrementată la fiecare creare a unei instanţe, astfel încât angajaţii vor primi mărci unice.

  • Ruby dispune de întregul mecanism de expresii regulate.

  • Referirea la metodele clasei părinte se fac prin variabila super.

  • Constantele poartă nume care încep cu majusculă.

  • Metodele de clasă poartă nume prefixate cu numele clasei.

Folosind şi codul pentru exemplificarea clasei Persoana, putem scrie:

      a1 = Angajat.new(pers, 21000)
      a2 = Angajat.new(pers2, 12000)
      puts a2.to_s
         ==>> Dl. Ionescu Ion (Bucuresti) marca= 2
      puts Angajat.preaMulti?
         ==>> false

Iteraţii

O altă caracteristică pe care Ruby a preluat-o direct din Smalltalk este posibilitatea de a furniza unei metode un bloc. Un bloc cuprinde o porţiune de cod cuprins între acolade sau între do şi end. Iată un exemplu:

      [1, 3, 7].each { | x | print x * x, ' ' }
        ==>> 1 9 49

Metoda each aparţine clasei Array (de fapt tuturor colecţiilor) şi am putea crede că e ceva magic în ea. Nu este nimic altceva decât metoda yield, care furnizează argumente blocului pe care metoda îl va primi (blocul din exemplu dispune de parametrul formal x, declarat între bare verticale). De fapt, putem scrie o metodă proprie pentru aceasta:

      class Array
        def fiecare
          for x in self 
            yield x 
          end
        end
      end

      [1, 3, 7].fiecare do 
        | x | 
        print x * x, ' ' 
      end
        ==>> 1 9 49

Lucrurile se petrec exact ca în Smalltalk. Un bloc este o secvenţă de cod a cărei execuţie este amânată. Blocul trebuie să înceapă pe aceeaşi linie pe care se termină metoda, deoarece interpretorul îl citeşte şi-l memorează înainte de a evalua metoda. După ce l-a memorat, interpretorul evaluează metoda şi, de câte ori întâlneşte metoda yield, evaluează blocul.

O metodă urmată de un bloc de numeşte iterator. Putem să ne definim iteratorii pe care îi dorim, în funcţie de specificul problemei. Iată, spre exemplu, o posibilă utilizare într-o clasă desemnată să cuprindă angajaţii care lucrează într-un departament.

Înainte de a defini clasa Departament, voi completa clasa Angajat cu o metodă de sortare:

      class Angajat
        def <=> (unAng)
          return @nume <=> unAng.nume
        end
      end

Am redefinit operatorul de comparaţie <=> astfel încât să compare doi angajaţi prin numele lor. Operatorul <=> returnează -1 (mai mic), 0 (egal) şi 1 (mai mare), fiind implicit folosit de metoda sort a clasei Array.

Acum, clasa Departament :

      class Departament
        def initialize
          @angajati = Array.new
          @nr = 0
        end
        def <<(unAng)
          @angajati << unAng
          @nr += 1
        end
        def parcurge
          for x in @angajati.sort
            yield x
          end
        end
      end

Operatorul << adaugă elemente într-o listă (este echivalent cu metoda Array::push). L-am redefinit pentru a funcţiona şi pentru departamente.

Putem acum să adăugăm angajaţi astfel:

      marketing = Departament.new
      marketing << a1
      marketing << a2
      marketing << Angajat.new(
         Persoana.new("Adriana", "Radu", :f, "Ploiesti"),
         19000)

Acum putem să obţinem, de exemplu, lista angajaţilor cu salariul mai mare de 15.000:

      marketing.parcurge do
        |a| 
        puts a.nume if a.salar >15000 
      end     

Altele

Ruby mai dispune de multe alte caracteristici extrem de interesante. Spaţiul tipografic (precum şi experienţa mea limitată cu Ruby) nu-mi permit să le prezint pe toate, dar o scurtă enumerare poate fi edificatoare:

  • Tratarea excepţiilor (similar cu Python)

  • Fire de execuţie (implementate independent de thread-urile sistemului de operare gazdă!)

  • Module şi mixins (acestea din urmă permit o formă sigură de moştenire multiplă)

  • Mecanisme reflexive (permit examinarea dinamică a caracteristicilor obiectelor)

  • Colectare automată a memoriei reziduale (garbage collection)

  • Implementează direct câteva "tipare software" (software patterns): visitor, singleton, observer, delegation

  • Dispune de biblioteci bogate

  • Posibilităţi de extensie

Merită să-l încercaţi, mai ales că găsiţi pe Web, alături de software, şi conţinutul complet al cărţii Programming Ruby (The Pragmatic Programmer's Guide) de Dave Thomas şi Andy Hunt.


 

(Publicat în NET Report 110 - noiembrie 2001)

 

Copyright © 2001 Agora Media

Creative Commons License
This work is licensed under a Creative Commons License.