10. Blokovi i procedure

Ovo su definitivno neke od najzanimljivijih stvari koje Ruby ima.

Imaju njih i neki drugi programski jezici, iako ih oni nazivaju drukčijim imenima, ali oni najpopularniji ih nemaju – stvarno šteta!

Šta je to tako cool i koje su to stvari cool u Ruby? Ono što je ustvari sjajno je da možemo odvojiti dio koda, jedan blok (block), umotati ga u jedan drugi objekat (nazvan Proc), smjestiti ga u varijablu ili predati nekoj metodi i izvršiti taj dio kad poželimo (čak i više puta, ako poželimo). Dakle, liči pomalo na metodu, osim što nije vezano za objekat (ono ustvari jeste objekat) i može se pohraniti ili predati nekoj metodi kao bilo koji drugi objekat. Mislim da je vrijeme za primjer:

toast = Proc.new do

puts 'Cheers!'

end

toast.call

toast.call

toast.call

Ispis:

Cheers!

Cheers!

Cheers!

Dakle, kreirao sam -Proc- (što bi trebalo da je skraćenica od „procedure“, ali što je još važnije, rimuje se sa „block“) koji je sadržavao jedan blok koda, a nakon toga sam pozvao proceduru tri puta. Primjetit ćete da je plaho nalik na metode.

Ustvari, procedure su još sličnije metodama nego sam vam ja to pokazao u ovom primjeru, jer blokovi mogu sadržavati parametre:

doYouLike = Proc.new do |aGoodThing|

puts 'I *really* like '+aGoodThing+'!'

end

doYouLike.call 'chocolate'

doYouLike.call 'ruby'

Ispis:

I *really* like chocolate!

I *really* like ruby!

Dobro,...vidjesmo šta su procedure i blokovi, kako ih koristiti,...a da li smo shvatili poentu? Zašto jednostavno ne koristiti metode? E, pa...zato što postoje neke stvari koje s metodama jednostavno ne možete učiniti. Da budem precizniji, ne možete uvoditi jednu metodu u drugu (ali procedure u metode možete), a metode ne mogu kao povratnu vrijednost dati druge metode (ali mogu vraćati procedure). Da pojednostavim, razlog je što su procedure objekti, a metode to nisu.

(Usput, je li vam poznat išto od ovoga? Aha, vidjeli ste vi blokove i ranije...kad smo učili iteratore. Hajde da popričamo malo više o tome.)

Metode koje prihvaćaju procedure

Kad proceduru uvrstimo u neku metodu, onda možemo kontrolisati kako ili koliko puta ćemo pozvati tu proceduru. Naprimjer, recimo da nešto želimo uraditi prije i nakon što se dio koda izvrši:

def doSelfImportantly someProc

puts 'Everybody just HOLD ON! I have something to do...'

someProc.call

puts 'Ok everyone, I\'m done. Go on with what you were doing.'

end

sayHello = Proc.new do

puts 'hello'

end

sayGoodbye = Proc.new do

puts 'goodbye'

end

doSelfImportantly sayHello

doSelfImportantly sayGoodbye

Ispis:

Everybody just HOLD ON! I have something to do...

hello

Ok everyone, I'm done. Go on with what you were doing.

Everybody just HOLD ON! I have something to do...

goodbye

Ok everyone, I'm done. Go on with what you were doing.

Možda se na prvi pogled ovaj primjer i ne čini tako spektakularnim, ali on to jeste :o). Postalo je uobičajeno u programiranju imati striktne zahtjeve kad se šta treba izvršiti. Ako, naprimjer, želite spasiti neki fajl, morate otvoriti fajl, ispisati informacije koje želite imati, i onda zatvoriti fajl. Ako ga zaboravite zatvoriti, svašta bi se moglo desiti. No, svaki put kad želite spasiti ili učitati fajl, morate uraditi opet isto: otvoriti ga, izvršiti unos ili izmjenu, a onda ga zatvoriti. Sve je to naporno i brzo se zaboravlja. U Ruby, spašavanje (ili učitavanje) fajlova funkcioniše na sličan način kao u prethodnom primjeru, tako da se ne morate brinuti nizašta drugo osim onoga što ustvari želite spasiti/sačuvati. (U narednom poglavlju ću vam pokazati gdje ćete naći stvari koje će vam pomoći da uradite stvari kao što je spašavanje ili učitavanje fajlova).

Takođe možete napisati i metode koje će utvrditi koliko puta ili čak da li uopšte pozvati proceduru. Evo metode koja će pozvati proceduru za upola manje vremena i još jedne koju ćemo pozvati dvaput:

def maybeDo someProc

if rand(2) == 0

someProc.call

end

end

def twiceDo someProc

someProc.call

someProc.call

end

wink = Proc.new do

puts '<wink>'

end

glance = Proc.new do

puts '<glance>'

end

maybeDo wink

maybeDo glance

twiceDo wink

twiceDo glance

Ispis:

<glance>

<wink>

<wink>

<glance>

<glance>

(Na originalnoj stranici [http://pine.fm/LearnToProgram/] vidjet ćete svaki put kad pristupite stranici ili uradite refresh, da će se pojaviti drukčiji rezultat.) Ovo su neke od raširenijih primjena procedura, koje nam daju mogućnost da uradimo stvari koje ne bismo mogli koristeći samo metode. Naravno, mogli bismo napisati metodu da namigne dvaput, ali ne bismo mogli napisati metodu da uradimo „nešto“ dvaput!

Prije nego nastavimo, još jedan mali primjer. Do ovog momenta, procedure su prikazane na prilično sličan način, kad se uzajamno porede. Ovaj put će biti znatno drukčije, da biste vidjeli koliko metoda zavisi od procedura koje su u njoj sadržane. Naša metoda će tretirati objekat i proceduru,...i pozvati proceduru nad tim objektom. Ako procedura vrati „false“, prekidamo aplikaciju. u suprotnom pozvaćemo proceduru s vraćenim objektom. Proces će trajati sve dok ne dobije povratnu vrijednost „false“ (koju je bolje da uradi što prije, da program ne padne). Metoda treba da vrati poslijednju vrijednost koja nije „false!“.

def doUntilFalse firstInput, someProc

input = firstInput

output = firstInput

while output

input = output

output = someProc.call input

end

input

end

buildArrayOfSquares = Proc.new do |array|

lastNumber = array.last

if lastNumber <= 0

false

else

array.pop # Uzmemo poslijednji broj u nizu...

array.push lastNumber*lastNumber # ...zamijenimo ga njegovim kvadratom

array.push lastNumber-1 # ...nakon čega slijedi broj manji za 1 number.

end

end

alwaysFalse = Proc.new do |justIgnoreMe|

false

end

puts doUntilFalse([5], buildArrayOfSquares).inspect

puts doUntilFalse('I\'m writing this at 3:00 am; someone knock me out!', alwaysFalse)

Ispis:

[25, 16, 9, 4, 1, 0]

I'm writing this at 3:00 am; someone knock me out!

Dobro, bijaše to prilično čudan primjer, priznajem. Međutim, ovaj primjer pokazuje koliko su različite primjene metoda kad im se daju različite procedure.

Metoda –inspect- je vrlo slična metodi -to_s-, osim što na string koji vraća pokušava pokazati kod koji je namijenjen za izgradnju objekta koji smo mu proslijedili. U ovom primjeru, pokazuje nam čitav niz (array), nakon što prvi put pozovemo metodu doUntilFalse. Takođe, možete uočiti da ustvari nikad nismo kvadrirali nulu na kraju niza, ali kako je kvadrat nule opet nula, nismo ni morali. Pošto je alwaysFalse uvijek netačno, doUntilFalse metoda nije drugi put uradila apsolutno ništa kad smo je pozvali; samo je vratila ono što smo joj proslijedili.

Metode čije su povratne vrijednosti procedure

Jedna od mnogih “strava” stvari koje možemo uraditi s procedurama je da ih kreiramo i dobijemo kao povratnu vrijednost metoda. To nam dozvoljava mnogo lude programerske moći (stvari impresivnih imena, kao što su lijena evaluacija, beskonačne strukture podataka), ali stvar je u tome što ja to skoro nikad ne koristim u praksi, niti se sjećam da sam ikoga vidio da je uradio nešto tako u svom kodu. Mislim da je to jedna od onih stvari koje na kraju i ne iskoristimo u Ruby, ili nas možda Ruby samo ohrabruje da pokušamo naći i drugačija riješanja. Ne znam. U svakom slučaju, samo ću navesti ovo kao opciju.

U ovom primjeru, metoda –compose- uzima dvije procedure i kao povratnu vrijednost daje treću, koja, kad se pozove, poziva prvu proceduru i njen rezultat šalje u drugu.

def compose proc1, proc2

Proc.new do |x|

proc2.call(proc1.call(x))

end

end

squareIt = Proc.new do |x|

x * x

end

doubleIt = Proc.new do |x|

x + x

end

doubleThenSquare = compose doubleIt, squareIt

squareThenDouble = compose squareIt, doubleIt

puts doubleThenSquare.call(5)

puts squareThenDouble.call(5)

Ispis:

100

50

Napomena: važno je reći da se –proc1- morala naći unutar zagrade da bi bila izvršena prije –proc2-.

Proslijeđivanje blokova (ne procedura) u metode

Dobro, ovo je bio dio zanimljiv za akademsku upotrebu, ali i pomalo mučan za koristiti. Veliki je problem je što postoje tri koraka kroz koja moramo proći (definisanje metode, kreiranje procedure i pozivanje metode koja sadrži tu proceduru), a moglo bi ih biti samo dva (definisanje metode i proslijeđivanje/uvrštavanje bloka koda odmah u nju, bez korištenja procedure), pošto većinom više ne želite koristiti proceduru/blok nakon što ih jednom provučete kroz metodu. Eh, pa, trebali biste znati da je to Ruby sve skontala za nas! Ustvari, svaki put kad smo upotrebljavali iteratore, koristili smo ovo.

Prvo ću dati jedan mali primjer, pa ćemo onda na objašnjenja:

class Array

def eachEven(&wasABlock_nowAProc)

isEven = true

# Počinjemo sa "true" jer nizovi počinju brojem 0, koji se smatra parnim

self.each do |object|

if isEven

wasABlock_nowAProc.call object

end

isEven = (not isEven) # Praviti razliku među parnim i neparnim

end

end

end

['apple', 'bad apple', 'cherry', 'durian'].eachEven do |fruit|

puts 'Yum! I just love '+fruit+' pies, don\'t you?'

end

# Upamtite, iz niza uzimamo parne brojeve, a u nizu postoje

# i oni koji to nisu,...samo zato što ja volim praviti probleme.

[1, 2, 3, 4, 5].eachEven do |oddBall|

puts oddBall.to_s+' is NOT an even number!'

end

Ispis:

Yum! I just love apple pies, don't you?

Yum! I just love cherry pies, don't you?

1 is NOT an even number!

3 is NOT an even number!

5 is NOT an even number!

Da bi proslijedili blok metodi –eachEven-, sve što smo trebali učiniti je zalijepiti taj blok nakon metode. Na ovaj način možete zalijepiti bilo koji blok koda, iako će većina metoda jednostavno ignorisati blok. Da se to ne bi desilo, nego da bi metoda „prigrabila“ blok i pretvorila ga u proceduru, postavite ime te procedure na kraj liste parametara vaše metode, stavljajući ispred imena znak (&). Taj dio je malo zamršen, ali ne i previše, obzirom da ga je potrebno uraditi samo jednom, na početku, pri definisanju metode. Nakon toga možete tu metodu koristiti onoliko puta koliko poželite, baš kao i neke integrisane metode,...kao što su –each- ili –times- (sjećate li se 5.times do?)

Ako se zbunite na momenat, pomislite samo što bi eachEven trebalo da učini: pozove proslijeđeni blok koda za svaki od elemenata u nizu. Nakon što ste je (metodu) napisali i uvjerili se da radi, nije potrebno da razmišljate šta se dešava „pod haubom“ (koji blok se kada poziva??); ustvari, to je upravo razlog zbog kojeg pišemo metod na ovaj način: da ne bismo morali misliti na koji način one rade. Jednostavno, mi ih samo koristimo.

Sjećam se da sam pokušavao vidjeti koliko traju pojedini dijelovi programa (što još poznato i kao profiliranje koda). Tako sam napisao metodu koja mjeri vrijeme prije izvršavanja koda, onda ga izvršava, a nakon toga ponovo mjeri vrijeme na kraju i ispisuje razlike. Trenutno ne mogu pronaći taj kod, ali mi trenutno i ne treba; vjerovatno je ličio na nešto ovako:

def profile descriptionOfBlock, &block

startTime = Time.now

block.call

duration = Time.now - startTime

puts descriptionOfBlock+': '+duration.to_s+' seconds'

end

profile '25000 doublings' do

number = 1

25000.times do

number = number + number

end

puts number.to_s.length.to_s+' digits' # That is, the number of digits in this HUGE number.

end

profile 'count to a million' do

number = 0

1000000.times do

number = number + 1

end

end

Ispis:

7526 digits

25000 doublings: 1.06135 seconds

count to a million: 0.84302 seconds

Kako je samo jednostavno! Elegantno! Ovom malom metodom, mogu izmjeriti vrijeme između pojedinih odjeljaka bilo kojeg programa. Jednostavno ubacim kod u blok i pošaljem ga na profiliranje. Ima li šta jednostavnije? U većini programskih jezika, morao bih eksplicitno dodati kod za mjerenje vremena za svai dio programa kojemu sam želio izmjeriti vrijeme. U Ruby, međutim, sve je ne jednom mjestu i što je još važnije – nije mi na putu!

Par stvarčica za oprobati

    • Đedov sahat“. Napravite metodu koja uzima blok koda i poziva ga svaka četiri sata u jednom danu. Naprimjer, ako bismo proslijedili blok „do puts 'DONG!' end“, program bi vratio neku vrstu alarma (klinga), kao djedov stari sat. Testirajte svoj kod sa par različitih blokova (uključujući i ovaj koji sam vam dao). Caka: možete iskoristiti Time.now.hour da dobijete trenutno vijeme (sat). Međutim, ova metoda će vam vratiti broj između 0 i 23, tako da ćete morati izmijeniti te vrijednosti da biste dobili standardne vrijednosti koji ima jedan zidni sat,...stari „Đedov sahat“ (1-12).
    • Bilježnik programa“ Napišite metodu koju ćete nazvati –log-, koja daje tekstualni ispis koji opisuje blok koda na kome trenutno radi; slično metodi doSelfImportantly, trebao bi redati stringove koji nam govore kad počne jedan blok, a kad završava, te uz to šta je i povratna vrijednost bloka. Testirajte svoju metodu tako što ćete joj poslati blok nekog koda. Unutar bloka, pozovite se još jednom na -log- proslijeđujući i njemu novi blok (ovakav način integrisanja koda se naziva „gnježdenje“). Drugim riječima, ispis bi trebao izgledati otprilike ovako

Beginning "outer block"...

Beginning "some little block"...

..."some little block" finished, returning: 5

Beginning "yet another block"...

..."yet another block" finished, returning: I like Thai food!

..."outer block" finished, returning: false

    • Bolji bilježnik”. Ispis koji je imao prošli program je nekako težak za razumjeti i što bi ga više koristili, bivalo bi sve gore. Bilo bi ga daleko bolje shvatiti ako bi se nekako sortirale linije koda tako da možemo razlikovati unutrašnje i vanjske blokove. Da bismo postigli ovo, trebaće nam globalne varijable, one koje možemo vidjeti/pristupiti im u bilo kojem dijeu koda. Globalne varijable se definišu tako da ispred njihovog imena postavimo zna ($), kao npr. $global, $nestingDepth... Na kraju, bilježnik bi trebao dati ispis sličan ovome: Beginning "outer block"...

Beginning "some little block"...

Beginning "teeny-tiny block"...

..."teeny-tiny block" finished, returning: lots of love

..."some little block" finished, returning: 42

Beginning "yet another block"...

..."yet another block" finished, returning: I love Indian food!

..."outer block" finished, returning: true

Dobro, to bi bilo sve što ćemo naučiti u ovom tutorialu. Čestitam! Dosta ste naučili! Moguće je da ćete trenutno osjećati da se baš svega i ne sjećate, ili da ste određene dijelove preskočili...ali to nema veze, stvarno! Ne radi se u programiranju o tome šta ste zapamtili i šta od toga znate, nego se radi o tome šta možete „skontati“. Sve dok možete i znate gdje naći stvari koje ste zaboravili, ne morate se puno brinuti. Nadam se da ne mislite da sam sve ovo napisao ne gledajući uvijek na poglavlja iza i podsjećajući sebe na neke stvari koje sam zaboravio! Zato što jesam – stalno sam se vraćao na prethodna poglavlja. Dosta mi je pomogao i kod koji je dio ovog tutoriala, kod u vidu primjera. A sad pogledajte gdje sam to ja sve tražio ono što sam na trenutak zaboravio ili gdje sam tražio pomoć kad mi je trebala.

----------------------------------------------

© 2003-2009 Chris Pine

http://pine.fm/LearnToProgram/