Bonne année, et bon retour sur Ruby Magic ! Dans cet épisode hivernal, nous allons plonger dans les fixations et les scopes. Alors mettez vos skis et suivez-nous au plus profond des bois.
La dernière fois, nous avons examiné les fermetures en Ruby en comparant les blocs, les procs et les lambdas. En dehors des différences entre les trois types, nous avons abordé ce qui définit une fermeture.
Une fermeture est une fonction de première classe avec un environnement. L’environnement est un mappage vers les variables qui existaient lorsque la fermeture a été créée. La fermeture conserve son accès à ces variables, même si elles sont définies dans une autre portée.
Nous avons exploré l’équivalent en Ruby des fonctions de première classe, mais nous avons commodément sauté les environnements. Dans cet épisode, nous allons voir comment cet environnement fonctionne pour les fermetures, les classes et les instances de classe en examinant comment Ruby gère la portée lexicale à travers ses bindings.
Portée lexicale
En programmation, la portée fait référence aux bindings disponibles à une partie spécifique du code. Une liaison, ou name binding, lie un nom à une référence mémoire, comme le nom d’une variable à sa valeur. La portée définit ce que self
signifie, les méthodes qui peuvent être appelées, et les variables qui sont disponibles.
Ruby, comme la plupart des langages de programmation modernes, utilise une portée statique, souvent appelée portée lexicale (par opposition à la portée dynamique). La portée courante est basée sur la structure du code et détermine les variables disponibles à des parties spécifiques du code. Cela signifie que la portée change lorsque le code saute entre les méthodes, les blocs et les classes – car ils peuvent tous avoir des variables locales différentes, par exemple.
1 2 3 4 5 6 |
def bar foo = 1 foo end bar # => 1 |
Dans cette méthode, nous créons une variable locale à l’intérieur d’une méthode et l’imprimons sur la console. La variable est en portée à l’intérieur de la méthode, car elle y est créée.
1 2 3 4 5 6 7 |
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object) |
Dans cet exemple, nous créons la variable à l’extérieur de la méthode. Lorsque nous appelons la variable à l’intérieur d’une méthode, nous obtenons une erreur, car la variable est hors de portée. Les variables locales ont une portée étroite, ce qui signifie qu’une méthode ne peut pas accéder à une variable en dehors d’elle-même, à moins qu’elle ne soit passée comme argument.
1 2 3 4 5 6 7 |
@foo = 1 def bar @foo end bar # => 1 |
Alors que les variables locales sont disponibles localement, les variables d’instance sont disponibles pour toutes les méthodes d’une instance de classe.
Scopes hérités et liaisons Proc
Comme nous l’avons vu dans les exemples précédents, le scope est basé sur l’emplacement dans le code. Une variable locale définie en dehors d’une méthode n’est pas en scope à l’intérieur de la méthode mais peut être rendue disponible en la transformant en variable d’instance. Les méthodes ne peuvent pas accéder aux variables locales définies en dehors d’elles car les méthodes ont leur propre scope, avec leurs propres bindings.
Les procs (y compris les blocs et les lambdas, par extension) sont différents. Chaque fois qu’un proc est instancié, une liaison est créée qui hérite des références aux variables locales dans le contexte où le bloc a été créé.
1 2 |
foo = 1 Proc.new { foo }.call # => 1 |
Dans cet exemple, nous définissons une variable nommée foo
à 1
. En interne, l’objet Proc créé sur la deuxième ligne crée une nouvelle liaison. Lors de l’appel du proc, nous pouvons demander la valeur de la variable.
Puisque le binding est créé lorsque le proc est initialisé, nous ne pouvons pas créer le proc avant de définir la variable, même si le bloc n’est appelé qu’après la définition de la variable.
1 2 3 |
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object) |
L’appel du proc produira un NameError
puisque la variable n’est pas définie dans les bindings du proc. Ainsi, toute variable à laquelle on accède dans une proc doit être définie avant que la proc ne soit créée ou passée comme argument.
1 2 3 4 |
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2 |
On peut cependant modifier la variable après qu’elle ait été définie dans le contexte principal puisque le binding de la proc en détient une référence au lieu de la copier.
1 2 3 |
foo = 1 Proc.new { foo = 2 }.call foo #=> 2 |
Dans cet exemple, nous pouvons voir que la variable foo
pointe vers le même objet lorsqu’elle est dans la proc et en dehors de celle-ci. Nous pouvons la mettre à jour à l’intérieur du proc pour que la variable à l’extérieur de celui-ci soit également mise à jour.
Bindings
Pour garder la trace de la portée actuelle, Ruby utilise des bindings, qui encapsulent le contexte d’exécution à chaque position dans le code. La méthode binding
renvoie un objet Binding
qui décrit les bindings à la position actuelle.
1 2 |
foo = 1 binding.local_variables # => |
L’objet binding possède une méthode nommée #local_variables
qui renvoie les noms de toutes les variables locales disponibles dans la portée actuelle.
1 2 |
foo = 1 binding.eval("foo") # => 1 |
Le code peut être évalué sur le binding en utilisant la méthode #eval
. L’exemple ci-dessus n’est pas très utile, car le simple fait d’appeler foo
aurait le même résultat. Cependant, comme un binding est un objet qui peut être transmis, il peut être utilisé pour des choses plus intéressantes. Regardons un exemple.
Un exemple de la vie réelle
Maintenant que nous avons appris les bindings dans la sécurité de notre garage, comme les sortir sur les pistes et jouer dans la neige. En dehors de l’utilisation interne des bindings par Ruby dans tout le langage, il existe certaines situations où les objets de binding sont utilisés explicitement. Un bon exemple est ERB – le système de template de Ruby.
1 2 3 4 5 6 7 8 9 10 |
require 'erb' x = 1 def y 2 end template = ERB.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`") template.result(binding) # => "x is 1, y() returns 2, self is `main`" |
Dans cet exemple, nous créons une variable nommée x
, une méthode appelée y
, et un template ERB qui référence les deux. Nous passons ensuite la liaison actuelle à ERB#result
, qui évalue les balises ERB dans le modèle et renvoie une chaîne avec les variables remplies.
Sous le capot, ERB utilise Binding#eval
pour évaluer le contenu de chaque balise ERB dans la portée de la liaison passée. Une implémentation simplifiée qui fonctionne pour l’exemple ci-dessus pourrait ressembler à ceci :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class DiyErb def initialize(template) @template = template end def result(binding) @template.gsub(/<%=(.+?)%>/) do binding.eval() end end end x = 1 def y 2 end template = DiyErb.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`") template.result(binding) # => "x is 1, y() returns 2, self is `main`" |
La classe DiyErb
prend une chaîne de caractères modèle à l’initialisation. Sa méthode #result
trouve toutes les balises ERB et les remplace par le résultat de l’évaluation de leur contenu. Pour ce faire, elle appelle Binding#eval
sur le binding passé, avec le contenu des balises ERB.
En passant le binding courant lors de l’appel de la méthode #result
, les appels eval
peuvent accéder aux variables définies en dehors de la méthode, et même en dehors de la classe, sans avoir à les passer explicitement.
Nous vous avons perdu dans les bois ?
Nous espérons que vous avez apprécié notre voyage de ski dans les bois. Nous avons approfondi les champs d’application et les fermetures, après les avoir survolés. Nous espérons ne pas vous avoir perdu dans les bois. N’hésitez pas à nous faire savoir si vous souhaitez en savoir plus sur les fixations, ou si vous avez un autre sujet Ruby dans lequel vous aimeriez vous plonger.
Merci de nous suivre et merci de donner un coup de pied à la neige sur vos fixations avant de les laisser au prochain développeur. Si vous aimez ces voyages magiques, vous pourriez vous abonner à Ruby Magic pour recevoir un e-mail lorsque nous publions un nouvel article environ une fois par mois.