1.4.1.10. fejezet, Blokkok, Eljárások és Lambdák

Mi is az a blokk a Ruby-ban. Erre egy példa:

3.times do |i|
  puts( i )
end

Ez egyenértékű az alábbival:

3.times { |i| # névtelen, u.n. Lambda függvény
  puts( i ) 
}

és az alábbival is:

3.times
do |i|
  puts( i )
end

Nézzük meg egy tömbön keresztül a cikluskezelést:

arr = ['one','two','three','four']
arr.each
  do |s| 
  puts(s)
end

és egy számsorozaton:

(1..3).each do |i|
  puts(i)
end

A ciklusok is sokfélék lehetnek. Nézzük például az alábbit:

i=0 
loop { 
  puts(arr[i]) 
  i+=1 
  if (i == arr.length) then 
    break 
  end
}

vagy ezeket:

b3 = [1,2,3].collect{|x| x*2}
b4 = ["hello","good day","how do you do"].collect{|x| x.capitalize }
b5 = ["hello","good day","how do you do"].each{|x| x.capitalize }

Mint látszik, a blokkok jelölése igen sokféle. de a b5-nek az értéke változatlan marad. De ha használjuk a ! jelet, megváltoztathatjuk az eredeti objektumot, és így már a kimeneti eredményt is:

b6 = ["hello","good day","how do you do"].each{|x| x.capitalize! }

Így lesz a b6 értéke:

["Hello", "Good day", "How do you do"]

És így fűzi egymás után a nagybetűvé alakított szöveget:

a = "hello world".split(//).each{ |x| newstr << x.capitalize }

Vigyázzunk azonban itt a felkiáltójel használatára. A

capitalize!

szóköz esetén (vagy ha nem történik értékváltozás) nil értéket próbál hozzáfűzni a newstr-hez, ami kivételt okozhat (TypeError). Ezt megkerülhetjük az each_byte metódussal:

a = "hello world".each_byte{|x| newstr << (x.chr).capitalize }

Az each_byte az ASCII karakterláncot bájtokra bontja, amit a chr metódussal visszaalakítunk karakterekké. Ez persze az UTF-8 karaktereknél nem működik, csak mintaként szolgált a két metódus használatára.

Hátul tesztelő ciklusra egy példát már láthattunk. A B osztály őseit felsoroló ciklus:

x=B
begin
  x = x.superclass
  puts(x)
end until x == nil

Eljárások és lambdák

A Ruby-ban minden objektum. Minden objektum osztályból keletkezik. Próbáljuk ki ezt egy Hash táblán:

puts({1=>2}.class)

Ha ezt egy blokkal próbálnánk ki, hibaüzenetet kapnánk.

puts({|i| puts(i)}.class)

Mégis megpróbálhatjuk objektumokká alakítani a blokkokat. Erre való a proc és a lambda függvény.

a = Proc.new{|x| x = x*10; puts(x) }
b = lambda{|x| x = x*10; puts(x) }
c = proc{|x| x.capitalize! }

Az "a" függvényt az a.call(100) kifejezéssel hívhatjuk. A "b" és "c" függvényeket ugyanígy. A Proc osztály annyiban tér el a lambda és proc függvényektől (Kernel metódusoktól), hogy olyan lambda objektumot hoz létre, ami nem ellenőrzi a metódus paramétereinek a számát. Például:

a = Proc.new{|x,y,z| x = y*z; puts(x) }
a.call(2,5,10,100) # nincs hibajelzés
b = lambda{|x,y,z| x = y*z; puts(x) }
b.call(2,5,10,100) # hibás paraméterezés (ArgumentError)
c = proc{|x,y,z| x = y*z; puts(x) }
c.call(2,5,10,100) # hibás paraméterezés (ArgumentError)

Yield

Most nézzünk egy-két mintát a név nélküli blokkok futtatására. Ezt a yield kulcsszóval érjük el.

def caps( anarg )
  yield( anarg )
end
 
caps( "a lowercase string" ){ |x| x.capitalize! ; puts( x ) }

Itt egy blokk és egy argumentum átadás látható. A névtelen blokk átadása a paraméter lista után történik. Amikor a caps metódus hívja a yield( anarg ) metódust, a string argumentum ("a lowercase string") átadódik a blokkban, az x változó értékül kapja, nagybetűssé alakítja, majd a puts kiírja a képernyőre.

Blokk a blokkban

Már láttuk, hogyan használjunk egy blokkot, végigléptetve azt egy tömb elemein. A következőkben egy blokkot arra használok, hogy végiglépkedjen egy stringeket tartalmazó tömb elemein, mindegyiket hozzárendelve a metódus s paraméterének. A másik blokk pedig átadja a caps metódusnak sorban, hogy kezdőbetűit nagybetűssé alakítsa

["hello","good day","how do you do"].each{
  |s|
  caps( s ){ |x| x.capitalize! 
    puts( x )
  }
}

Az eredmény pedig:

Hello
Good day
How do you do

Eddig láttunk már eljárás hívást call metóduson keresztül, yield kulcsszóval, most nézzünk meg mégegy formáját. Ha egy metódus argumentumlistája végén & jelet teszünk a változónév elé, akkor az biztosan Proc objektumként kerül átadásra.

def abc( a, b, c ) 
  uts('---abc---')
  a.call
  b.call
  c.call
  yield
end
 
def abc2( &d )
  puts('---abc2---')
  d.call
end
 
abc2{ puts "four" }
 
def abc3( a, b, c, &d)
  puts('---abc3---')
  a.call
  b.call
  c.call
  d.call(1)
  yield(2)
end
 
a = lambda{ puts "one" }
b = lambda{ puts "two" }
c = proc{ puts "three" }
myproc = proc{|x| puts("#{x} my proc") }
 
abc(a, b, c ){ puts "four" }
abc3(a, b, c, &myproc )

Visszatérési értékek:

---abc2---
four
---abc---
one
two
three
four
---abc3---
one
two
three
1 my proc
2 my proc

Az abc2 hívásánál vegyük észre, hogy a puts "four" egy névtelen blokk (puts("four")), és az abc metódus hasonlít az abc3-hoz, csak ott névtelen blokkot adunk át.

A blokk paraméterek (|abc|) csak a blokkon belül érhetők el. De az 1.8-as Ruby-ban a blokk hívási környezetébe inicializálhat helyi változót, ha a nevük egyezik (a,b,c eljárás változók). Erre érdemes odafigyelni, mert nagy galibát okozhatnak. Pl.:

a = "hello world"
def foo 
  yield 100 
end 
 
puts( a ) 
foo{ |a| puts( a ) }
puts( a ) #< a Ruby 1.8-ban ez mostmár 100. 1.9.1-ben marad "hello world".

Precedencia szabályok

A kapcsos zárójelek precedenciája erősebb, mint a do és az end.

foo bar do |s| puts( s ) end
foo bar{ |s| puts(s) }

Itt a foo és a bar két metódus. Melyiknek lessz a blokk átadva? Itt a do és end közé zárt blokk a bal oldalibb metódusnak lesz átadva, vagyis foo metódusnak, míg a kapcsos zárójelek közé zárt blokk a jobb oldalibb metódusnak, a bar metódusnak adódnak át. Az alábbi metódus megegyezik a fenti első metódussal:

foo( bar ) do |s| puts( s ) end

és a következő a második metódussal:

foo( bar{ |s| puts(s) } )

Blokkok mint léptetők

Objektumok, mint a tömb vagy a Finxnum, vannak létető metódusai (pl.: 3.times()). Ha szeretnénk saját metódusokat készíteni erre a célra, a következő mintát ajánlom:

def timesRepeat( aNum )
  for i in 1..aNum do 
    yield i 
  end
end
 
timesRepeat( 3 ){ |i| puts("[#{i}] hello world") }

Mindenképpen előnyös, objektum-orientált szemszögből, hogy egy osztálynak saját léptető metódusa legyen.

Blokk visszatérési értéke egy újabb blokk

def calcTax( taxRate )
	return lambda{
		|subtotal|
		subtotal * taxRate
		} 
end
 
salesTax = calcTax( 0.10 )
vat = calcTax( 0.175 )
 
print( "Tax due on book = ")
print( salesTax.call( 10 ) ) #<= prints: 1.0
 
print( "\nVat due on DVD = ")
print( vat.call( 10 ) ) #<= prints: 1.75

Így gyárthatók metódus sablonok is.

Blokk és példány változók

Először lokális változón vizsgáljuk meg a működést.

def aFunc( aClosure )
  @hello = "hello world"
  puts("inside the aFunc function")
  aClosure.call
end

Ez a névtelen függvény hivatkozik a main példány @hello tulajdonságra (definiáltuk az aFunc metódusban).

aClos = lambda{ 
	@hello << " yikes!"
	puts("in #{self} object of class #{self.class}, @hello = #{@hello}")
	}
# aClos.call #<= Ez hibát jelezne, mivel a @hello még ismeretlen (nil)
 
aFunc(aClos) #<= @hello = "hello world yikes!"
puts("outside the aFunc function")
 
aClos.call #<= @hello = "hello world yikes! yikes!"
aClos.call #<= @hello = “hello world yikes! yikes! yikes!”

Eredmény:

inside the aFunc function
in main object of class Object, @hello = hello world yikes!
outside the aFunc function
in main object of class Object, @hello = hello world yikes! yikes!
in main object of class Object, @hello = hello world yikes! yikes! yikes!

Most nézzük meg, mi történik, ha ezt a függvényt egy másik objektumnak adjuk át, és hogyan módosítja a példány tulajdonságát.

aClos = lambda{
  @hello << " yikes!"
  puts("in #{self} object of class #{self.class}, @hello = #{@hello}")
}
 
class X
  def y( b )
    @hello = "I say, I say, I say!!!"
    puts( " [In X.y]" ) 
    puts("in #{self} object of class #{self.class}, @hello = #{@hello}")
    puts( " [In X.y] when block is called..." )
    b.call 
  end
end
 
x = X.new
x.y( aClos )

Eredménye:

[In X.y]
in #<X:0x32a6e64> object of class X, @hello = I say, I say, I say!!!
[In X.y] when block is called...
in `block in <main>': undefined method `<<' for nil:NilClass (NoMethodError)

Világos, hogy a blokk abban a környezetben fut le, ahol létrehoztuk. A példány változót változatlanul hagyja. Habár ezzel a névvel létezik példány tulajdonság, nem látja (nil), így hozzáfűzni sem tud, kivételt generál. Ha előbb ott lenne az aFunc definíció, akkor a main objektumnak lenne @hello tulajdonsága, és azt módosítaná.

def aFunc( aClosure )
  @hello = "hello world"
  puts("inside the aFunc function")
  aClosure.call
end
 
aClos = lambda{ 
	@hello << " yikes!"
	puts("in #{self} object of class #{self.class}, @hello = #{@hello}")
	}
 
aFunc(aClos)
 
class X
	def y( b )
		@hello = "I say, I say, I say!!!"
		puts( "   [In X.y]" )
		puts("in #{self} object of class #{self.class}, @hello = #{@hello}")
		puts( "   [In X.y] when block is called..." )
		b.call		# <=== watch the value of the var @hello!
	end
end
 
puts( "======== x.y( aClos )" )
x = X.new
x.y( aClos )
puts( "======== self.inspect" )
puts self.inspect
puts( "======== x.inspect" )
puts x.inspect

Eredmény:

inside the aFunc function
in main object of class Object, @hello = hello world yikes!
======== x.y( aClos )
   [In X.y]
in #<X:0x9111f94> object of class X, @hello = I say, I say, I say!!!
   [In X.y] when block is called...
in main object of class Object, @hello = hello world yikes! yikes!
======== self.inspect
main
======== x.inspect
#<X:0x9111f94 @hello="I say, I say, I say!!!">

Blokk és lokális változó

x = 3000
 
c1 = lambda{
        |z|
	return z + 100	
	}
 
c2 = lambda{ 
	|y|	
	return y + 100	
	}
 
c3 = lambda{ 
	|x|
	return x + 100	
	}
 
puts( '=== c1 ===' )	
someval=1000
someval=c1.call(someval); puts(someval)
someval=c1.call(someval); puts(someval) 
# 2.times{ someval=c1.call(someval); puts(someval) }
puts( "x=#{x}" )
 
puts( "=== c2 ===" )
someval=1000
someval=c2.call(someval); puts(someval)
someval=c2.call(someval); puts(someval) 
# 2.times{ someval=c2.call(someval); puts(someval) }
puts( "x=#{x}" )
 
puts( "=== c3 ===" )
someval=1000
someval=c3.call(someval); puts(someval)
someval=c3.call(someval); puts(someval) 
# 2.times{ someval=c3.call(someval); puts(someval) }
puts( "x=#{x}" )
 
print("Which variables are defined?\n")
print("x=[#{defined?(x)}], z=[#{defined?(z)}]")

Eredmények Ruby 1.8.7-ben:

=== c1 ===
1100
1200
x=3000
=== c2 ===
1100
1200
x=3000
=== c3 ===
1100
1200
x=1100
Which variables are defined?
x=[local-variable], z=[]

Különbségként itt az 1.9.1-es visszatérési érték is, amiben már változtattak a c3 hatáskörén, az x lokális változó változatlan marad a c3-on kívül:

Eredmények Ruby 1.9.1-ben:

=== c1 ===
1100
1200
x=3000
=== c2 ===
1100
1200
x=3000
=== c3 ===
1100
1200
x=3000
Which variables are defined?
x=[local-variable], z=[]

A lokális változókról és láthatóságukról még egy példa:

def foo2 
  a = 100 
  for b in [1,2,3] do 
    c = b 
    a = b 
    print("a=#{a}, b=#{b}, c=#{c}\n") 
  end 
  print("Outside block: a=#{a}, b=#{b}, c=#{b}\n")
end
 
foo2

Eredmények:

a=1, b=1, c=1
a=2, b=2, c=2
a=3, b=3, c=3
Blokkon kívül: a=3, b=3, c=3

A blokk lokális változó metódus lokális változó is egyben. Azonban mielőtt hivatkozunk rá, inicializálni kell.