Gelukkig nieuwjaar, en welkom terug bij Ruby Magic! In deze winteraflevering duiken we in bindingen en scopes. Dus trek je ski’s aan en volg ons tot diep in het bos.
De vorige keer hebben we gekeken naar closures in Ruby door blokken, procs en lambdas te vergelijken. Afgezien van de verschillen tussen de drie typen, raakten we aan wat een closure definieert.
Een closure is een eersteklas functie met een omgeving. De omgeving is een mapping naar de variabelen die bestonden toen de closure werd gemaakt. De closure behoudt zijn toegang tot deze variabelen, zelfs als ze in een ander bereik zijn gedefinieerd.
We hebben Ruby’s equivalent van eersteklas functies verkend, maar we hebben handig omgevingen overgeslagen. In deze aflevering zullen we kijken hoe die omgeving werkt voor closures, klassen en klasse-instanties door te onderzoeken hoe Ruby lexical scope afhandelt via zijn bindingen.
Lexical Scope
In programmeren verwijst scope naar de bindingen die beschikbaar zijn op een specifiek deel van de code. Een binding, of naam binding, bindt een naam aan een geheugenverwijzing, zoals de naam van een variabele aan zijn waarde. De scope definieert wat self
betekent, de methoden die kunnen worden aangeroepen, en de variabelen die beschikbaar zijn.
Ruby, net als de meeste moderne programmeertalen, maakt gebruik van een statische scope, vaak lexical scope genoemd (in tegenstelling tot dynamische scope). De huidige scope is gebaseerd op de structuur van de code en bepaalt de variabelen die beschikbaar zijn op specifieke delen van de code. Dit betekent dat de scope verandert wanneer code tussen methodes, blokken en klassen springt-omdat ze allemaal verschillende lokale variabelen kunnen hebben, bijvoorbeeld.
1 2 3 4 5 6 |
def bar foo = 1 foo end bar # => 1 |
In deze methode maken we een lokale variabele binnen een methode en drukken deze af naar de console. De variabele is in scope binnen de methode, aangezien deze daar is aangemaakt.
1 2 3 4 5 6 7 |
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object) |
In dit voorbeeld maken we de variabele buiten de methode aan. Wanneer we de variabele binnen een methode oproepen, krijgen we een foutmelding, omdat de variabele buiten scope is. Lokale variabelen hebben een beperkte reikwijdte, wat betekent dat een methode geen toegang heeft tot een variabele buiten zichzelf, tenzij deze als argument wordt doorgegeven.
1 2 3 4 5 6 7 |
@foo = 1 def bar @foo end bar # => 1 |
Lokale variabelen zijn lokaal beschikbaar, maar instance-variabelen zijn beschikbaar voor alle methoden van een klasse-instantie.
Inherited Scopes and Proc Bindings
Zoals we in de vorige voorbeelden hebben gezien, is de scope gebaseerd op de plaats in de code. Een lokale variabele gedefinieerd buiten een methode is niet in scope binnen de methode, maar kan beschikbaar worden gemaakt door er een instantie variabele van te maken. Methoden hebben geen toegang tot lokale variabelen die buiten hen zijn gedefinieerd, omdat methoden hun eigen scope hebben, met hun eigen bindings.
Procs (inclusief blokken en lambda’s, bij uitbreiding) zijn anders. Telkens wanneer een proc wordt geïnstantieerd, wordt een binding gemaakt die verwijzingen erft naar de lokale variabelen in de context waarin het blok is gemaakt.
1 2 |
foo = 1 Proc.new { foo }.call # => 1 |
In dit voorbeeld stellen we een variabele met de naam foo
in op 1
. Intern maakt het Proc-object dat op de tweede regel is gemaakt een nieuwe binding. Wanneer we de proc aanroepen, kunnen we om de waarde van de variabele vragen.
Omdat de binding wordt aangemaakt wanneer de proc wordt geïnitialiseerd, kunnen we de proc niet aanmaken voordat we de variabele hebben gedefinieerd, zelfs niet als het blok pas wordt aangeroepen nadat de variabele is gedefinieerd.
1 2 3 |
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object) |
Het aanroepen van de proc zal een NameError
opleveren omdat de variabele niet is gedefinieerd in de bindingen van de proc. Variabelen die in een proc worden aangeroepen moeten dus worden gedefinieerd voordat de proc wordt aangemaakt of als argument wordt doorgegeven.
1 2 3 4 |
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2 |
We kunnen de variabele echter wijzigen nadat deze in de hoofdcontext is gedefinieerd, aangezien de binding van de proc een verwijzing naar de variabele bevat in plaats van deze te kopiëren.
1 2 3 |
foo = 1 Proc.new { foo = 2 }.call foo #=> 2 |
In dit voorbeeld kunnen we zien dat de variabele foo
naar hetzelfde object wijst als het zich in de proc bevindt of erbuiten. We kunnen het binnen de proc bijwerken om de variabele erbuiten ook te laten bijwerken.
Bindingen
Om de huidige scope bij te houden, gebruikt Ruby bindingen, die de uitvoeringscontext inkapselen op elke positie in de code. De binding
methode retourneert een Binding
object dat de bindingen op de huidige positie beschrijft.
1 2 |
foo = 1 binding.local_variables # => |
Het binding object heeft een methode genaamd #local_variables
die de namen retourneert van alle lokale variabelen die beschikbaar zijn in het huidige bereik.
1 2 |
foo = 1 binding.eval("foo") # => 1 |
Code kan worden geëvalueerd op de binding met behulp van de methode #eval
. Het bovenstaande voorbeeld is niet erg nuttig, aangezien het simpelweg oproepen van foo
hetzelfde resultaat zou opleveren. Maar omdat een binding een object is dat kan worden doorgegeven, kan het voor interessantere dingen worden gebruikt. Laten we eens kijken naar een voorbeeld.
Een voorbeeld uit het echte leven
Nu we geleerd hebben over bindingen in de veiligheid van onze garage, willen we ze graag meenemen naar de piste en wat spelen in de sneeuw. Afgezien van Ruby’s interne gebruik van bindingen door de taal heen, zijn er enkele situaties waarin bindende objecten expliciet worden gebruikt. Een goed voorbeeld is ERB-Ruby’s templating systeem.
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`" |
In dit voorbeeld maken we een variabele aan met de naam x
, een methode met de naam y
, en een ERB template dat naar beide verwijst. Vervolgens geven we de huidige binding door aan ERB#result
, die de ERB tags in het template evalueert en een string teruggeeft met de ingevulde variabelen.
Onder de motorkap gebruikt ERB Binding#eval
om de inhoud van elke ERB tag te evalueren in het bereik van de doorgegeven binding. Een vereenvoudigde implementatie die werkt voor bovenstaand voorbeeld zou er als volgt uit kunnen zien:
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`" |
De DiyErb
klasse neemt bij initialisatie een template string aan. De methode #result
vindt alle ERB-tags en vervangt deze door het resultaat van de evaluatie van de inhoud. Om dat te doen, roept hij Binding#eval
aan op de doorgegeven binding, met de inhoud van de ERB tags.
Door de huidige binding door te geven bij het aanroepen van de #result
methode, kunnen de eval
aanroepen toegang krijgen tot de variabelen die buiten de methode zijn gedefinieerd, en zelfs buiten de klasse, zonder dat ze expliciet hoeven te worden doorgegeven.
Zijn we je kwijtgeraakt in het bos?
We hopen dat je genoten hebt van onze skitocht in het bos. We zijn dieper ingegaan op scopes en sluitingen, nadat we ze hadden verdoezeld. We hopen dat we u niet kwijt zijn geraakt in het bos. Laat het ons weten als je meer wilt leren over bindingen, of een ander Ruby-onderwerp hebt waar je in wilt duiken.
Bedankt voor het volgen van ons en schop alsjeblieft de sneeuw van je bindingen voordat je ze achterlaat voor de volgende ontwikkelaar. Als je deze magische uitstapjes leuk vindt, wil je je misschien abonneren op Ruby Magic om een e-mail te ontvangen wanneer we ongeveer een keer per maand een nieuw artikel publiceren.