10. Lær at programmere

Blokke og procedurer 

 10 Blokke og Processer

 
Her kommer en af de smarteste ting ved Ruby. Der findes andre sprog der har det selv om de så normalt kalder det noget andet ("closures" -"aflukker") men de fleste af de mere populære sprog har det ikke og det er en skam.

Så hvad er denne smarte nye ting? Det er muligheden for at tage en blok af kode (kode mellem do og end), pakke det ind i et objekt (kaldet en proc (proces)) gemme det i en variabel eller videregive det til en metode og køre koden i blokken nårsomhelst du har lyst til det (mere end en gang hvis du vil) Så det er en slags metode, bortset fra at den ikke er bundet til et objekt (det ér et objekt) og du kan sende det rundt lige som du kan med ethvert andet objekt. Her er et eksempel:

hilsen = Proc.new do
    puts 'Skål!'
end

hilsen.call
hilsen.call
hilsen.call


Skål!
Skål!
Skål!


Så jeg skabte en proc (som jeg tror er en forkortelse for 'procedure', men vigtigere er det, at det rimer med 'blok') som indeholdt vores blok af kode. Så kaldte jeg denne proc tre gange. Som du kan se ligner det en metode meget.

Faktisk ligner det en metode endnu  for en proc kan også tage parametre:

kanDuLide = Proc.new do |nogetGodt|
  puts 'Jeg elsker virkelig  '+nogetGodt+'!'
end

kanDuLide.call 'chokolade'
kanDuLide.call 'ruby'


Jeg elsker virkelig chokolade!
Jeg elsker virkelig ruby!


Ok så vi kan se hvad blok og proc er, og hvordan vi kan bruge dem, men hvad er det så lige det smarte det er? Hvorfor ikke bare bruge metoder? Det er fordi der bare er ting du ikke kan gøre med metoder. Helt præcist kan du ikke sende metoder til andre metoder (men du kan sende procs til metoder), og metoder kan ikke returnere andre metoder (men de kan returnere procs). Det er altsammen fordi procs er objekter og det er metoder ikke.
 (Forresten, hvis det ligner noget du har set før, så er det fordi du hár set blokke før ... da du lærte om iteratorer. Men lad os snakke videre om det om lidt)

Metoder som kan tage procs



Når vi sender en proc til en metode kan vi kontrollere hvordan, om og hvor mange gange vi vil kalde vores proc. For eksempel lad os sige at der er noget vi ønsker at gøre før og efter noget kode skal køres:

def jegErVigtig    enProc
  puts "STOP allesammen, jeg har noget vigtigt jeg skal gøre"
  enProc.call
  puts 'Ok allesammen, nu er jeg færdig. Nu kan I gå videre med det I lavede.'
end

sigHej = Proc.new do
  puts 'hej'
end

sigFarvel = Proc.new do
  puts 'farvel'
end

jegErVigtig sigHej
jegErVigtig sigFarvel


STOP allesammen, jeg har noget vigtigt jeg skal gøre
hej
Ok allesammen, nu er jeg færdig. Nu kan I gå videre med det I lavede.
STOP allesammen, jeg har noget vigtigt jeg skal gøre
farvel
Ok allesammen, nu er jeg færdig. Nu kan I gå videre med det I lavede.


Måske synes det ikke som noget særligt ... men det er det ;-) Det er alt for almindeligt i programmering at have strikse krav til hvad der skal gøres hvornår. Hvis du ønsker at gemme en fil for eksempel skal du åbne filen, skrive den information som du ønsker den skal have og så lukke filen igen. Hvis du glemmer at lukke filen så kan der  ske slemme ting. Hver gang du vil gemme eller indlæse en fil så skal du gøre det samme: åbne filen, gøre det der skal gøres og så lukke filen. Det er besværligt og nemt at glemme. Når man i Ruby gemmer eller indlæser en fil virker det ligesom koden ovenfor, så du ikke skal bekymre dig om andet end det du vil gemme (eller indlæse). (I næste kapitel vil jeg vise dig hvordan du kan finde ud af hvordan man gør ting som fx at gemme eller at indlæse en fil.

Du kan også skrive metoder som vil bestemme hvor mange gange eller endog óm du vil kalde en proc. Her er en metode som vil kalde en proc som er sendt til den cirka halvdelen af gangene og en anden som vil kalde den to gange:

def kanLaves    enEllerAndenProc
  if rand(2) == 0
    enEllerAndenProc.call
  end
end

def lavToGange    enEllerAndenProc
    enEllerAndenProc.call
    enEllerAndenProc.call
end

seMig = Proc.new do
  puts '<seMig>'
end

opdagMig = Proc.new do
  puts '<opdagMig>'
end

kanLaves    seMig
kanLaves    opdagMig
 

lavToGange    seMig
lavToGange    opdagMig

<seMig>
<seMig>
<opdagMig>
<opdagMig>


(Hvis du reloader siden nogle få gange (på den engelske side) vil du se outputtet ændres) Her så du nogle af de mere almindelige måder at bruge procs på, der gør os i stand til at gøre ting som vi simpelthen ikke kunne gøre med metoder alene. Man kunne da helt sikkert have skrevet en metode der udskrev 'seMig' to gange men du kunne ikke skrive en som bare ville gøre noget to gange.

Før vi går videre lad os prøve at se på et sidste eksempel. Indtil nu har vi sendt nogle ret simple procs til frem og tilbage. Det vil vi ændre nu  så du kan se hvor meget sådan en metode afhænger af det proc objekt der sendes til det.  Vores metode vil tage et objekt og en proc. og vil kalde proc på dette objekt.  Hvis proc returnerer falsk afslutter vi. Ellers kalder vi proc med det returnerede objekt. Vi bliver ved med at gøre dette til proc returnerer false (som det bare har at gøre for ellers vil programmet gøre systemet inoperativt). Metoden vil returnere den sidste ikke falske værdi som proc'en returnerede.

 

def blivVedIndtilFalsk  inputEt , enProc
  input  = inputEt
  output = inputEt
 
  while output
    input  = output
    output = enProc.call input
  end
 
  input
end

bygArrayAfKvadrat= Proc.new do |array|
  senesteTal = array.last
  if senesteTal <= 0
    false
  else
    array.pop                         #      Tag det seneste tal væk ...
    array.push senesteTal*senesteTal  #  ... og erstat det med dets kvadrat ...
    array.push senesteTal-1           #  ... efterfulgt af det næste mindre tal ...
  end
end

altidFalsk = Proc.new do |ignorerMigBare|
  false
end

puts blivVedIndtilFalsk([5], bygArrayAfKvadrat).inspect
puts blivVedIndtilFalsk('Jeg skriver dette kl  3:00 om natten; er der en eller anden der gider slaa mig omkuld!', altidFalsk)


[25, 16, 9, 4, 1, 0]
Jeg skriver dette kl  3:00 om natten; er der en eller anden der gider slaa mig omkuld!




Ok så jeg vil godt indrømme at det var et temmelig skørt eksempel. Men det viser hvor forskellige ting en metode kan gøre, afhængigt af hvilke procs den får sendt.

Inspect metoden ligner meget metoden 'to_s'. bortset fra at strengen den returnerer forsøger at vise ruby koden som byggede objektet som du gav til den. Her viser den os hele sættet som blev returneret af vores første kald til blivVedIndtilFalsk. Måske kan du også se at vi faktisk aldrig kvadrerede tallet 0  (nul)  i slutningen af sættet men da 0 i anden, stadig er nul, behøvede vi det ikke. Og når altidFalsk var, ja altid falsk, behøvede blivVedIndtilFalsk ikke at gøre noget overhovedet anden gang vi kaldte den. Den returnerede bare hvad den fik sendt.

Metoder som returnerer procs


En af de andre smarte ting som du kan gøre med procs er at skabe dem inden i metoder og returnere dem. Det giver mange skøre programmeringsmuligheder: nogle med imponerende navne som fx, langsom evaluering, uendelige strukturer og 'currying' (efter en matematiker ved navn Curry), men hvis sandheden skal frem så bruger jeg det næsten aldrig i virkeligheden og jeg kan heller ikke komme på nogen andre der bruger det i deres kode. Jeg tror det er en at de ting du ikke normalt behøver at skulle lave når du programmerer i ruby. Eller måske opmuntrer ruby bare til andre løsninger. Jeg ved det ikke og vil kun berøre det kort.

I dette eksempel tager 'komponer' to procs og returnerer en ny proc som, når den bliver kaldt, kalder den første proc og returnerer resultatet i den anden proc.

def komponer  proc1, proc2
  Proc.new do |x|
    proc2.call(proc1.call(x))
  end
end

kvadrerDen = Proc.new do |x|
  x * x
end

fordobbelDen= Proc.new do |x|
  x + x
end

fordobbelOgBagefterKavdrer = komponer    fordobbelDen, kvadrerDen
kvadrerOgBagefterFordobbel = komponer   kvadrerDen, fordobbelDen

puts fordobbelOgBagefterKavdrer.call(5)
puts kvadrerOgBagefterFordobbel.call(5)

100
50

Bemærk at kaldet til proc1 skulle være indeni parenteserne til proc2 for at kunne udføres først


At videregive blokke (ikke procs) til metoder


Ok, så det var akademisk interessant men også besværligt at bruge. En stor del af problemet er at det er  tre trin man skal igennem (at definere metoden, lave proc'en og kalde metoden med proc'en), når det føles som om der faktisk kun burde  være to (at definere metoden og videregive blokken direkte ind i metoden uden at bruge proc'en overhovedet), fordi man for det meste ikke ønsker at bruge proc'en/blokken efter man har videregivet den til metoden. Og, fanfare!, det har ruby regnet ud for os. Faktisk har du allerede gjort det hver gang du har brugt gentagere.

Jeg vil vise dig et hurtigt eksempel først og så kan vi tale om det.

class Array
 
  def hverLige(&varEnBlock_nuEnProc)
    erLige = true  #  Vi starter med 'true' fordi sæt (arrays) starter med 0 som er et lige tal
   
    self.each do |object|
      if erLige
        varEnBlock_nuEnProc.call object
      end
     
      erLige = (not erLige)  #  Skift fra lige til ulige eller fra lige til ulige 
    end
  end

end

['apple', 'bad apple', 'cherry', 'durian'].hverLige do |frugt|
  puts 'Yum!  I just love '+frugt+' pies, don\'t you?'
end


# Husk vi får alle de lige elementer i
# sættet, som alle viser sig at have ulige tal,
# bare fordi jeg kan lide at lave den slags problemer.
[1, 2, 3, 4, 5].hverLige do |umulius|
  puts umulius.to_s+' er Ikke et lige tal!'
end



Yum!  I just love apple pies, don't you?
Yum!  I just love cherry pies, don't you?
1 er Ikke et lige tal!
3 er Ikke et lige tal!
5 er Ikke et lige tal!



Så hvis man vil sende en blok til hverLige skal man altså bare sætte en blok efter metoden. Du kan sende en blok til enhver metode på denne måde, men mange metoder vil bare ignorere blokken. For at gøre din metode i stand til at modtage blokken og gøre den til en proc, skal du sætte proc'ens navn i slutningen af din metodes parameter list, med en ampersand(&) foran. Så det er en smule besværligt men ikke alt for slemt, og så er det jo bare en gang (når du definerer metoden). Så kan du bruge metoden igen og igen ligesom en indbygget metode der tager blokke, for eksempel  'each' og 'times'. (Kan du huske den: 5.times do ... ?) 

Hvis du bliver forvirret så husk bare på hvad det er meningen at hverLige skal gøre: kalde blokken der er sendt til den med hver andet element i sættet. Så snart du har skrevet den og den virker, behøver du ikke længere at tænke på hvad den faktisk laver under motorhjælmen (hvilken blok der kaldes hvornår). Det er faktisk præcist grunden til at vi skriver sådanne metoder: så behøver vi aldrig mere at tænke på hvordan de virker, vi bruger dem bare. 

Jeg kan huske en gang hvor jeg ønskede at være i stand til at måle hvor lang tid forskellige dele af et program tog. (Det er også kendt som 'at profilere koden'). Så jeg skrev en metode som måler tiden før koden blev kørt. så kørte den koden og så måler den tiden igen ved slutningen og regner forskellen ud. Jeg kan ikke finde koden lige nu men det behøver jeg heller ikke. Den så sikkert nogenlunde sådan her ud:



def profil beskrivelseAfBlok, &blok
  startTime = Time.now
 
  blok.call
 
  varighed = Time.now - startTime
 
  puts beskrivelseAfBlok+':  '+varighed.to_s+' sekunder'
end

profil '25000 fordoblinger' do
  tal = 1
 
  25000.times do
    tal = tal + tal
  end
 
  puts tal.to_s.length.to_s+' cifre'  # Det vil sige antallet af cifre på dette KÆMPE tal
end

profil 'op til en million' do

  tal = 0
 
  1000000.times do
    tal = tal + 1
  end
end







7526 cifre
25000 fordoblinger:  0.191116 sekunder
op til en million:  0.563903 sekunder


Hvor simpelt! Hvor elegant!  Med denne korte metode kan jeg nu nemt måle tidsforbrug for enhver del af programmet som jeg ønsker. Jeg skal bare kaste koden ind i blokken og sende den til profil. Det kunne ikke være mere simplelt. På de fleste sprog ville det være nødvendigt udtrykkeligt at skrive denne timing kode rundt om hver sektion som jeg ønskede at måle tid på. I ruby  kan jeg holde det hele på et sted og vigtigere er det  at det er pakket væk.

Nogle ting du kan prøve.


Bedstefars Ur. Skriv en metode som tager en blok og kalder den en gang for hver time der er gået. På denne måde hvis jeg sendte den blokken 'do puts "DONG" end' så ville den kime (sådan da) som et gammelt Bedstefar ur. Test din metode  med nogle forskellige blokke (inklusiv den jeg gav dig) Tip: Du kan bruge Time.now.hour til at få timetallet vi ligenu har. Den giver dig et tal mellem o og 23 så du bliver nødt til at ændre det til et almindeligt urs tal mellem 1 og 12.

Program Logning. Skriv en metode som du kalder 'log' som tager en streng der beskriver blokken og så selvfølgelig blokken. Ligesom jegErVigtig skulle den skrive en streng der fortæller at den har startet blokken,  og en anden streng ved slutningen der fortæller at den nu har færdiggjort blokken og som også fortæller hvad blokken returnerer. Test din metode ved at sende den en blok. Inde i blokken sætter du et andet kald til 'log' idet du sender en blok til den (det kaldes 'nesting') med andre ord skulle dit output se cirka sådan her ud:

Begynder "ydre blok" ...
Begynder "en lille blok" ...
... "en lille blok" sluttede, returnerede: 5
Begynder "endnu en blok" ...
... "endnu en blok" sluttede, returnerede: Jeg kan lide Thai mad!
... "ydre blok" sluttede, returnerede: false


Bedre logning. Outputtet af denne seneste logger var lidt svær at læse, og det ville være blive værre jo mere du bruger den. Det ville være meget lettere at læse hvis den indryggede linierne i de indre blokke. For at gøre det skal man holde styr på hvor dybt inde i systemet du er, hver gang loggerne skal skrive noget. For at gøre det skal du bruge en global variabel, en variabel som du kan se alle steder fra i din kode. For at lave en global variabel skal du bare skrive $foran den, ligesom her: $global og  $indrykningsData.
Til sidst skal outputtet se cirka sådan her ud:

Begynder "Ydre blok" ...
  Begynder "en lille blok" ...
    Begynder "lille bitte blok" ...
    ..."lille bitte blok sluttede, returnerede: masser af kærlighed
  ..."en lille blok" sluttede, returnerede:  42
  Begynder "endnu en anden blok" ...
  ..."endnu en anden blok" sluttede, returnerede:  Jeg elsker indisk mad!
..."Ydre blok" sluttede, returnerede: true

Ja, det var så alt hvad du kommer til at lære fra denne lærebog. Tillykke! Du har lært en masse. Måske føler du ikke at du kan huske alting eller at du gik let hen over nogle dele ... Men det er helt i orden. At programmere handler ikke om hvad du ved. det er om hvad du kan finde ud af. Så længe du ved hvor du skal finde ud af de ting du glemte, klarer du dig fint. Jeg håber ikke du tror jeg har skrevet det hele uden at slå ting op hver andet minut. For det gjorde jeg. Jeg fik også en masse hjælp med koden som kører alle eksempler i denne lærebog. Men hvor slog jeg tingene op og hvem kunne jeg spørge om hjælp? Lad mig vise dig det!


 

Af Chris Pine

oversat af Gunner Carstens

Original tekst

Indledning

Kapitel 1 - Tal

Kapitel 2 -Bogstaver

Kapitel 3 Variable og tildelinger

Kapitel 4 Blande det hele sammen

Kapitel 5 Mere om metoder

Kapitel 6 Kontrol med strømmen

Kapitel 7 Sæt og Gentagere

Kapitel 8 Skriv dine egne metoder

Kapitel 9 Klasser

Kapitel 10 Blokke og Procs

Kapitel 11 Hvad nu?