Blocs et Procs
Les méthodes acceptent un bloc de code qui sera exécuté
avec le mot clé yield
. Par exemple:
def twice
yield
yield
end
twice do
puts "Hello!"
end
Le programme précédent affiche "Hello!" deux fois, une fois pour chaque yield
.
Pour définir une méthode qui reçoit un bloc, utilisez simplement yield
depuis celle-ci pour le faire savoir au compilateur.
Vous pouvez le faire de manière encore plus explicite en déclarant un argument de bloc factice, donné en tant que dernier argument préfixé avec un esperluette (&
):
def twice(&block)
yield
yield
end
Pour invoquer une méthode et passer un bloc, utilisez do ... end
ou { ... }
. Ce qui suit a un effet équivalent:
twice() do
puts "Hello!"
end
twice do
puts "Hello!"
end
twice { puts "Hello!" }
La différence entre do ... end
et { ... }
est que do ... end
s'applique à l'appel le plus à gauche, alors que { ... }
s'applique à l'appel le plus à droite:
foo bar do
something
end
# Ce qui précéde est équivalent à
foo(bar) do
something
end
foo bar { something }
# Ce qui précéde est équivalent à
foo(bar { something })
Cela a pour raison de permettre de créer des DSLs (Domain Specific Languages) en utilisant do ... end
afin de les rendre lisibles en anglais:
open file "foo.cr" do
something
end
# Equivalent à:
open(file("foo.cr")) do
end
Vous ne voudriez pas que ce qui précéde soit équivalent à:
open(file("foo.cr") do
end)
Surcharges
Deux méthodes, une qui yielde et l'autre non, sont considérées comme différentes surcharges, comme expliqué dans la section surcharge.
Arguments de yield
L'expression yield
est l'équivalent d'un appel et peut prendre des arguments. Par exemple:
def twice
yield 1
yield 2
end
twice do |i|
puts "Got #{i}"
end
Ce qui précéde affiche "Got 1" et "Got 2".
Une notation avec des accolades est aussi possible:
twice { |i| puts "Got #{i}" }
Un yield
peut prendre plusieurs valeurs:
def many
yield 1, 2, 3
end
many do |x, y, z|
puts x + y + z
end
# Output: 6
Un bloc peut spécifier moins que les arguments à utiliser par le yield:
def many
yield 1, 2, 3
end
many do |x, y|
puts x + y
end
# Output: 3
C'est une erreur que de spécifier plus d'arguments au bloc que ceux à utiliser par le yield:
def twice
yield
yield
end
twice do |i| # Error: too many block arguments
end
Chaque variable de bloc a le type de chaque expression du yield à cette même position. Exemple:
def some
yield 1, 'a'
yield true, "hello"
yield 2, nil
end
some do |first, second|
# first is Int32 | Bool
# second is Char | String | Nil
end
La variable de block second
inclut également le type Nil
car la dernière expression yield
n'inclut pas de second argument.
Syntaxe argument unique courte
Une syntaxe courte existe pour spécifier un bloc qui reçoit un unique argument et invoque une méthode avec. Ceci:
method do |argument|
argument.some_method
end
Peut être écrit ainsi:
method &.some_method
Ou comme ça:
method(&.some_method)
Ce qui précéde est simplement du sucre syntaxique et n'impacte aucunement les performances.
Les arguments peuvent aussi bien être passés à some_method
:
method &.some_method(arg1, arg2)
Et les opérateurs peuvent tout aussi bien être invoqués également:
method &.+(2)
method &.[index]
Valeur d'un yield
L'expression yield
elle-même a une valeur: la dernière expression du bloc. Par exemple:
def twice
v1 = yield 1
puts v1
v2 = yield 2
puts v2
end
twice do |i|
i + 1
end
Ce qui précéde affiche "2" et "3".
La valeur d'une expression yield
est surtout utile pour transformer et filtrer les valeurs.
Les meilleurs exemples sont les Enumerable#map
et les Enumerable#select:
ary = [1, 2, 3]
ary.map { |x| x + 1 } #=> [2, 3, 4]
ary.select { |x| x % 2 == 1 } #=> [1, 3]
Une méthode de tranformation factice:
def transform(value)
yield value
end
transform(1) { |x| x + 1 } #=> 2
Le résultat de la dernière expression est 2
car la dernière expression de la méthode transform
est yield
, dont la valeur est la dernière expression du bloc.
Restrictions de type
Le type du bloc dans une méthode qui utilise yield
peut être restreint en utilisant la syntaxe &block
. Par exemple:
def transform_int(start : Int32, &block : Int32 -> Int32)
result = yield start
result * 2
end
transform_int(3) { |x| x + 2 } #=> 10
transform_int(3) { |x| "foo" } # Error: expected block to return Int32, not String
break
Une expression break
dans un bloc fait retourner plus tôt la méthode:
def thrice
puts "Before 1"
yield 1
puts "Before 2"
yield 2
puts "Before 3"
yield 3
puts "After 3"
end
thrice do |i|
if i == 2
break
end
end
Ce qui précéde affiche "Before 1" et "Before 2". La méthode thrice
n'a pas exécuté l'expression puts "Before 3"
à cause du break
.
break
peut aussi accepter des arguments: ils deviennent les valeurs de retour de la méthode. Par exemple:
def twice
yield 1
yield 2
end
twice { |i| i + 1 } #=> 3
twice { |i| break "hello" } #=> "hello"
La valeur du premier appel est 3 car la dernière expression de la méthode twice
est yield
,
qui prend la valeur du bloc. La valeur du second appel est "hello" car un break
a eu lieu.
Si il y a des breaks conditionnels, la valeur de retour de l'appel sera l'union du type de la valeur du bloc et du type des différents break
:
value = twice do |i|
if i == 1
break "hello"
end
i + 1
end
value #:: Int32 | String
Si un break
reçoit plusieurs arguments, ils sont automatiquement transformés en Tuple:
values = twice { break 1, 2 }
values #=> {1, 2}
Si un break
ne reçoit aucun argument, c'est équivalent à recevoir un seul argument nil
:
value = twice { break }
value #=> nil
next
L'expression next
dans un bloc provoque une sortie prématurée du bloc (pas de la méthode). Par exemple:
def twice
yield 1
yield 2
end
twice do |i|
if i == 1
puts "Skipping 1"
next
end
puts "Got #{i}"
end
# Ouptut:
# Skipping 1
# Got 2
L'expression next
accepte des arguments, et ceux-ci sont pris comme valeur du yield
invoqué par le bloc:
def twice
v1 = yield 1
puts v1
v2 = yield 2
puts v2
end
twice do |i|
if i == 1
next 10
end
i + 1
end
# Output
# 10
# 3
Si un next
reçoit plusieurs arguments, ils sont automatiquement transformés en Tuple.
Si il ne reçoit aucun argument c'est équivalent à recevoir un seul argument nil
.
with ... yield
Une expression yield
peut être modifiée, en utilisant le mot-clé with
, pour spécifier l'objet à utiliser comme récepteur par défaut des appels de méthode du bloc:
class Foo
def one
1
end
def yield_with_self
with self yield
end
def yield_normally
yield
end
end
def one
"one"
end
Foo.new.yield_with_self { one } # => 1
Foo.new.yield_normally { one } # => "one"
Dépaquetez des arguments bloc
Un argument bloc peut spécifier des sous-arguments inclus entre parenthèses:
array = [{1, "one"}, {2, "two"}]
array.each do |(number, word)|
puts "#{number}: #{word}"
end
Ce qui précéde est du sucre syntaxique équivalent à:
array = [{1, "one"}, {2, "two"}]
array.each do |arg|
number = arg[0]
word = arg[1]
puts "#{number}: #{word}"
end
Cela signifie que tout type qui répond à []
avec des entiers peut être dépaqueté en argument de bloc.
Performance
Quand vous utilisez des blocs avec yield
, les blocs sont toujours soulignés:
closures, appels ou pointeurs de fonction ne sont associés. Cela signifie que ceci:
def twice
yield 1
yield 2
end
twice do |i|
puts "Got: #{i}"
end
est exactement égal à cela:
i = 1
puts "Got: #{i}"
i = 2
puts "Got: #{i}"
Par exemple, la librairie standard inclut une méthode times
sur des entiers, vous permettant d'écrire:
3.times do |i|
puts i
end
Ça a l'air fort sympathique, mais est-ce réellement aussi rapide qu'une boucle C? La réponse est: oui!
Voilà la définition de Int#times
:
struct Int
def times
i = 0
while i < self
yield i
i += 1
end
end
end
Parce-qu'un bloc non capturé est toujours souligné, l'invocation de la méthode précédente est exactement identique à:
i = 0
while i < 3
puts i
i += 1
end
N'ayez pas peur d'utiliser les blocs pour la lisibilité ou la ré-utilisation de code, cela n'affectera pas les performances de l'exécutable généré.