Ruby Language
Blocs et Procs et Lambdas
Recherche…
Syntaxe
- Proc.new ( bloc )
- lambda {| args | code}
- -> (arg1, arg2) {code}
- object.to_proc
- {| single_arg | code}
- do | arg, (clé, valeur) | fin du code
Remarques
Faites attention à la priorité des opérateurs lorsque vous avez une ligne avec plusieurs méthodes enchaînées, comme:
str = "abcdefg"
puts str.gsub(/./) do |match|
rand(2).zero? ? match.upcase : match.downcase
end
Au lieu d'imprimer quelque chose comme abCDeFg
, comme on pouvait s'y attendre, il imprime quelque chose comme #<Enumerator:0x00000000af42b28>
- c'est parce que do ... end
a priorité moindre que les méthodes, ce qui signifie que gsub
ne voit que l' /./
arguments , et non l'argument de bloc. Il retourne un énumérateur. Le bloc finit par passer à puts
, qui l'ignore et affiche simplement le résultat de gsub(/./)
.
Pour résoudre ce problème, gsub
appel gsub
entre parenthèses ou utilisez plutôt { ... }
.
Proc
def call_the_block(&calling); calling.call; end
its_a = proc do |*args|
puts "It's a..." unless args.empty?
"beautiful day"
end
puts its_a #=> "beautiful day"
puts its_a.call #=> "beautiful day"
puts its_a[1, 2] #=> "It's a..." "beautiful day"
Nous avons copié la méthode call_the_block
du dernier exemple. Ici, vous pouvez voir qu'un processus est fait en appelant la méthode proc
avec un bloc. Vous pouvez également voir que les blocs, comme les méthodes, ont des retours implicites, ce qui signifie que procs (et lambdas) le font aussi. Dans la définition de its_a
, vous pouvez voir que les blocs peuvent prendre des arguments splat aussi bien que des arguments normaux; ils sont également capables de prendre des arguments par défaut, mais je ne pouvais pas penser à un moyen de travailler avec. Enfin, vous pouvez voir qu'il est possible d'utiliser plusieurs syntaxes pour appeler une méthode - soit la méthode d' call
, soit le []
opérateur
Lambdas
# lambda using the arrow syntax
hello_world = -> { 'Hello World!' }
hello_world[]
# 'Hello World!'
# lambda using the arrow syntax accepting 1 argument
hello_world = ->(name) { "Hello #{name}!" }
hello_world['Sven']
# "Hello Sven!"
the_thing = lambda do |magic, ohai, dere|
puts "magic! #{magic}"
puts "ohai #{dere}"
puts "#{ohai} means hello"
end
the_thing.call(1, 2, 3)
# magic! 1
# ohai 3
# 2 means hello
the_thing.call(1, 2)
# ArgumentError: wrong number of arguments (2 for 3)
the_thing[1, 2, 3, 4]
# ArgumentError: wrong number of arguments (4 for 3)
Vous pouvez également utiliser ->
pour créer et .()
Pour appeler lambda
the_thing = ->(magic, ohai, dere) {
puts "magic! #{magic}"
puts "ohai #{dere}"
puts "#{ohai} means hello"
}
the_thing.(1, 2, 3)
# => magic! 1
# => ohai 3
# => 2 means hello
Ici, vous pouvez voir qu'un lambda est presque identique à un proc. Cependant, il y a plusieurs mises en garde:
L'arité des arguments d'un lambda est appliquée; transmettre le nombre d'arguments incorrect à un lambda, déclenche une erreur
ArgumentError
. Ils peuvent toujours avoir des paramètres par défaut, des paramètres de splat, etc.return
depuis un lambda retourne du lambda, tout enreturn
d'un proc retourne hors de la portée englobante:def try_proc x = Proc.new { return # Return from try_proc } x.call puts "After x.call" # this line is never reached end def try_lambda y = -> { return # return from y } y.call puts "After y.call" # this line is not skipped end try_proc # No output try_lambda # Outputs "After y.call"
Objets en tant qu'arguments de bloc aux méthodes
Mettre un &
(esperluette) devant un argument le transmettra comme bloc de la méthode. Les objets seront convertis en un Proc
utilisant la méthode to_proc
.
class Greeter
def to_proc
Proc.new do |item|
puts "Hello, #{item}"
end
end
end
greet = Greeter.new
%w(world life).each(&greet)
Ceci est un modèle courant dans Ruby et de nombreuses classes standard le fournissent.
Par exemple, Symbol
implémente to_proc
en s'envoyant à l'argument:
# Example implementation
class Symbol
def to_proc
Proc.new do |receiver|
receiver.send self
end
end
end
Cela active l'idiome utile &:symbol
, couramment utilisé avec les objets Enumerable
:
letter_counts = %w(just some words).map(&:length) # [4, 4, 5]
Blocs
Les blocs sont des morceaux de code entre accolades {}
(généralement pour les blocs à une seule ligne) ou do..end
(utilisés pour les blocs à plusieurs lignes).
5.times { puts "Hello world" } # recommended style for single line blocks
5.times do
print "Hello "
puts "world"
end # recommended style for multi-line blocks
5.times {
print "hello "
puts "world" } # does not throw an error but is not recommended
Remarque: les accolades ont une priorité plus élevée que do..end
Céder
Les blocs peuvent être utilisés dans les méthodes et les fonctions en utilisant le mot yield
:
def block_caller
puts "some code"
yield
puts "other code"
end
block_caller { puts "My own block" } # the block is passed as an argument to the method.
#some code
#My own block
#other code
Attention, si yield
est appelé sans bloc, il LocalJumpError
une LocalJumpError
. block_given?
fournit à cette fin une autre méthode appelée block_given?
cela vous permet de vérifier si un bloc a été passé avant d'appeler le rendement
def block_caller
puts "some code"
if block_given?
yield
else
puts "default"
end
puts "other code"
end
block_caller
# some code
# default
# other code
block_caller { puts "not defaulted"}
# some code
# not defaulted
# other code
yield
peut également fournir des arguments au bloc
def yield_n(n)
p = yield n if block_given?
p || n
end
yield_n(12) {|n| n + 7 }
#=> 19
yield_n(4)
#=> 4
Bien qu'il s'agisse d'un exemple simple, le yield
peut être très utile pour permettre un accès direct aux variables d'instance ou aux évaluations dans le contexte d'un autre objet. Par exemple:
class Application
def configuration
@configuration ||= Configuration.new
block_given? ? yield(@configuration) : @configuration
end
end
class Configuration; end
app = Application.new
app.configuration do |config|
puts config.class.name
end
# Configuration
#=> nil
app.configuration
#=> #<Configuration:0x2bf1d30>
Comme vous pouvez le constater, l'utilisation de yield
de cette manière rend le code plus lisible que l'appel continu de app.configuration.#method_name
. Au lieu de cela, vous pouvez effectuer toute la configuration à l'intérieur du bloc en conservant le code contenu.
Les variables
Les variables pour les blocs sont locales au bloc (similaire aux variables des fonctions), elles meurent lorsque le bloc est exécuté.
my_variable = 8
3.times do |x|
my_variable = x
puts my_variable
end
puts my_variable
#=> 0
# 1
# 2
# 8
Les blocs ne peuvent pas être enregistrés, ils meurent une fois exécutés. Pour enregistrer des blocs, vous devez utiliser procs
et lambdas
.
Conversion en Proc
Les objets qui répondent à to_proc
peuvent être convertis en procs avec l'opérateur &
(qui leur permettra également d'être transmis en tant que blocs).
La classe Symbol définit #to_proc
afin qu'elle tente d'appeler la méthode correspondante sur l'objet qu'elle reçoit en paramètre.
p [ 'rabbit', 'grass' ].map( &:upcase ) # => ["RABBIT", "GRASS"]
Les objets méthode définissent également #to_proc
.
output = method( :p )
[ 'rabbit', 'grass' ].map( &output ) # => "rabbit\ngrass"
Application partielle et currying
Techniquement, Ruby n'a pas de fonctions, mais des méthodes. Cependant, une méthode Ruby se comporte presque de manière identique aux fonctions dans un autre langage:
def double(n)
n * 2
end
Cette méthode / fonction normale prend un paramètre n
, le double et renvoie la valeur. Définissons maintenant une fonction d'ordre supérieur (ou méthode):
def triple(n)
lambda {3 * n}
end
Au lieu de renvoyer un nombre, triple
renvoie une méthode. Vous pouvez le tester à l'aide d' Interactive Ruby Shell :
$ irb --simple-prompt
>> def double(n)
>> n * 2
>> end
=> :double
>> def triple(n)
>> lambda {3 * n}
>> end
=> :triple
>> double(2)
=> 4
>> triple(2)
=> #<Proc:0x007fd07f07bdc0@(irb):7 (lambda)>
Si vous voulez réellement obtenir le nombre triplé, vous devez appeler (ou "réduire") le lambda:
triple_two = triple(2)
triple_two.call # => 6
Ou plus concis:
triple(2).call
Currying et applications partielles
Cela n'est pas utile pour définir des fonctionnalités très basiques, mais cela est utile si vous voulez avoir des méthodes / fonctions qui ne sont pas appelées ou réduites instantanément. Par exemple, supposons que vous souhaitiez définir des méthodes qui ajoutent un nombre à un nombre spécifique (par exemple, add_one(2) = 3
). Si vous deviez en définir une tonne, vous pourriez faire:
def add_one(n)
n + 1
end
def add_two(n)
n + 2
end
Cependant, vous pourriez aussi faire ceci:
add = -> (a, b) { a + b }
add_one = add.curry.(1)
add_two = add.curry.(2)
En utilisant le lambda calcul, on peut dire que add
est (λa.(λb.(a+b)))
. Le curry est une façon d' appliquer partiellement add
. Donc add.curry.(1)
, est (λa.(λb.(a+b)))(1)
qui peut être réduit à (λb.(1+b))
. L'application partielle signifie que nous avons passé un argument à add
mais que l'autre argument a été fourni ultérieurement. La sortie est une méthode spécialisée.
Des exemples plus utiles de currying
Disons que nous avons une très grande formule générale, que si nous spécifions certains arguments, nous pouvons en tirer des formules spécifiques. Considérez cette formule:
f(x, y, z) = sin(x\*y)*sin(y\*z)*sin(z\*x)
Cette formule est conçue pour fonctionner en trois dimensions, mais supposons que nous ne souhaitons que cette formule en ce qui concerne y et z. Disons aussi que pour ignorer x, nous voulons lui donner la valeur pi / 2. Faisons d'abord la formule générale:
f = ->(x, y, z) {Math.sin(x*y) * Math.sin(y*z) * Math.sin(z*x)}
Maintenant, utilisons le curry pour obtenir notre formule yz
:
f_yz = f.curry.(Math::PI/2)
Ensuite, appeler le lambda stocké dans f_yz
:
f_xy.call(some_value_x, some_value_y)
C'est assez simple, mais disons que nous voulons obtenir la formule pour xz
. Comment pouvons-nous régler y
Math::PI/2
si ce n'est pas le dernier argument? Eh bien, c'est un peu plus compliqué:
f_xz = -> (x,z) {f.curry.(x, Math::PI/2, z)}
Dans ce cas, nous devons fournir des espaces réservés pour le paramètre que nous ne pré-remplissons pas. Pour plus de cohérence, nous pourrions écrire f_xy
comme ceci:
f_xy = -> (x,y) {f.curry.(x, y, Math::PI/2)}
Voici comment fonctionne le calcul lambda pour f_yz
:
f = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x))))
f_yz = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x)))) (π/2) # Reduce =>
f_yz = (λy.(λz.(sin((π/2)*y) * sin(y*z) * sin(z*(π/2))))
Maintenant, regardons f_xz
f = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x))))
f_xz = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x)))) (λt.t) (π/2) # Reduce =>
f_xz = (λt.(λz.(sin(t*(π/2)) * sin((π/2)*z) * sin(z*t))))
Pour plus d'informations sur le lambda, essayez ceci .