Godt nytår, og velkommen tilbage til Ruby Magic! I denne vinteraflevering dykker vi ned i bindinger og scopes. Så tag dine ski på, og følg med os dybt ind i skoven.
Sidste gang kiggede vi på closures i Ruby ved at sammenligne blocks, procs og lambdas. Ud over forskellene mellem de tre typer berørte vi også, hvad der definerer en closure.
En closure er en førsteklasses funktion med et miljø. Miljøet er en mapping til de variabler, der eksisterede, da closuren blev oprettet. Closure beholder sin adgang til disse variabler, selv om de er defineret i et andet scope.
Vi har udforsket Rubys ækvivalent til førsteklassesfunktioner, men vi sprang bekvemt over miljøer. I denne episode vil vi se på, hvordan dette miljø fungerer for closures, klasser og klasseinstanser ved at undersøge, hvordan Ruby håndterer lexical scope gennem sine bindinger.
Lexical Scope
I programmering henviser scope til de bindinger, der er tilgængelige på en bestemt del af koden. En binding, eller navnebinding, binder et navn til en hukommelsesreference, ligesom en variabels navn til dens værdi. Omfanget definerer, hvad self
betyder, hvilke metoder der kan kaldes, og hvilke variabler der er tilgængelige.
Ruby bruger, ligesom de fleste moderne programmeringssprog, et statisk omfang, ofte kaldet lexikalt omfang (i modsætning til dynamisk omfang). Det aktuelle scope er baseret på kodens struktur og bestemmer de variabler, der er tilgængelige på bestemte dele af koden. Det betyder, at scope ændres, når koden hopper mellem metoder, blokke og klasser – da de f.eks. alle kan have forskellige lokale variabler.
1 2 3 4 5 6 |
def bar foo = 1 foo end bar # => 1 |
I denne metode opretter vi en lokal variabel inde i en metode og udskriver den til konsollen. Variablen er i scope inde i metoden, da den oprettes der.
1 2 3 4 5 6 7 |
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object) |
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object) |
I dette eksempel opretter vi variablen uden for metoden. Når vi kalder variablen inde i en metode, får vi en fejl, da variablen er uden for scope. Lokale variabler har et snævert scope, hvilket betyder, at en metode ikke kan få adgang til en variabel uden for sig selv, medmindre den overføres som et argument.
1 2 3 4 5 6 7 |
@foo = 1 def bar @foo end bar # => 1 |
Mens lokale variabler er tilgængelige lokalt, er instansvariabler tilgængelige for alle metoder i en klasseinstans.
Inherited Scopes and Proc Bindings
Som vi har set i de foregående eksempler, er scope baseret på placeringen i koden. En lokal variabel, der er defineret uden for en metode, er ikke i scope inden for metoden, men kan gøres tilgængelig ved at gøre den til en instansvariabel. Metoder kan ikke få adgang til lokale variabler, der er defineret uden for dem, fordi metoder har deres eget scope med deres egne bindinger.
Procs (herunder blokke og lambda’er i forlængelse heraf) er anderledes. Når en proc instantieres, oprettes der en binding, som arver referencer til de lokale variabler i den kontekst, hvor blokken blev oprettet.
1 2 |
foo = 1 Proc.new { foo }.call # => 1 |
I dette eksempel sætter vi en variabel ved navn foo
til 1
. Internt opretter det Proc-objekt, der oprettes på den anden linje, en ny binding. Når vi kalder proc’en, kan vi bede om værdien af variablen.
Da bindingen oprettes, når proc’en initialiseres, kan vi ikke oprette proc’en, før vi har defineret variablen, selv om blokken ikke kaldes før efter, at variablen er defineret.
1 2 3 |
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object) |
Afkald af proc’en vil give en NameError
, da variablen ikke er defineret i proc’ens bindinger. Derfor skal alle variabler, der tilgås i en proc, defineres, før proc’en oprettes eller overgives som argument.
1 2 3 4 |
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2 |
Vi kan dog ændre variablen, efter at den er blevet defineret i hovedkonteksten, da proc’ens binding holder en reference til den i stedet for at kopiere den.
1 2 3 |
foo = 1 Proc.new { foo = 2 }.call foo #=> 2 |
I dette eksempel kan vi se, at foo
-variablen peger på det samme objekt, når den er i proc’en, som uden for den. Vi kan opdatere den inde i proc’en for at få variablen uden for den også opdateret.
Bindinger
For at holde styr på det aktuelle scope bruger Ruby bindinger, som indkapsler eksekveringskonteksten ved hver position i koden. Metoden binding
returnerer et Binding
-objekt, som beskriver bindingerne på den aktuelle position.
1 2 |
foo = 1 binding.local_variables # => |
Binding-objektet har en metode ved navn #local_variables
, som returnerer navnene på alle de lokale variabler, der er tilgængelige i det aktuelle scope.
1 2 |
foo = 1 binding.eval("foo") # => 1 |
Kode kan evalueres på bindingen ved hjælp af #eval
-metoden. Eksemplet ovenfor er ikke særlig nyttigt, da det blot at kalde foo
ville give det samme resultat. Da en binding imidlertid er et objekt, der kan sendes rundt, kan den bruges til nogle mere interessante ting. Lad os se på et eksempel.
Et eksempel fra det virkelige liv
Nu har vi lært om bindinger i garagens trygge rammer, så tag dem gerne med ud på pisterne og leg lidt rundt i sneen. Udover Rubys interne brug af bindinger i hele sproget, er der nogle situationer, hvor bindingsobjekter bruges eksplicit. Et godt eksempel er ERB-Rubys templating-system.
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`" |
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`" |
I dette eksempel opretter vi en variabel ved navn x
, en metode ved navn y
og en ERB-skabelon, der refererer til begge. Derefter overdrager vi den aktuelle binding til ERB#result
, som evaluerer ERB-taggene i skabelonen og returnerer en streng med variablerne udfyldt.
Under motorhjelmen bruger ERB Binding#eval
til at evaluere indholdet af hvert ERB-tag inden for rammerne af den overdragne binding. En forenklet implementering, der fungerer for ovenstående eksempel, kunne se således ud:
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`" |
Klassen DiyErb
tager en skabelonstreng ved initialiseringen. Dens #result
metode finder alle ERB-tags og erstatter dem med resultatet af evalueringen af deres indhold. For at gøre det kalder den Binding#eval
på den overgivne binding med indholdet af ERB-tagsene.
Ved at overgive den aktuelle binding, når den kalder #result
-metoden, kan eval
-kaldet få adgang til de variabler, der er defineret uden for metoden og endda uden for klassen, uden at skulle overgive dem eksplicit.
Faldt vi fra dig i skoven?
Vi håber, du nød vores skitur i skoven. Vi gik dybere ind i scopes og lukninger, efter at vi havde gloset over dem. Vi håber ikke, at vi har mistet dig i skoven. Lad os vide, hvis du gerne vil lære mere om bindinger, eller hvis du har et andet Ruby-emne, du gerne vil dykke ned i.
Tak for at følge os, og spar venligst sneen af dine bindinger, før du efterlader dem til den næste udvikler. Hvis du kan lide disse magiske ture, vil du måske gerne abonnere på Ruby Magic, så du får en e-mail, når vi udgiver en ny artikel ca. en gang om måneden.