Přeskočit na obsah

Uzávěr (programování)

Z Wikipedie, otevřené encyklopedie

Uzávěr, lexikální uzávěr nebo funkční uzávěr je technika implementace vazby jmen s lexikálním oborem platnosti[pozn. 1] v programovacích jazycích, v nichž jsou funkce objekty první kategorie. Z operačního hlediska je uzávěr záznam, který obsahuje nejen funkci,[pozn. 2] ale také prostředí, ve kterém byla funkce definována.[1] Prostředí zobrazuje každou volnou proměnnou funkce (proměnné, které se ve funkci používají, ale jsou definovány v obklopujícím oboru platnosti) na hodnotu nebo referenci, na kterou bylo jméno vázané v okamžiku, kdy byl uzávěr vytvořen.[pozn. 3] Na rozdíl od běžné funkce uzávěr umožňuje funkci přistupovat k zachyceným proměnným prostřednictvím kopií jejich hodnot nebo referencí vytvořených pro uzávěr, když se uzávěr vyvolá mimo obor jejich platnosti.

Historie a etymologie

[editovat | editovat zdroj]

Koncept uzávěrů byl vyvinut v 60. letech 20. století pro mechanické vyhodnocování výrazů v lambda kalkulu a poprvé byl plně implementován v roce 1970 v programovacím jazyce PAL pro podporu funkcí první kategorie s lexikálním oborem platnosti.[2]

Termín uzávěr definoval v roce 1964 Peter Landin jako entitu, která má složku prostředí a složku řízení, kterou používal jeho SECD stroj pro vyhodnocování výrazů.[3] Joel Moses uvádí, že termín uzávěr zavedl Landin pro označení anonymní funkce (lambda výrazu) s otevřenými vazbami (volnými proměnnými), které byly uzavřeny (nebo vázány) lexikálním prostředím, čímž vznikl uzavřený výraz neboli uzávěr.[4][5] Toto použití následně převzali Gerald Jay Sussman a Guy L. Steele Jr., když v roce 1975 definovali jazyk Scheme,[6] variantu Lispu s lexikálním oborem platnosti, a následně se rozšířilo.

Termín „uzávěr“ použil Sussman s Abelsonem v 80. letech 20. století také v jiném významu: jako vlastnost operátoru, který přidává data k datové struktuře, a je také schopen přidávat vnořené datové struktury. Toto použití termínu „uzávěr“ je převzato z matematiky, zatímco předchozí použití z matematické informatiky. Autoři později označili toto překrývání v terminologii za nešťastné.[7]

Anonymní funkce

[editovat | editovat zdroj]
Podrobnější informace naleznete v článku Anonymní funkce.

Termín uzávěr se často používá jako synonymum pro anonymní funkce, i když přísně řečeno, anonymní funkce je funkční literál bez jména, zatímco uzávěr je instance (hodnota) funkce, jejíž nelokální proměnné jsou vázány buď k hodnotám nebo paměťovým lokacím (podle jazyka – viz část lexikální prostředí níže).

Například následující program v Pythonu:

def f(x):
    def g(y):
        return x + y
    return g  # Vrací uzávěr.

def h(x):
    return lambda y: x + y  # Vrací uzávěr.

# Přiřazení specifického uzávěru do proměnných:
a = f(1)
b = h(1)

# Použití uzávěrů z proměnných:
assert a(5) == 6
assert b(5) == 6

# Použití uzávěrů bez jejich přiřazení do proměnných:
assert f(1)(5) == 6  # f(1) je uzávěr.
assert h(1)(5) == 6  # h(1) je uzávěr.

hodnoty a a b jsou uzávěry, v obou případech vytvořené vrácením z vnořené funkce s volnou proměnnou z obklopující funkce, takže volná proměnná se váže na hodnotu parametru x obklopující funkce. Uzávěry v a a b jsou funkčně identické. Jediným rozdílem v implementaci je, že v prvním případě byla použita vnořená funkce se jménem g, zatímco v druhém případě anonymní vnořená funkce (s použitím klíčového slova lambda jazyka Python pro vytvoření anonymní funkce). Původní jméno, pokud existuje, použité při její definici je irelevantní.

Uzávěr je hodnota jako každá jiná. Nemusí být přiřazena do proměnné, ale může být použita přímo, jak je vidět na posledních dvou řádcích příkladu. Toto použití může být považováno za „anonymní uzávěr“.

Vnořené definice funkcí samy o sobě nejsou uzávěry: mají volnou proměnnou, která ještě není vázaná. Teprve po vyhodnocení obklopující funkce s hodnotou parametru je volná proměnná vnořené funkce vázána, čímž vzniká uzávěr, který je pak vrácen z obklopující funkce.

Konečně se uzávěr od funkce s volnými proměnnými liší pouze tehdy, když je mimo obor platnosti nelokálních proměnných, jinak se definiční prostředí a prostředí provádění shodují a nelze je ničím rozlišit (statickou a dynamickou vazbu nelze rozlišit, protože jména se resolvují na stejné hodnoty). Například v níže uvedeném programu se funkce s volnou proměnnou x (vázanou na nelokální proměnnou x s globálním oborem platnosti) provádějí ve stejném prostředí, kde je x definováno, takže není podstatné, zda se jedná o skutečné uzávěry:

x = 1
nums = [1, 2, 3]

def f(y):
    return x + y

map(f, nums)
map(lambda y: x + y, nums)

Toho se nejčastěji dosáhne návratem funkce, protože funkce musí bát definována v rámci oboru platnosti nelokálních proměnných, v takovém případě bude její vlastní obor platnosti obvykle menší.

Toho lze také dosáhnout zakrýváním proměnných (které zmenšuje obor platnosti nelokální proměnné), i když je to v praxi méně obvyklé, protože je to méně užitečné a zakrývání se nedoporučuje. V následujícím příkladě lze f považovat za uzávěr, protože x v těle funkce f je vázáno na proměnnou x v globálním jmenném prostoru, ne na x lokální v g:

x = 0

def f(y):
    return x + y

def g(z):
    x = 1  # lokální x zakrývá globální x
    return f(z)

g(1)  # g se vyčísluje na 1, ne na 2

Uzávěry se používají v jazycích, kde funkce jsou objekty první kategorie, v nichž mohou být funkce vracené jako výsledky funkcí vyššího řádu nebo předávané jako argumenty při volání jiných funkcí; pokud funkce s volnými proměnnými jsou první kategorie, pak vrácení funkce vytváří uzávěr. K těmto jazykům patří jazyky pro funkcionální programování, např. Lisp a ML, a mnoho moderních multiparadigmatických jazyků, např. Julia, Python nebo Rust. Uzávěry se také často používají pro callbacky, obzvláště pro funkce pro obsluhu události, např. v JavaScriptu, kde se používají pro interakce s dynamickými webovými stránkami.

Uzávěry se mohou také používat ve stylu continuation-passing pro skrytí stavu. Konstrukty, jako jsou objekty a řídicí struktury lze tedy implementovat pomocí uzávěrů. V některých jazycích může uzávěr vzniknout, když je funkce definována uvnitř jiné funkce, a vnitřní funkce se odkazuje na lokální proměnné vnější funkce. V době běhu, kdy se provádí vnější funkce, se vytvoří uzávěr, sestávající z kódu vnitřní funkce a odkazů (upvalues) na všechny proměnné vnější funkce, které uzávěr vyžaduje.

Funkce první kategorie

[editovat | editovat zdroj]

Uzávěry se obvykle vyskytují v jazycích s funkcemi první kategorie, jinými slovy v takových jazycích, kde je dovoleno předávat funkce jako argumenty, návratové hodnoty z volání funkcí, vazby na jména proměnných, atd., stejně jako jednodušší typy, např. řetězce a celá čísla. Například uvažujme následující funkci v jazyce Scheme:

; Vrátí seznam všech knih, jichž se prodalo alespoň threshold kopií.
(define (best-selling-books threshold)
  (filter
    (lambda (book)
      (>= (book-sales book) threshold))
    book-list))

Lambda výraz (lambda (book) (>= (book-sales book) threshold)) se v tomto příkladě objevuje ve funkci best-selling-books. Když se lambda výraz vyčísluje, Scheme vytvoří uzávěr sestávající z kódu pro lambda výraz a odkazu na proměnnou threshold, která je volnou proměnnou v lambda výrazu.

Uzávěr je pak předán funkci filter, která jej volá opakovaně pro určení, jaké knihy mají být přidány k výslednému seznamu a jaké mají být ignorovány. Protože uzávěr má odkaz na threshold, může používat tuto proměnnou pokaždé, když jej filter zavolá. Funkce filter může být definována v samostatném souboru.

Zde je stejný příklad přepsaný v JavaScriptu, jiném oblíbeném jazyce, který má uzávěry:

// Vrátí seznam všech knih, jichž se prodalo nejméně 'threshold' kopií.
function bestSellingBooks(threshold) {
  return bookList.filter(book => book.sales >= threshold);
}

Operátor šipka => se používá pro definici šipkového funkčního výrazu, a metoda Pole.filtr[8] místo globální filtr funkce, ale jinak struktura a vliv kódu jsou stejný.

Funkce může vytvořit uzávěr a vrátit jej, jako v tomto příkladě:

// Vrátí funkci, které aproximuje derivaci funkce f
// pomocí intervalu dx, který by měl být dostatečně malý.
function derivative(f, dx) {
  return x => (f(x + dx) - f(x)) / dx;
}

Protože uzávěr v tomto případě přežívá provedení funkce, která jej vytvořila, proměnné f a dx existují i poté, co funkce derivative skončila, i když provádění programu opustilo jejich obor platnosti a už nejsou viditelné. V jazycích bez uzávěrů odpovídá životnost automatické lokální proměnné době provádění zásobníkového rámce, ve kterém je proměnná deklarována. V jazycích s uzávěry musí proměnné existovat tak dlouho, dokud se na ně odkazují nějaké existující uzávěry. Toto chování se obvykle implementuje použitím nějaké formy garbage kolektoru.

Reprezentace stavu

[editovat | editovat zdroj]

Uzávěr lze použít pro spojení funkce se sadou „soukromých“ proměnných, které přetrvávají po několik vyvolání funkce. obor platnosti proměnné zahrnuje pouze uzavřenou funkci, takže k ní nelze přistupovat z jiného programového kódu. Tyto jsou analogický na soukromý proměnné v Objektově orientované programování, a ve skutečnosti jsou uzávěry podobné stavovým funkčním objektům (nebo funktorům) s jednou metodou (operátorem) volání.

Ve stavových jazycích lze tedy uzávěry používat pro implementaci vzorů pro reprezentaci stavu a skrývání informací, protože upvalues uzávěru (jeho uzavřených proměnných) mají neomezený rozsah, takže hodnota stanovená při jedno vyvolání zůstává dostupná v dalším. Takto používané uzávěry již nemají referenční průhlednost, a nejsou tedy již čistými funkcemi; přesto se však často používají v nečistých funkcionálních jazycích, jako je Scheme.

Jiná použití

[editovat | editovat zdroj]

Uzávěry mají mnoho použití:

  • Protože uzávěry odkládají vyhodnocování, tj. „nedělají nic“, dokud nejsou vyvolány, lze je použít k definování řídicích struktur. Například všechny standardní řídicí struktury jazyka Smalltalk, včetně větvení (if/then/else) a smyček (while a for), jsou definovány pomocí objektů, jejichž metody akceptují uzávěry. Uživatelé mohou snadno definovat i své vlastní řídicí struktury.
  • V jazycích, které implementují přiřazování, lze vytvořit více funkcí se stejným uzávěrem, což jim umožňuje soukromou komunikaci pomocí změn tohoto prostředí. V jazyce Scheme:
(define foo #f)
(define bar #f)

(let ((secret-message "none"))
  (set! foo (lambda (msg) (set! secret-message msg)))
  (set! bar (lambda () secret-message)))

(display (bar)) ; vypíše "none"
(newline)
(foo "meet me by the docks at midnight")
(display (bar)) ; vypíše "meet me by the docks at midnight"
  • Uzávěry mohou být používány pro implementaci objektových systémů.[9]

Poznámka: Někteří autoři nazývají uzávěr jakoukoli datovou strukturu, které váže lexikální prostředí, ale termín se obvykle používá pouze pro funkce.

Implementace a teorie

[editovat | editovat zdroj]

Uzávěry se obvykle implementují speciální datovou strukturou, která obsahuje ukazatel na kód funkce a reprezentaci lexikálního prostředí funkce (tj. množinu dostupných proměnných) v okamžiku, kdy byl uzávěr vytvořen. Odkazující prostředí váže nelokální jména na odpovídající proměnné v lexikální prostředí v době vytvoření uzávěru a navíc rozšiřuje jejich životnost alespoň na dobu životnosti uzávěru. Při pozdějším zadaní uzávěru, případně s jiným lexikálním prostředím, se funkce provede s nelokálními proměnnými, které se odkazují na proměnné zachycené uzávěrem, nikoli s aktuálním prostředím.

V jazycích, které alokují všechny automatické proměnné na lineárním zásobníku, je implementace plných uzávěrů obtížná. V takových jazycích jsou automatické lokální proměnné dealokovány při ukončení funkce. Uzávěr však vyžaduje, aby volné proměnné, na které se v uzávěru odkazuje, existovaly i po ukončení obklopující funkce. Tyto proměnné proto musí být alokované tak, aby přetrvaly, dokud nebudou potřeba, obvykle díky přidělování paměti na haldě nikoli na zásobníku, a jejich životnost musí být řízena tak, aby byly zachovány, dokud se budou používat všechny uzávěry, které se na ně odkazují.

To vysvětluje, proč jazyky, které nativně podporují uzávěry, obvykle používají také garbage kolektor. Alternativou je ruční správa paměti nelokálních proměnných (jejich explicitní alokace na haldě a uvolnění po ukončení) nebo, pokud se používá zásobník, aby jazyk akceptoval, že určité případy použití způsobí nedefinované chování, kvůli dangling ukazatelům na uvolněné proměnnéi, jako je tomu v lambda výrazech v C++11[10] nebo vnořený funkcí v GNU C.[11] Problém funarg (problém „argumentů funkce“) popisuje obtíže při implementaci funkcí jako objektů první kategorie v programovacích jazycích založených na zásobníku, jako je např. C nebo C++. Podobně se v jazyce D verze 1 se předpokládá, že programátor ví, co dělat s delegáty a automatickými lokálními proměnnými, protože jejich reference budou neplatné po návratu z oboru platnosti, v němž jsou definovány, (automatické lokální proměnné jsou na zásobníku) – i to stále umožňuje mnoho užitečných funkčních vzorů, ale pro složité případy je nezbytná explicitní správa haldy pro proměnné. Jazyk D verze 2 to vyřešil detekcí, jaké proměnné musí být umístěny na haldě, a automatickou alokací. Protože jazyk D používá v obou verzích garbage kolektor, není potřeba sledovat použití proměnných při jejich předávání.

Ve striktně funkcionálních jazycích s immutable daty (například v jazyce Erlang), je velmi snadné implementovat automatickou správu paměti (garbage collecor), protože nemohou existovat cykly v referencích na proměnné. V Erlangu jsou například všechny argumenty a proměnné alokované na haldě, ale reference na ně jsou navíc uložené na zásobníku. Po návratu z funkce jsou reference stále platné. Čištění haldy se provádí inkrementálním garbage kolektorem.

V jazyce ML mají lokální proměnné lexikální obor platnosti a proto definují model podobný zásobníku, ale protože nejsou vázané na objekty ale na hodnoty, implementace může klidně kopírovat tyto hodnoty do datové struktury uzávěru za zády programátora.

Jazyk Scheme, který používá systém lexikálních oborů platnosti ve stylu ALGOLu s dynamickými proměnnými a garbage kolektorem, nemá zásobníkový programovací model a netrpí omezeními jazyků založených na zásobníku. Ve Scheme se uzávěry vyjadřují přirozeně. Lambda tvar obklopuje kód, a volné proměnné jeho prostředí přetrvávat v programu, dokud k nim lze přistupovat, a tak mohou být používány stejně volně jako jiné výrazy ve Scheme.[zdroj?]

Uzávěry se podobají Aktérům v Aktorovém modelu paralelních výpočtů, kde se hodnoty v lexikálním prostředí funkce nazývají acquaintances. Důležitou otázkou týkající se uzávěrů v jazycích pro paralelní programování je, zda lze modifikovat proměnné v uzávěru, a pokud ano, jak lze tyto modifikace synchronizovat. Jedno řešení poskytují aktéři.[12]

Uzávěry jsou podobné funkčním objektům; transformaci uzávěrů na funkční objekty se říká defunctionalization nebo lambda lifting; viz také konverze uzávěrů.[zdroj?]

Rozdíly v sémantice

[editovat | editovat zdroj]

Lexikální prostředí

[editovat | editovat zdroj]

Protože různé jazyky nemají vždy stejně definované lexikální prostředí, mohou mít také různé definice uzávěru. Obvyklá minimalistická definice lexikálního prostředí jej definuje jako sadu všech vazeb proměnných v oboru platnosti, což musí také uzávěry v libovolném jazyce zachycovat. Také význam vazeb proměnných je různý. V imperativních jazycích se proměnné vážou k relativním lokacím v paměti, do které se ukládají hodnoty. I když se relativní umístění vazeb v době běhu nemění, hodnota ve vázané lokaci se měnit může. Protože uzávěr v takových jazycích zachycuje vazby, jakákoli operace s proměnnou, ať je provedena z uzávěru nebo ne, se provádí na stejnou relativní lokaci v paměti. Tomu se obvykle říká zachycení proměnné „referencí“. Tento koncept ilustruje následující příklad v ECMAScriptu:

// Javascript
var f, g;
function foo() {
  var x;
  f = function() { return ++x; };
  g = function() { return --x; };
  x = 1;
  alert('v foo, volání na f(): ' + f());
}
foo(); // 2
alert('volání na g(): ' + g()); // 1 (--x)
alert('volání na g(): ' + g()); // 0 (--x)
alert('volání na f(): ' + f()); // 1 (++x)
alert('volání na f(): ' + f()); // 2 (++x)

Funkce foo a uzávěry, na které se odkazuje proměnná f a g používají stejné relativní lokace v paměti popsané lokální proměnnou x.

V některých instancích může být výše uvedené chování nežádoucí, a je třeba připojit jiný lexikální uzávěr. V ECMAScriptu je to možné provést pomocí Function.bind().

Příklad 1: Odkaz na nevázanou proměnnou

[editovat | editovat zdroj]

Podle[13]

var modul = {
  x: 42,
  getX: funkce() {return toto.x; }
}
var unboundGetX = modul.getX;
console.log(unboundGetX()); // Funkce je vyvolána v globálním oboru platnosti
// vypíše undefined, protože 'x' není definováno v globálním oboru platnosti.

var boundGetX = unboundGetX.bind(modul); // specifikují modul objektu jako uzávěr
console.log(boundGetX()); // vypíše 42

Příklad 2: Neúmyslný odkaz na vázanou proměnnou

[editovat | editovat zdroj]

Očekávané chování v tomto příkladě by bylo, že každý odkaz musí vypsat svůj id, když se na něj klikne; ale protože proměnná 'e' je vázaná k oboru platnosti výše, a při kliknutí je líně vyhodnocena, dojde k tomu, že každá událost kliknutí emituje identifikátor posledního prvku z 'prvků' vázaných na konci for cyklu.[14]

var elements = document.getElementsByTagName('a');
// Nesprávný: e je vázaný k funkci obsahující 'for' cyklus, ne uzávěr funkce "handle"
pro (var e of elements) {   e.onclick = function handle() {     alert(e.id);
  } }

Proměnná e zde opět musí být vázána oborem platnosti bloku pomocí handle.bind(this) nebo klíčovým slovem let.

Na druhou stranu mnoho funkcionálních jazyků, např. ML, váže proměnné přímo na hodnoty. Protože v tomto případě neexistuje žádný způsob, jak změnit hodnotu proměnné, jakmile je jednou svázána, neexistuje žádné potřebuje pro sdílení stav mezi uzávěry – prostě používají stejné hodnoty. Tento postup se obvykle nazývá zachycení proměnné „hodnotou“. Lokální a anonymní třídy Javy také spadají do této kategorie – vyžadují, aby zachycené lokální proměnné byly final, což také znamená, že není třeba sdílet stav.

Některé jazyky umožňují výběr mezi zachycením hodnoty proměnné nebo odkazu na proměnnou. Například v C++11 se zachycené proměnné deklarují buď s [&], což znamená zachycení referencí nebo s [=], což znamená zachycení hodnotou.

Další skupina, funkcionální jazyky s odloženým vyhodnocováním, jako např. Haskell (programovací jazyk), svazuje proměnné k výsledkům budoucí výpočtů místo hodnot. Uvažujme tento příklad v Haskellu:

-- Haskell
foo :: Fractional a => a -> a -> (a -> a)
foo x y = (\z -> z + r)
          where r = x / y

f :: Fractional a => a -> a
f = foo 1 0

main = print (f 123)

Vazba r zachycená uzávěrem definovaným uvnitř funkce foo je pro výpočet (x / y),, což v tomto případě vede k dělení nulou. Protože však je zachycen výpočet, nikoli hodnota, dojde k chybě pouze pokud je vyvolán uzávěr, a pak se snaží použít zachycenou vazbu.

Opuštění uzávěru

[editovat | editovat zdroj]

Ještě více rozdílů se objevuje v chování jiných konstruktů s lexikálním oborem platnosti, např. příkazů return, break a continue. Tyto konstrukty lze obecně považovat za vyvolání únikového pokračování vytvořeného obklopujícím řídicím příkazem (v případě break a continue takové interpretace vyžaduje, aby smyčky byly považovány za rekurzivní volání funkcí). V některých jazycích, např. v ECMAScriptu, příkaz return znamená pokračování vytvořených uzávěrem lexikálně nejvnitřnějšího podle příkaz—tedy, return uvnitř uzávěru přenese řízení do kódu, který jej vyvolal. Ale, v Smalltalk, superficially podobný operátor ^ vyvolá únikové pokračování vytvořených pro volání metody, zanedbáváme úniková pokračování libovolného intervening vnořeného uzávěru. Únikové pokračování určitého uzávěru může být ve Smalltalk vyvoláno pouze implicitně dosažením konce kódu uzávěru. Rozdíl ukazují následující příklady v ECMAScriptu a Smalltalku:

"Smalltalk"
foo
  | xs |
  xs := #(1 2 3 4).
  xs do: [:x | ^x].
  ^0
bar
  Transcript ukazují: (self foo printString) "vypíše 1"
// ECMAScript
function foo() {
  var xs = [1, 2, 3, 4];
  xs.forEach(function (x) { return x; });
  return 0;
}
alert(foo()); // vypíše 0

Výše uvedené kusy kódu se budou chovat odlišně, protože operátor ^ ve Smalltalku a operátor return v JavaScriptu se chovají různě. V případě ECMAScriptu return x opustí vnitřní uzávěr a začne další iteraci cyklu forEach, zatímco v případě Smalltalku ^x ukončí cyklus a provede návrat z metody foo.

Common Lisp poskytuje konstrukci, která umožňuje vyjádřit obě výše uvedené možnosti: (return-from foo x) se chová jako ^x ve Smalltalku, zatímco (return-from nil x) se chová jako return x v JavaScriptu. Smalltalk tedy umožňuje, aby zachycené únikové pokračování přežilo oblast, ve které může být úspěšně vyvolané. Uvažujme:

"Smalltalk"
foo
    ^[ :x | ^x ]
bar
    | f |
    f := self foo.
    f value: 123 "chyba!"

Když je uzávěr vrácený metodou foo vyvolaný, pokusí se vrátit hodnotu z vyvolání mwtody foo, která uzávěr vytvořila. Protože z tohoto volání má už vracené a model volání metody ve Smalltalk se neřídí pořádkem daným špagetovým zásobníkem pro umožnění vícenásobných návratů, tato operace způsobí chybu.

Některé jazyky jako Ruby dávají programátorovi možnost výběru, jak má být return zachyceno. Příklad v Ruby:

# Ruby

# Uzávěr používající Proc
def foo
  f = Proc.new { return "návrat z foo v proc" }
  f.call # zde řízení opouští foo
  return "návrat z foo"
end

# Uzávěr používající lambda
def bar
  f = lambda { return "návrat z lambda" }
  f.volání # kontrola/kontrolovat does ne ponechá bar zde
  return "návrat z bar"
end

puts foo # vypíše "návrat z foo z v proc"
puts bar # vypíše "návrat z bar"

Proc.new i lambda v tomto příkladě vytvářejí uzávěr, ale sémantika takto vytvořených uzávěrů závisí na příkazu return.

V jazyce Scheme je definice a obor platnosti řídicího příkazu return explicitní (a v tomto příkladě je pojmenovaný 'return'). Následující příklad je přímým překladem příkladu v jazyce Ruby:

; Scheme
(define call/cc call-with-current-continuation)

(define (foo)
  (call/cc
   (lambda (return)
     (define (f) (return "návrat z foo v proc"))
     (f) ; control leaves foo here
     (return "návrat z foo"))))

(define (bar)
  (call/cc
   (lambda (return)
     (define (f) (call/cc (lambda (return) (return "návrat z lambda"))))
     (f) ; control does not leave bar here
     (return "návrat z bar"))))

(display (foo)) ; vypíše "návrat z foo v proc"
(newline)
(display (bar)) ; vypíše "návrat z bar"

Konstrukty podobné uzávěrům

[editovat | editovat zdroj]

Některé jazyky mají vlastnosti, které se podobají chování uzávěrů. V jazycích jako je C++, C#, D, Java, Objective-C a Visual Basic .NET (VB.NET) jsou tyto vlastnosti důsledkem objektové orientace jazyka.

Callbacky (C)

[editovat | editovat zdroj]

Některé knihovny jazyka C podporují callbacky. To se někdy implementuje použitím dvou hodnot při registraci callbacku v knihovně: jeden je ukazatel na funkci a druhý zvláštní ukazatel typu void* na libovolná data podle potřeby uživatele. Když knihovna volá callback funkci, předá jí datový ukazatel. To umožňuje, aby callback udržoval stav a odkazoval se na informace zachycené v okamžiku, kdy byl registrovaný v knihovně. Tento idiom má podobnou funkčnost jako uzávěr, ale jinou syntaxi. Ukazatel void* není typově bezpečný, čímž se tento idiom v jazyce C liší od typově bezpečných uzávěrů v jazycích C#, Haskell nebo ML.

Callbacky se široce používají ve widget toolkitech pro grafické uživatelské rozhraní (GUI) pro implementaci událostmi řízeného programování propojením obecných funkcí grafických widgetů (nabídek, tlačítek, zaškrtávacích polí, šoupátek, číselníků, atd.) s funkcemi specifickými pro aplikaci, které v aplikaci implementují požadované chování.

Vnořené funkce a ukazatel na funkci (C)

[editovat | editovat zdroj]

Rozšíření v překladači GCC umožňuje používat vnořené funkce[15] a pomocí ukazatelů na funkce je možné emulovat uzávěry, pokud funkce does neopustí obklopující obor platnosti. Následující příklad je chybný, protože adder je definováno na nejvyšší úrovni (v některáých verzích překladače může produkovat správný výsledek při překladu bez optimalizace, tj. s volbou -O0):

#include <stdio.h>

typedef int (*fn_int_to_int)(int); // typ funkce int->int

fn_int_to_int adder(int number) {
  int add (int value) { return value + number; }
  return &add; // & operátor je zde nepovinný, protože jméno funkce v jazyce C je ukazatel ukazující na funkci
}

int main(void) {
  fn_int_to_int add10 = adder(10);
  printf("%d\n", add10(1));
  return 0;
}

Po přesunu definice funkce adder (a volitelným použitím typedef) do main je však kód dobře:

#include <stdio.h>

int main(void) {
  typedef int (*fn_int_to_int)(int); // typ funkce int->int
  
  fn_int_to_int adder(int number) {
    int add (int value) { return value + number; }
    return add;
  }
  
  fn_int_to_int add10 = adder(10);
  printf("%d\n", add10(1));
  return 0;
}

Program vypíše podle očekávání 11.

Lokální třídy a lambda funkce (Java)

[editovat | editovat zdroj]

Java povoluje třídy být definovaný v metody. Nazývají se lokální třídy. Když takový třídy nejsou pojmenovaný, jsou známou jako anonymní třídy (nebo anonymní vnitřní třídy). A lokální třída (buď pojmenovaný nebo anonymní) může se odkazuje na jména lexikálně obklopujícího třídy, anebo proměnné pouze pro čtení (vyznačené jako final) v lexikálně obklopujícího metoda.

class CalculationWindow extends JFrame {
    private volatile int result;
    // ...
    public void calculateInSeparateThread(final URI uri) {
        // Výraz "new Runnable() { ... }" je anonymní třída implementujícíous 'Runnable' rozhraní.
        new Thread(
            new Runnable() {
                void run() {
                    // Může číst lokální proměnné označené jako final:
                    calculate(uri);
                    // Může přistupovat k soukromým položkám obklopující třídy:
                    result = result + 10;
                }
            }
        ).start();
    }
}

Zachycení proměnné final umožňuje zachycení proměnné hodnotou. I kdyby proměnná, která se má zachytit nebyla final, je možné ji zkopírovat do dočasné proměnné, která je final těsně před třídou.

Zachycení proměnných referencí lze emulovat pomocí final odkazu na mutable kontejner, například jednoprvkové pole. Lokální třída nebude moci měnit hodnotu reference na kontejner, ale bude schopna změnit obsah kontejneru.

S příchodem lambda výrazů ve verzi 8 Javy[16] uzávěr způsobí, že se výše uvedený kód bude provádět takto:

class CalculationWindow extends JFrame {
    private volatile int result;
    // ...
    public void calculateInSeparateThread(final URI uri) {
        // Část code () -> { /* code */ } je uzávěr.
        new Thread(() -> {
            calculate(uri);
            result = result + 10;
        }).start();
    }
}

Lokální třídy jsou jedním z typů vnitřních tříd, které se deklarují v těle metody. Java také podporuje vnitřní třídy, které se deklarují jako nestatické členy obklopující třídy,[17] a obvykle jsou jednoduše nazývány „vnitřní třídy“.[18] Jsou definovány v těle obklopující třídy a mají plný přístup k instančním proměnným obklopující třídy. Kvůli jejich vazbě na tyto instanční proměnné mohou být vnitřní třídy instanciovány pouze s explicitní vazbou na instance obklopující třída pomocí/použití speciální syntax.[19]

public class EnclosingClass {
    /* Definice vnitřní třídy */
    public class InnerClass {
        public int incrementAndReturnCounter() {
            return counter++;
        }
    }

    private int counter;
    {
        counter = 0;
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        EnclosingClass enclosingClassInstance = new EnclosingClass();
        /* Instanciace vnitřní třídy s vazbou na instance */
        EnclosingClass.InnerClass innerClassInstance =
            enclosingClassInstance.new InnerClass();

        for (int i = enclosingClassInstance.getCounter();
             (i = innerClassInstance.incrementAndReturnCounter()) < 10;
             /* inkrement není použit */) {
            System.out.println(i);
        }
    }
}

Program vytiskne čísla od 0 do 9. Tento typ třídy se nesmí zaměňovat s vnořenou třídou deklarovanou stejně, ale s modifikátorem „static“; pak by šlo o třídu bez speciální vazby definované v obklopující třídě.

Java podporuje funkce jako objekty první kategorie od verze 8. Lambda výrazy tohoto tvaru jsou považovány za typ Function<t,U>, kde T je doména a U typ obrazu. Tento výraz nelze volat standardním způsobem volání metody, je třeba použít metodu .apply(T t).

public static void main(String[] args) {
    Function<String, Integer> length = s -> s.length();

    System.out.println( length.apply("Hello, world!") ); // Vypíše 13.
}
Podrobnější informace naleznete v článku Blocks_(C_language_extension).

Firma Apple zavedla bloky, forma uzávěru, jako nestandardní rozšíření jazyků C, C++, Objective-C a v Mac OS X 10.6 „Snow Leopard“ a iOS 4.0. Apple vytvořený jejich implementace dostupný pro GCC a clang překladače.

Ukazatele na bloky a blokové literály jsou označeny ^. Normální lokální proměnné jsou při vytvoření bloku zachyceny hodnotou, a uvnitř bloku je lze pouze číst. Proměnné, které mají být zachyceny referencí, jsou označeny __block. Bloky, které mají přetrvávat mimo obor platnosti, v němž jsou vytvořeny, může být potřebné zkopírovat.[20][21]

typedef int (^IntBlock)();

IntBlock downCounter(int start) {
	 __block int i = start;
	 return [[ ^int() {
		 return i--;
	 } copy] autorelease];
}

IntBlock f = downCounter(5);
NSLog(@"%d", f());
NSLog(@"%d", f());
NSLog(@"%d", f());

Delegáti (C#, VB.NET, D)

[editovat | editovat zdroj]

Anonymní metody a lambda výrazy v C# podporují uzávěry:

var data = new[] {1, 2, 3, 4};
var multiplier = 2;
var result = data.Select(x => x * multiplier);

Také Visual Basic .NET, který se v mnoha ohledech podobá C#, podporuje lambda výrazy s uzávěry:

Dim data = {1, 2, 3, 4}
Dim multiplier = 2
Dim result = data.Select(Function(x) x * multiplier)

V jazyce D jsou uzávěry implementovány pomocí delegátů, ukazatelů na funkci spojených s ukazatelem na kontext (kterým může být např. instance třídy nebo rámec zásobníku na haldě v případě uzávěrů).

auto test1() {
    int = 7;
    return delegate() { return + 3; }; // konstrukce anonymního delegáta
}

auto test2() {
    int = 20;
    int foo() { return + 5; } // vnitřní funkce
    return &foo; // jiný způsob konstrukce delegáta
}

void bar() {
    auto dg = test1();
    dg(); // =10 // ok, test1.a je v uzávěr a stále existuje

    dg = test2();
    dg(); // =25 // ok, test2.a je v uzávěr a stále existuje
}

D verze 1 má omezenou podporu uzávěrů. Například výše uvedený kód nebude pracovat správně, protože proměnná je na zásobníku, a po návratu z funkce test(), už ji nelze používat (nejpravděpodobněji volání foo pomocí dg() bude vracet 'náhodné' celé číslo). To lze vyřešit explicitním alokováním proměnné 'a' na haldě nebo použitím structs nebo třídy pro uložení všech potřebných uzavřených proměnných a zkonstruování delegáta z metody implementující stejný kód. Uzávěry je možné předat do jiných funkcí, ale musí se používat, pouze když referencované hodnoty jsou stále platné (například voláním jiné funkce s uzávěr jako callback parametrem), a jsou užitečné pro zapisování kódu, který zpracovává obecná data, takže v praxi toto omezení obvykle nezpůsobuje žádné potíže.

Toto omezení bylo opraveno ve verzi 2 – proměnná 'a' bude automaticky umístěná na haldě, protože se používá ve vnitřní funkci, a delegace této funkce může opustit aktuální obor platnosti (přiřazením do dg nebo návratem). Jiné lokální proměnné (nebo argumenty), které nejsou referencovanými delegáty nebo které jsou pouze referencovanými delegáty, které neopustí aktuální obor platnosti, zůstávají na zásobníku, což je jednodušší a rychlejší než jejich alokace na haldě. Totéž je splněno pro metody vnitřní třídy, které referencují proměnné funkce.

Funkční objekty (C++)

[editovat | editovat zdroj]

Jazyk C++ umožňuje vytváření funkčních objektů přetěžováním operátoru operator(). Chování těchto objektů poněkud připomíná chování funkcí v funkcionálních programovacích jazycích. Mohou se vytvářet v době běhu a mohou obsahovat stav, ale na rozdíl od uzávěrů implicitně nezachycují lokální proměnné. Jazyk C++ podporuje od verze C++11 také uzávěry, což jsou funkční objekty vytvořené automaticky ze speciální jazykové konstrukce nazývané lambda-výraz. Uzávěr v C++ může zachytit svůj kontext buď tím, že uloží kopie proměnných, ke kterým přistupuje, jako členy uzávěrového objektu nebo pomocí referencí. Pokud při zachycení referencí uzávěrový objekt opustí obor platnosti referenceovaného objektu, vyvolání funkce z uzávěru způsobí nedefinované chování, protože v C++ uzávěry nerozšiřují životnost svého kontext.

Podrobnější informace naleznete v článku Anonymní_funkce#C++_(od_C++11).
void foo(string myname) {
    int y;
    vector<string> n;
    // ...
    auto i = std::find_if(n.begin(), n.end(),
               // toto je lambda výraz:
               [&](const string& s) { return s != myname && s.size() > y; }
             );
    // 'i' je nyní buď 'n.end()' anebo ukazuje na první řetězec v 'n',
    // který není roven 'myname' a jehož délka je větší než 'y'
}

Inline agenti (Eiffel)

[editovat | editovat zdroj]

V jazyce Eiffel lze definovat uzávěry pomocí inline agentů. Inline agent je objekt reprezentující funkci definovaný uvedením kódu funkce v-řádek/vedení. Například v

ok_button.click_event.subscribe (
	agent (x, y: INTEGER) do
		map.country_at_coordinates (x, y).display
	end
)

Argument funkce subscribe je agent tvořený procedurou se dvěma argumenty; procedura najde zemi na odpovídajících souřadnicích a zobrazí ji. Celý agent má „předplacené“ události typu click_event na určité tlačítko, takže se když instance typu události objeví na tomto tlačítku – protože na něj uživatel klikl – provede se procedura, které se předají jako argumenty x a y souřadnice myši.

Hlavním omezením agentů v Eiffelu, které je odlišuje od uzávěrů v jiných jazycích, je, že se nemohou odkazovat na lokální proměnné z obklopujícího oboru platnosti. Toto designové rozhodnutí zabraňuje nejednoznačnosti, když hovoříme o hodnotě lokální proměnné v uzávěru – má to být nejnovější hodnota proměnné nebo hodnota zachycená, když byl agent vytvořen? Z těla agenta lze přistupovat pouze ke Current (odkaz na aktuální objekt, obdoba this v Javě), jeho vlastnostem a argumentům agenta. Hodnoty vnějších lokálních proměnných mohou být agentovi předány pomocí dalších uzavřených operandů.

Rezervované slovo __closure v C++Builder

[editovat | editovat zdroj]

Embarcadero C++Builder má rezervované slovo __closure, kterým lze předat ukazatel na metodu s podobnou syntaxí jako ukazatel na funkci.[22]

Standardní jazyk C umožňuje použít typedef pro ukazatel na typ funkce s následující syntaxí:

typedef void (*TMyFunctionPointer)( void );

Podobným způsobem lze deklarovat typedef pro ukazatel na metodu s následující syntaxí:

typedef void (__closure *TMyMethodPointer)();
  1. Tato jména se obvykle odkazují na hodnoty, proměnné nebo funkce, ale mohou se odkazovat i na jiné entity např. konstanty, typy, třídy nebo návěstí.
  2. Funkce může být uložena jako reference na funkci, např. jako funkce ukazatel.
  3. Tato jména se obvykle odkazují na hodnoty, mutable proměnné nebo funkce; mohou se však odkazovat také na jiné entity, např. konstanty, typy, třídy nebo návěstí.

V tomto článku byl použit překlad textu z článku Closure (computer programming) na anglické Wikipedii.

  1. Sussman and Steele. "Scheme: An interpreter for extended lambda calculues". "... datová struktura obsahující lambda výraz a prostředí, které má být použito, když se tento lambda výraz aplikuje na argumenty." (Wikisource)
  2. TURNER, David A., 2012. Some History of Functional Programming Languages. In: International Symposium on Trends in Functional Programming. [s.l.]: Springer. Dostupné online. ISBN 978-3-642-40447-4. DOI 10.1007/978-3-642-40447-4_1. S. 1–20 See 12 §2, note 8 for the claim about M-expressions.
  3. LANDIN, P.J., 1964. The mechanical evaluation of expressions. The Computer Journal. Leden 1964, roč. 6, čís. 4, s. 308–320. Dostupné online. DOI 10.1093/comjnl/6.4.308. 
  4. MOSES, Joel, 1970. The Function of FUNCTION in LISP, or Why the FUNARG Problem Should Be Called the Environment Problem. ACM Sigsam Bulletin. Červen 1970, čís. 15, s. 13–27. Užitečnou metaforou rozdílu mezi FUNCTION a QUOTE v LISPu je uvažovat QUOTE jako porézní nebo otevřený obal funkce, protože volné proměnné unikají do aktuálního prostředí. FUNCTION funguje jako uzavřený nebo neporézní obal (odtud termín „uzávěr“, který používal Landin). Hovoříme tedy o „otevřených“ Lambda výrazech (funkce v LISPu jsou obvykle Lambda výrazy) a „uzavřených“ Lambda výrazech. [...] Můj zájem o problém prostředí začal v době, kdy Landin, který se tímto problémem hluboce zabýval, navštívil v letech 1966–67 MIT. Tehdy jsem si uvědomil korespondenci mezi FUNARG seznamy, které jsou výsledkem vyhodnocení „uzavřených“ Lambda výrazů v Lispu a uzavřenými lambda výrazy v programovacím jazyce ISWIM.. DOI 10.1145/1093410.1093411. S2CID 17514262. AI Memo199. 
  5. WIKSTRÖM, Åke, 1987. Functional Programming using Standard ML. [s.l.]: Prentice Hall. ISBN 0-13-331968-7. Důvodem používání názvu „uzávěr“ je, že výraz obsahující volné proměnné se nazývá „otevřený“ výraz, a přiřazením vazeb jeho volným proměnným se uzavře.. 
  6. SUSSMAN, Gerald Jay; STEELE, Guy L. Jr. Scheme: An Interpreter for the Extended Lambda Calculus. [s.l.]: [s.n.], prosinec 1975. AI Memo 349. 
  7. ABELSON, Harold; SUSSMAN, Gerald Jay; SUSSMAN, Julie, 1996. Structure and Interpretation of Computer Programs. [s.l.]: MIT Press. Dostupné online. ISBN 0-262-51087-1. S. 98–99. 
  8. array.filter [online]. 2010-01-10 [cit. 2010-02-09]. Dostupné online. 
  9. Re: FP, OO and relations. Does anyone trump the others? [online]. 1999-12-29 [cit. 2008-12-23]. Dostupné v archivu pořízeném dne 2008-12-26. 
  10. Lambda Expressions and Closures C++ Standards Committee. 29 February 2008.
  11. 6.4 Nested Functions [online]. Pokud se snažíte volat vnořenou funkci její adresou poté, co funkce, která ji obklopuje, skončila, vyvoláte peklo. Pokud se ji snažíte vyvolat po opuštění obklopujícího oboru platnosti, a pokud se odkazuje na některé z proměnných, které už nejsou v oboru platnosti, můžete mít štěstí, ale za to riziko to nestojí. Pokud se však vnořená funkce neodkazuje na nic, co zmizelo z oboru platnosti, je to bezpečné.. Dostupné online. 
  12. Foundations of Actor Semantics Will Clinger. MIT Mathematics Doctoral Dissertation. June 1981.
  13. Function.prototype.bind() [online]. MDN Web Docs [cit. 2018-11-20]. Dostupné online. 
  14. Closures [online]. MDN Web Docs [cit. 2018-11-20]. Dostupné online. 
  15. Nested functions [online]. Dostupné online. 
  16. Lambda Expressions [online]. Dostupné online. 
  17. Nested, Inner, Member, and Top-Level Classes [online]. July 2007. Dostupné v archivu pořízeném z originálu dne 2016-08-31. 
  18. Inner Class Example [online]. Dostupné online. 
  19. Nested Classes [online]. Dostupné online. 
  20. Blocks Programming Topics [online]. Apple Inc., 2011-03-08 [cit. 2011-03-08]. Dostupné online. 
  21. BENGTSSON, Joachim. Programming with C Blocks on Apple Devices [online]. 2010-07-07 [cit. 2010-09-18]. Dostupné v archivu pořízeném dne 2010-10-25. 
  22. Kompletní dokumentace je na http://docwiki.embarcadero.com/RADStudio/Rio/en/Closure

Související články

[editovat | editovat zdroj]

Externí odkazy

[editovat | editovat zdroj]