Paula Kemppi, Juuso Montonen, Martin Pärtel
(26.04.2007)
Ruby on 1995 julkaistu reflektiivinen, dynaaminen, tulkittu olio-ohjelmointikieli.
Työtoverin "syntymäkiven" mukaan nimetyn ohjelmointikielen on kehittänyt Yukihiro "Matz" Matsumoto, joka aloitti Rubyn kehityksen 1993.
Rubyn ensimmäinen versio julkistettiin 1995 ja tällä hetkellä virallinen painos on 1.8.6. Yukihiro Matsumoton mukaan 2007 joulukuussa julkaistavan Ruby 2.0:n olennaisimpia muutoksia on Rubyn suoritusympäristön siirtäminen YARV- virtuaalikoneeseen. Tämän muutoksen tarkoitus on parantaa Rubyn suorituskykyä, joka tulkituissa kielissä on käännettyjä heikompi.
Ohjelmointikielen kehittäjän mukaan tärkein periaate Rubyn kehittämisessä on fokuksen siirtäminen pois laitetasosta inhimilliselle tasolle, eli asettaa järjestelmän käyttäjän tarpeet ensisijalle. Ruby on kehitetty ohjelmoijan tuottavuutta ja ohjelmoinnin hauskuutta silmällä pitäen. Sääli, ettei tulkki kerro puujalkavitsejä kohdatessaan virheen.
Rubya kutsutaan monen paradigman ohjelmointikieleksi, koska se mahdollistaa proseduraalisen, oliopohjaisen sekä funktionaalisen ohjelmoinnin. Lisäksi Rubystä löytyy tuki introspektiolle, reflektiolle ja metaohjelmoinnille. Ruby on siis hyvin monipuolinen ohjelmointikieli, joka soveltuu skriptaamisesta web-palveluiden rakentamiseen ja kaikkeen mahdolliseen mihin perinteiset olio-ohjelmointikielet kelpaa. Rubyn web-palveluiden kehitysympäristö tunnetaan erikseen nimellä Ruby on Rails.
Valitettavasti Rubyssa ei ole täyttä Unicode-tukea, vaikka siinä on osittainen tuki UTF-8:lle. Toinen puute on Rubyn käännettäviä kieliä heikompi suorituskyky. Tämän ongelman korjaamiseksi Rubyn suoritus siirtyy seuraavissa versioissa virtuaalikoneympäristöön, jonka pitäisi nopeuttaa ohjelmien suoritusta. Käsityksen Rubyn suoritustehokkuudesta saa tästä vertailusta C:n (gcc:llä) ja Rubyn välillä. Tässä myös kiinnostava kaavio Javan ja Rubyn eroista.
Ruby-tiedostojen pääte on tavallisesti .rb. Valmiita Ruby-ohjelmia suoritetaan komennolla ruby <tiedosto>.rb. Kielen opettelemiseen ja kokeilemiseen kannattaa käyttää irb -komentotulkkia. Dokumentaatiota voi selata ri-komennolla, esimerkiksi: ri Array#sort.
Paikalliset muuttujat täytyy aloittaa pienellä, luokat ja vakiot isolla alkukirjaimella (i, Alkuelain, MALLI). Olion jäsenmuuttujat alkavat @-merkillä (@x). Luokkamuuttujien etuliite on @@ (@@count). Globaalien eteen kirjoitetaan $ ($stdout).
Lisäksi on tapana päättää kysyvät metodit kysymysmerkkiin (empty?) ja vaaralliset, olion tilaa muuttavat metodit huutomerkkiin (sort!).
# Kommentit alkavat risuaidalla # Yksi lause / rivi x = 3 # Lauseet voi myös erottaa puolipisteellä y = x + 1; z = y * 3 # Laskujärjestys on intuitiivinen z = 1 + 2 * 3 # Lausetta voi jatkaa seuraavalle riville kenoviivalla x = 1 + \ 2 + \ 3 # Perustulostus print "Ruby on kivaa!\n" puts "Kivaa!" # autom. rivinvahito puts 123.to_s # to_s-metodi muuttaa luvun merkkijonoksi p 123 # funktio p tulostaa minkä tahansa tietotyypin jollain tavalla # (vrt. Javan System.out.println(Object))
# Perus ehtolause if x > 0 puts "Hello World" elsif x == 0 or x == -1 puts "bla" else puts ":(" end # If-lauseen voi kirjoittaa yhdelle riville joko then-avainsanalla... if x > 0 then puts "Hello World" end # ... tai Perlistä tutulla <lause> if <ehto> -rakenteella puts "w00t" if x >= 2 y = 14 unless x != 3
while x < 10 # (...) end # Alkuehtoinen toisto yhdellä rivillä x -= 1 while x > 0 x += 1 until x == 10
Loppuehtoinen toisto kirjoitetaan begin – end -lohkojen avulla:
begin # Suoritetaan ainakin kerran end while x > 0
Tavallinen for-silmukka näyttää tältä:
for i in 1..3 # Toistuu kolmesti end for i in 1...3 # Toistuu kahdesti; 3 jää pois end
Rubyn taulukot ovat heterogeenisia, eli ne voivat sisältää mitä tahansa arvoja ilman rajoituksia. Taulukoiden indeksointi alkaa nollasta, mutta niitä voi myös indeksoida negatiivisella luvulla, jolloin alkion paikka lasketaan lopusta alkaen.
a = [] # Tyhjä taulukko a = [0, 1, 2, 3] a = [nil, 1, [2], "viimeinen"] a[3] # => "viimeinen" a[-1] # => "viimeinen"
Assosiaatiotaulu (hash) on joukko avain/arvo -pareja. Sillä ei ole järjestystä.
h = {} # Tyhjä hash-taulu h = {"a" => 1, "b" => 2, "c" => 3} h = {0 => [], "nolla" => {}, nil => 42} h[0] # => [] h[nil] # => 42
Kuten kaikki muutkin Rubyn arvot, myös taulukot ja assosiaatiotaulut ovat olioita, ja niillä on suuri joukko metodeja. Metodeista saa helposti luettelon kirjoittamalla irb-tulkkiin:
[].methods.sort {}.methods.sort
Rubyssa ei ole erillistä monikkotietotyyppiä (tuple). Monikkojen sijaan käytetään taulukkoja.
Taulukot voidaan kirjoittaa sijoituslauseisiin ilman hakasulkuja. Näin saadaan muiden kielien monikkoja muistuttava muoto: a = 1, 2, 3 (arvoja ei saa ympäröidä tavallisilla suluilla). Taulukon elementit evaluoidaan vasemmalta oikealle.
Ruby tukee myös yhtäaikaista sijoitusta useaan muuttujaan: a, b = 1, 2 (vasemmalla puolella saa myös käyttää sulkuja). Näin saadaan aikaan monesta kielestä tuttu tapa vaihtaa kahden muuttujan arvot keskenään: a, b = b, a
Seuraavat lauseet tarkoittavat samaa:
a, b = b, a a, b = [b, a] (a, b) = b, a (a, b) = [b, a]
Symbolit ovat nopeita merkkijonoavaimia. Niitä käytetään mm. samanlaisissa tilanteissa kuin C:n tai Javan enum-arvoja. Luokkien yhteydessä nähdään, miten symboleilla voi kätevästi nimetä jäsenmetodeja ja -muuttujia kielen reflektiivisiä ominaisuuksia käytettäessä.
Symbolien etuliitteenä on kaksoispiste, ja niille pätevät jotakuinkin samat rajoitukset kuin muuttujien nimille. Esim. :sym, :wtf?, :$abc123 ovat kelvollisia symboleja.
Funktion määrittelyssä ja kutsussa voidaan jättää sulut pois, jos parametreja on enintään yksi. Vanhoissa Ruby-versioissa sulut sai jättää pois, vaikka parametreja oli enemmänkin, mutta tämä aiheutti liikaa epäselvyyksiä ja käytännöstä ollaan luopumassa.
Jos funktiossa ei ole erillistä return-avainsanaa, se palauttaa funktion viimeisen lauseen arvon. Rubyssa kaikki lauseet palauttavat jonkin arvon.
def toiseen x puts "Korotan nyt #{x} toiseen" x**2 # Viimeisen lauseen arvo palautetaan ilman returniakin end toiseen 3 # => 9 toiseen toiseen(3) # => 81 toiseen(toiseen 3) # => 81 ja varoitus sulutuksesta toiseen toiseen 3 # => 81 ja varoitus sulutuksesta # Palauttaa toisen asteen yhtälön a(x^2) + bx + c = 0 juurien joukon def tay(a, b, c) raise Exception.new("Ei a saa olla nolla!") if a == 0 d = b**2 - 4*a*c if d < 0 then return [] elsif d == 0 then return [-b/(2*a)] else return [(-b + Math.sqrt(d))/(2*a), (-b - Math.sqrt(d))/(2*a)] end end tay(1, 2, 3) # => nil tay(1, 2, 0) # => [0, -2] tay(1, 0, 0) # => [0]
Funktion voi määritellä ottamaan vaihtelevan määrän parametreja
def summa(*argumentit) summa = 0 for i in argumentit summa += i end return summa end puts summa(1, 2, 3) # => 6
Rubystä löytyy työkalut säännöllisten lausekkeiden monipuoliseen rakentamiseen, joten tässä kohdassa esittelemme vain perusteet. Näiden lisäksi on saatavilla erillisiä regexp-kirjastoja esimerkiksi Oniguruma. Kuten muutkin Rubyn palikat, säännölliset lausekkeet ovat olioita (regexp-luokan ilmentymiä). Säännöllinen lauseke luodaan kutsumalla regexp-luokan konstruktoria tai käyttämällä merkintöjä /hahmo/ ja %r\hahmo\ . Rubyn säännöllisten lausekkeiden syntaksi on Perlin kaltainen, joillakin pienillä eroilla. Lisätietoa ja tarkennuksia Rubyn säännöllisten lausekkeiden käytöstä löytyy parhaiten teoksesta "Programming Ruby, The Pragmatic Programmer's Guide" kappaleesta "Standard types" (kts. linkit oppaan lopussa). Ruby ymmärtää säännöllisiä lausekkeita pääpiirteittäin seuraavanlaisella syntaksilla:
# [] alueen määrittely, [a-z] merkitsee kirjainta a:sta z:taan väliltä. # \w kirjain tai luku, sama kuin [0-9A-Za-z] # \W ei kirjain eikä luku, eli \w:n negaatio # \s välilyönti merkki, sama kuin [ \t\n\r\f] # \S \s:n negaatio, eli mikä tahansa muu merkki kuin välilyönti # \d Lukumerkki, merkitsee lukua väliltä [0-9] # \D \d:n negaatio, eli mikä tahansa muu kuin numeroarvo välitä [0-9] # \b "backspace" (0x08) # \b sanan raja (jos se ei ole alueen määrittelyssä) # \B "ei-sanan" raja, \b:n negaatio. # =~ Hahmontunnistus operaattori, etsii annetun merkkijonon paikan. # Jos se ei löydy, palauttaa 'nil' arvon. # !~ =~ merkin negaatio, eli jos merkkijonoa ei löydy palauttaa true # . mikä tahansa 1 merkki paitsi uusi rivi. # * 0 tai useampi kerta # ? 1 tai 0 kerta # + 1 tai useampi kerta # ^ rivinalku tai hahmon sisällä negaatio esim. [^\d] on merkki joka ei ole luku. # | tai, \d|[a-b], lukumerkki tai a- tai b-kirjain # () merkintöjen ryhmittely # / escape-merkki # Esimerkkejä: "abcdef" =~ /d/ # => 3 ,koska d on merkkijonon indeksissä 3. "aaaaaa" =~ /d/ # => nil ,koska merkkiä 'd' ei löydy. # Metodi heksaluvun etsimiseksi <> -sulkeiden sisältä. # Palauttaa true, jos heksaluku löytyy. def hex(x) (x =~ /<0(x|X)(\d|[a-f]|[A-F])+>/) != nil end hex "onkos täällä heksaluku? <0xananasakäämä>" # => false hex "wtf? <0x800CCC097D>" # => true # Korottaa kaikki merkkijonossa esiintyvät numerot toiseen "11 12 böö 13 14 foo15bar".gsub /\d+/ do |numero| numero.to_i * 2 end # => "22 24 böö 26 28 foo30bar"
Säännöllisten lauseiden koko syntaksi on listattu tiiviisti Ruby QuickRef-sivustolla.
Lohkot (blocks) ovat sulkeumia, eli nimettömiä funktioita, jotka säilyttävät viittausympäristönsä muuttujat. Lohko voidaan määritellä joko aaltosulkujen { } avulla tai do – end -rakenteella. Mitä tahansa metodia voidaan kutsua antamalla sille lohko parametrina. Lohko kirjoitetaan aina jonkin metodikutsun yhteyteen. Metodi voi kutsua parametrina saamaansa lohkoa yield-avainsanalla.
# foo on funktio, joka käyttää saamaansa lohkoa def foo yield 10, 20 # kutsutaan lohkoa parametreilla 10 ja 20 end # kutsutaan foo:ta kaksiparametrisella lohkolla foo {|x, y| puts 7*x + y } # => 90 # metodi foo olisi voitu kirjoittaa myös näin: def foo2(&block) block.call(10, 20) end # seuraava metodi kutsuu lohkoaan kolme kertaa def kolmeKertaa yield yield yield end # Parametriton lohko do-syntaksilla kolmeKertaa do puts "Heippa" end # Sama voitaisiin muuten kirjoittaa näinkin 3.times do puts "Heippa" end
Lohkojen yleisin käyttötapa on erilaisten iteraattorien ja generaattorien määrittäminen. Kokoelmat, kuten listat ja assosiaatiotaulut sisältävät mm. each-metodin, joka kutsuu lohkoa vuorotellen kaikilla kokoelman alkioilla. for-silmukka on itse asiassa vain vaihtoehtoinen kirjoitustapa each-metodin kutsumiselle.
[1, 2, 3].each do |i| puts i end for i in [1, 2, 3] puts i end
Lohko voi käyttää viittausympäristönsä paikallisia muuttujia. Lohko viittausympäristöneen voidaan tallettaa Proc-olioon myöhempää käyttöä varten. Muuttujat säilyvät sulkeumassa vaikka lohkon luoneen metodin suoritus päättyy.
# lohko voi käyttää paikallista muuttujaa def suuret(joukko) keskiarvo = 165 return joukko.select {|e| e.koko > keskiarvo } end # luodaan sulkeuma-olio, joka käyttää metodin tee_kertoja parametria def tee_kertoja(kerroin) return Proc.new {|x| kerroin*x } end kertoja = tee_kertoja(4) kertoja.call(2) # => 8 [1, 2, 3].map {|x| kertoja.call(x) } # => [4, 8, 12]
Lohkot ovat yksi Rubyn keskeisimpiä ominaisuuksia, ja niitä käytetään joskus yllättävilläkin tavoilla.
# avaa tiedoston, suorittaa sulkeuman ja sulkee tiedoston (vaikka tulisi poikkeus) File.open(filename) {|f| doSomethingWithFile(f)} require 'cgi' cgi = CGI.new("html4") # Määrittää html-sivun cgi.out { cgi.html { cgi.head { cgi.title{"Ruby CGI esimerkki"} } + cgi.body { cgi.h1 { "Hello World!" } + cgi.p { cgi.b { "Ruby on kivaa!" } } } } }
Rubyssa kaikki muuttujaan sijoitettavat arvot ovat olioita, ja kaikilla olioilla on viitesemantiikka lukuun ottamatta pieniä kokonaislukuja (Fixnum-luokka), totuusarvoja (true/false) ja tyhjää arvoa (nil). Näitä poikkeustapauksia nimitetään "välittömiksi arvoiksi" (immediate value); esimerkiksi kokonaislukua 1 vastaa aina sama olioilmentymä.
class Yliluokka def yliluokan_metodi(x) x + 1 end end class Luokka < Yliluokka @@count = 0 # Luokkamuuttujan alustus def initialize(x) # Konstruktori @x = x # Sijoitus jäsenmuuttujaan @@count += 1 # Sijoitus luokkamuuttujaan end # Jäsenmetodi def jmetodi(a, b, c) a*x*x + b*x + c end # Luokkametodi def Luokka.lmetodi "Minusta on luotu " + @@count.to_s + " ilmentymää" end # Aksessoreja def x @x end def x=(new_x) @x = new_x end # Korvattu metodi def yliluokan_metodi(x) puts "yliluokan_metodia_kutsuttu" super # Välittää parametrin implisiittisesti end end
Jokainen luokka on luokan Class ilmentymä. Koska jokainen luokka on olio, voi luokillakin olla jäsenmuuttujia (instance variable, @). Nämä ovat kuitenkin eri asia kuin luokkamuuttujat (class variable, @@).
Luokkamuuttujat näkyvät sekä luokassa että sen olioissa.
Luokan jäsenmuuttujat ovat suoraan käytettävissä vain luokkametodeissa.
Olion jäsenmuuttujat ovat suoraan käytettävissä vain olion metodeissa.
Seuraava esimerkki selventänee tilannetta:
class Luokka @@foo = 1 # Luokkamuuttuja @foo = 2 # Luokan jäsenmuuttuja def initialize @foo = 3 # Olion jäsenmuuttuja end def printtaa puts @@foo.to_s # => 1 puts self.class.instance_variable_get(:@foo) # => 2 puts @foo.to_s # => 3 puts self.instance_variable_get(:@foo) # => 3 end def Luokka.printtaa puts @@foo.to_s # => 1 puts @foo.to_s # => 2 end end Luokka.new.printtaa puts Luokka.printtaa
Jokaisella oliolla (paitsi välittömillä arvoilla)
voi olla oma ilmentymäluokkansa
(eigenclass,
singleton class).
Ilmentymäluokka on oliokohtainen perintähierarkian loppuun lisättävä
luokka, jossa voidaan määrittää oliokohtaista toiminnallisuutta.
Esimerkki:
a = Object.new b = Object.new class << a def foo; puts "Olen vain a:ssa"; end end a.foo b.foo # Virhe
Kaikki Ruby-koodi on jonkin luokan (tai moduulin) kontekstissa. Myös tulkin päätasolle kirjoitettu koodi menee itse asiassa Object-luokan kontekstiin. Näin ollen seuraavanlainenkin koodi on aivan mahdollinen, vaikkakin typerä:
require 'time' class LaiskaLuokka if Time.new.wday == 0 puts "En tee töitä sunnuntaisin" else def Luokka.tervehdi puts "Tervehdys vuodelta #{Time.new.year}" end end end begin Luokka.tervehdi rescue NoMethodError => e puts "Virhe: Luokalla ei ole luokkametodia 'tervehdi'." end
Samanlaista temppuilua voisi harrastaa myös yksittäisten olioiden ilmentymäluokissa.
Luokkakontekstin voi myös avata uudelleen missä kohdassa koodia tahansa.
# Integer on Rubyn sisäinen yliluokka kaikille kokonaisluvuille (Fixnum ja Bignum) class Integer def prime? if self == 2 or self == 3 true elsif self < 2 false else ( (2..(self/2)).find {|x| self % x == 0} ) == nil end end end p (1..100).select {|x| x.prime?} # Hidasta mutta kaunista if 1 + 1 == 2 class Integer def in_range?(r) r.include? self end end end 123.in_range? 1..1000 # => true
Triviaalien aksessorimetodien kirjoittaminen on tylsää, sen tietää varsinkin jokainen Javalla ohjelmoinut. Ruby tarjoaa tähän apuvälineitä, nimittäin metodit attr_reader, attr_writer ja attr_accessor:
class Luokka attr_reader :a, :b, :c attr_writer :vain_kirjoitus attr_accessor :x, :y, :z def initialize @a = 1; @b = 2; @c = 3 # Näitä voi vain lukea @vain_kirjoitus = 13 # Tälle generoitiin vain kirjoitusaksessori @x = 10; @y = 100; @z = 1000 # Tälle luotiin sekä luku- että kirjoitusaksessorit end end
Kyseiset metodit on toteutettu Rubyn ytimessä C-kielellä C:n nopeusedun takia, mutta ne voitaisiin yhtä hyvin kirjoittaa Rubylla käyttämällä esimerkiksi class_eval-rakennetta (ks. Metaprogramming Ruby [pdf]).
public-määreellä merkityt metodit ovat kaikkien käytettävissä (tämä on oletusarvo).
private tarkoittaa, että vain kyseinen olio voi käyttää metodia
(toisen samantyyppisen olion metodiin ei ole pääsyä).
protected antaa pääsyn toisille saman luokan tai siitä perityn luokan edustajille.
Näkyvyydensäätelymääreitä voidaan kirjoittaa kahdella tavalla: joko listaamalla symboleilla metodeja, joille määre pätee, tai kirjoittamalla määreen avainsana jollekin riville yksin, jolloin määre on voimassa siitä kohdasta eteenpäin.
class Visibilities def privaatti end def privaatti2 end private :privaatti, :privaatti2 protected def tasta_eteenpain_kaikki_protectedeja end end
Moduulit ovat metodikokoelmia. Niitä voidaan käyttää nimiavaruuksina (esim. Math-moduuli) tai mixin-luokkina.
module OddEven def OddEven.odd?(x) # kirjastometodi; vrt. luokkametodi x % 2 == 1 end def OddEven.even?(x) x % 2 == 0 end end OddEven.odd? 123 # => true
Moduuliin voidaan kirjoitaa myös ilmentymämetodeja, joita voidaan liittää luokkiin (tai toisiin moduuleihin) include-avainsanalla.
module Odd def odd? self % 2 == 1 end end module OddEven include Odd # Moduuli voidaan liittää toiseen moduuliin def even? self % 2 == 0 end end class Integer include OddEven # Moduulin liittäminen luokkaan end 123.odd? # => true
Moduulin liittäminen korvaa luokassa tai moduulissa olevat samannimiset metodit.
Moduulin voi yhtä hyvin liittää yksittäisten olioiden ilmentymäluokkiin.
module OnlyWhiteSpace def only_whitespace? self.strip.empty? end end a = " " b = " 42" c = " " class << a include OnlyWhiteSpace end b.extend OnlyWhiteSpace # Vaihtoehtoinen tapa a.only_whitespace? # => true b.only_whitespace? # => false c.only_whitespace? # => virhe
Moduulin poistamiseen luokasta ei ole mitään valmista keinoa. Ominaisuus on jätetty tarkoituksella kielestä pois liian ongelmallisena esimerkiksi perinnän yhteydessä.
Poikkeuksia heitetään avainsanalla raise ja siepataan begin – end -lohkoissa rescue -ilmaisulla.
begin if not 123.respond_to? :even? raise NoMethodError.new("Haluan käyttää even?-metodia!") end rescue NoMethodError => ex puts 'Tapahtui virhe. Järjestelmä sanoo: "' + ex.message + '"' end
begin .. raise .. rescue on tarkoitettu virheiden käsittelyyn. raise:lla voidaan heittää vain Exception-luokasta periviä luokkia tai olioita (merkkijonoista tehdään RuntimeError-olioita).
Rubyssa on myös toinen vastaavanlainen rakenne, catch :sym { .. throw :sym arvo }, joka on tarkoitettu arvon palauttamiseen syvästä rekursiopinosta. throw ottaa parametrikseen symbolin ja palautettavan olion (oletuksena nil) ja palauttaa kontrollin ensimmäiselle vastaantulevalle catch-ilmaisulle, joka odottaa kyseistä symbolia.
def tee_jotain throw :valmis, 42 end arvo = catch :valmis do tee_jotain end puts arvo.to_s # => 42
Tätä epätyylikästä ilmaisua tarvitaan onneksi melko harvoin.
method_missing on eräs voimakas metaohjelmoinnin väline. Jokaisella oliolla on metodi method_missing, jota kutsutaan aina, kun oliolle lähetetään kutsu metodille, jota oliolla ei ole. Oletuksena method_missing heitää NoMethodError-poikkeuksen.
class Integer def method_missing(metodi, *argumentit, &lohko) puts "Ei kokonaisluvuilla ole metodia #{metodi}!" super end end 3.foo