Kaj je zaprtje v računalništvu (closure): definicija in primeri
V računalništvu je zaključek (angleško closure) funkcija, ki ima svoje okolje — to pomeni, da poleg telesa funkcije shrani tudi referenco na spremenljivke iz zunanjega obsega (vsaj ena take spremenljivke je vezana/prosta glede na to funkcijo). Okolje zaključka ohranja te vezane spremenljivke v pomnilniku in jih omogoča uporabljati tudi potem, ko je izvršitev zunanje funkcije že končana.
Kako deluje zaprtje
Zaprtje nastane, ko znotraj funkcije ustvarimo drugo (notranjo) funkcijo, ta notranja funkcija pa sklicuje spremenljivke iz zunanjega obsega. Ko zunanja funkcija vrne notranjo funkcijo, se običajno pričakuje, da bodo zunanje lokalne spremenljivke izginile; pri zaprtju pa te spremenljivke ostanejo dostopne preko notranje funkcije, ker jih okolje zaključka ohrani.
- Vezane (bound) spremenljivke: imena, ki jih notranja funkcija uporablja in so definirana v zunanjem obsegu.
- Proste (free) spremenljivke: spremenljivke, ki niso lokalne notranji funkciji, a so v njenem dosegu zaradi okolja.
- Levkonsko (lexical) skopanje: večina jezikov z zaprtji uporablja leksikalno skopanje — okolje se določi glede na strukturo kode v času pisanja (napram dinamičnemu skopanju).
Kratek zgodovinski kontekst
Peter J. Landin je leta 1964 to zamisel poimenoval Closure. Programski jezik Scheme je po letu 1975 pomembno prispeval k širitvi uporabe zaprtij. Danes številni sodobni programski jeziki (JavaScript, Python, Ruby, Lisp, Haskell, in mnogi drugi) podpirajo zaključke.
Primeri
Tipičen primer zaprtja v JavaScriptu (preprosti števec):
function makeCounter() { let count = 0; return function() { count++; return count; }; } const c = makeCounter(); console.log(c()); // 1 console.log(c()); // 2
V tem primeru notranja anonimna funkcija ohranja dostop do spremenljivke count
, tudi ko je makeCounter
že vrnila rezultat.
Podoben primer v Pythonu z uporabo nonlocal
(za spreminjanje zunanje spremenljivke):
def make_counter(): count = 0 def inc(): nonlocal count count += 1 return count return inc c = make_counter() print(c()) # 1 print(c()) # 2
Pogoste napačne predstave
- Anonomne funkcije niso vedno zaprtja: Anonimne funkcije so funkcije brez imena, vendar so zaprtja le, če resnično ujemajo okolje (tj. uporabljajo spremenljivke iz zunanjih obsegov). Anonimna funkcija brez lastnega okolja ni zaprtje.
- Poimenovana funkcija je lahko zaprtje: zaprtje ni sinonim za anonimnost; tudi poimenovana funkcija lahko tvori zaprtje, če ohranja zunanji kontekst.
- Zajemanje po vrednosti ali referenci: načelo zajemanja je odvisno od jezika — nekateri jeziki (npr. JavaScript) zajemajo referenco na spremenljivko, zato se spremembe vidijo v vseh funkcijah, ki jo uporabljajo; drugi jeziki lahko zagotovijo kopijo vrednosti.
Uporabnost in kompromisi
- Uporabe: enkapsulacija (skriti podatki), tvorba generatorjev in števcem, funkcije višjega reda, delne aplikacije, dekoratorji, asinhroni povratni klici, itd.
- Kompromisi: zaprtja ohranjajo spremenljivke v pomnilniku, kar lahko vpliva na porabo pomnilnika; nepravilna raba lahko povzroči težave (npr. nepričakovane vrednosti pri zanki, če se zajemajo spremenljivke, ki se spreminjajo).
Zaključek
Zaprtja so močno orodje v programiranju, ki omogočajo, da funkcije prenesejo in ohranijo svoj kontekst. Razumevanje razlik med anonimnimi funkcijami in zaprtji, med leksikalnim in dinamičnim skopanjem ter specifičnim vedenjem posameznega programskega jezika je ključnega pomena za pravilno in učinkovito uporabo zaprtij.
Zapiranje in prvovrstne funkcije
Vrednosti so lahko številke ali druge vrste podatkov, na primer črke, ali podatkovne strukture, sestavljene iz enostavnejših delov. V pravilih programskega jezika so vrednosti prvega razreda vrednosti, ki jih lahko podamo funkcijam, jih funkcije vrnejo in se vežejo na ime spremenljivke. Funkcije, ki sprejemajo ali vračajo druge funkcije, se imenujejo funkcije višjega reda. Večina jezikov, ki imajo funkcije kot vrednosti prvega razreda, ima tudi funkcije višjega reda in zapore.
Oglejte si na primer naslednjo funkcijo sheme:
V tem primeru je izraz lambda (lambda (book) (>= (book-sales book) threshold))
del funkcije best-selling-books
. Ko se funkcija zažene, mora Scheme ustvariti vrednost izraza lambda. To stori tako, da ustvari zaključek s kodo za lambdo in referenco na spremenljivko prag
, ki je prosta spremenljivka znotraj lambde. (Prosta spremenljivka je ime, ki ni vezano na vrednost.)
Funkcija filtra
nato izvede zaključek za vsako knjigo na seznamu in izbere knjige, ki jih bo vrnila. Ker ima zaključek sam referenco na prag,
lahko zaključek uporabi to vrednost vsakič, ko filter
zažene zaključek. Sama funkcija filter
je lahko zapisana v popolnoma ločeni datoteki.
Isti primer je prepisan v jeziku ECMAScript (JavaScript), še enem priljubljenem jeziku s podporo za zapiranje:
ECMAScript namesto lambda
uporablja besedo function, namesto
funkcije filter
pa metodo Array.filter,
sicer pa koda izvaja isto stvar na enak način.
Funkcija lahko ustvari zaključek in ga vrne. Naslednji primer je funkcija, ki vrne funkcijo.
V shemi:
V ECMAScriptu:
Okolje zapiranja ohrani vezani spremenljivki f
in dx
tudi po vrnitvi obkrožujoče funkcije (derivata).
V jezikih brez zaprtja bi se ti vrednosti po vrnitvi obkrožujoče funkcije izgubili. V jezikih z zaprtji je treba vezano spremenljivko hraniti v pomnilniku, dokler jo ima katero koli zaprtje.
Zaprtja ni treba oblikovati z anonimno funkcijo. Programski jezik Python, na primer, ima omejeno podporo za anonimne funkcije, vendar ima zaključke. Na primer, zgornji primer ECMAScript bi lahko v jeziku Python izvedli na naslednji način:
V tem primeru je funkcija z imenom gradient skupaj s spremenljivkama f in dx zaprta. Funkcija z imenom derivativ vrne to zaprtje. V tem primeru bi delovala tudi anonimna funkcija.
Python mora namesto tega pogosto uporabljati poimenovane funkcije, saj lahko njegovi lambda izrazi vsebujejo le druge izraze (kodo, ki vrača vrednost) in ne izjav (kodo, ki ima učinke, vendar nima vrednosti). V drugih jezikih, kot je Scheme, pa vsa koda vrača vrednost; v Scheme je vse izraz.
Uporaba zaključkov
Zapirala se lahko uporabljajo za številne namene:
- Oblikovalci programskih knjižnic lahko uporabnikom omogočijo, da prilagodijo obnašanje tako, da pomembnim funkcijam kot argumente posredujejo zapore. Na primer, funkcija, ki razvršča vrednosti, lahko sprejme argument zapore, ki primerja vrednosti, ki jih je treba razvrstiti v skladu z uporabniško opredeljenim merilom.
- Ker zaključki odložijo vrednotenje, tj. ne naredijo ničesar, dokler jih ne pokličemo, jih lahko uporabimo za definiranje nadzornih struktur. Vse standardne kontrolne strukture Smalltalka, vključno z vejami (if/then/else) in zankami (while in for), so na primer definirane s predmeti, katerih metode sprejemajo zapore. Uporabniki lahko enostavno definirajo tudi svoje lastne kontrolne strukture.
- Izdelati je mogoče več funkcij, ki so si blizu v istem okolju, kar jim omogoča zasebno komunikacijo s spreminjanjem tega okolja (v jezikih, ki omogočajo dodeljevanje).
V shemi
- Zapore lahko uporabite za izvajanje objektnih sistemov.
Opomba: Nekateri govorci vsako podatkovno strukturo, ki povezuje leksikalno okolje, imenujejo zaključek, vendar se ta izraz običajno nanaša predvsem na funkcije.
Vprašanja in odgovori
V: Kaj je zaključek v računalništvu?
O: Zaključek je funkcija, ki ima lastno okolje.
V: Kaj vsebuje okolje zaključka?
O: Okolje zaključka vsebuje vsaj eno vezano spremenljivko.
V: Kdo je dal ideji o zaprtju ime?
O: Peter J. Landin je leta 1964 poimenoval zamisel o zaprtju.
V: V katerem programskem jeziku so bile zapore priljubljene po letu 1975?
O: Programski jezik Scheme je po letu 1975 populariziral zapore.
V: Ali so anonimne funkcije in zaprtja ista stvar?
O: Anonimne funkcije se včasih napačno imenujejo zapore, vendar vse anonimne funkcije niso zapore.
V: Kaj naredi anonimno funkcijo za zaključek?
O: Anonimna funkcija je zaključek, če ima lastno okolje z vsaj eno vezano spremenljivko.
V: Ali je poimenovana zapiralna funkcija anonimna?
O: Ne, poimenovana zapiralna funkcija ni anonimna.