Strategie výpočtu

Aktuální verze stránky ještě nebyla zkontrolována zkušenými přispěvateli a může se výrazně lišit od verze recenzované 10. srpna 2022; ověření vyžaduje 1 úpravu .

Strategie hodnocení - pravidla sémantiky programovacího jazyka , která určují, kdy mají být vyhodnoceny argumenty funkce ( metoda, operace, vztah) a jaké hodnoty by měly být předány .  Strategie call-by-worth/pass-by-reference například diktuje , že argumenty musí být vyhodnoceny před provedením těla volané funkce a že musí mít pro každý argument dvě možnosti: čtení aktuální hodnoty a změnou pomocí operátoru přiřazení [1] . Tato strategie je podobná redukční strategii v lambda kalkulu, ale existují rozdíly.

V praxi se výpočetní model mnoha průmyslových jazyků ( Java , C# ) scvrkává na strategii „ call-at-mention/pass-by-reference “ . Některé starší jazyky, zvláště ty nebezpečné , jako je C++ , kombinují několik různých vzorů volání. Historicky se „ volání podle hodnoty “ a „ volání podle jména “ vrací k Algol-60 , vytvořenému koncem 50. let 20. století . Pouze čisté funkční jazyky jako Clean a Haskell používají „ volání z nutnosti “.

Poznámka  – v ruskojazyčné literatuře se strategie výpočtu také nazývá „ metoda předávání parametrů “, „ výpočetní model “ nebo „ model volání “. Poslednímožnost může způsobit zmatek s konvencí volání . Termín " předávání parametrů " je pro mnoho strategií výpočtu nesprávný.

Přísné výpočty

Striktní model hodnocení znamená, že argumenty jsou vždy plně vyhodnoceny předtím, než je na ně aplikována funkce . 

V církevní notaci dychtivé hodnocení výroků odpovídá přísnému hodnocení funkcí, a proto se přísné hodnocení někdy nazývá „ netrpělivý “. Většina existujících jazyků používá přísné hodnocení funkcí.

Aplikační pořadí

Aplikační pořadí , také „ zleva doprava, dovnitř ven “, ( nejvnitřnější zleva ) [2] [3] , znamená strategii  výpočtu , ve které zdola nahoru AST vyhodnocuje argumenty zleva doprava v redukovaných výrazech.

Na rozdíl od volání podle hodnoty, aplikační pořadí vyhodnocení redukuje termíny v těle funkce, než je to možné, než je aplikováno.

Abychom zvážili příklad výpočtů v aplikačním pořadí, definujeme několik funkcí [4] :

čtverec (x) = x * x součet_čtverců (x, y) = čtverec (x) + čtverec (y) f(x) = součet_čtverců (x + 1, x * 2)

Při výpočtu hodnoty f(5) dostaneme následující sadu substitucí:

f(5) = součet_čtverců (5 + 1, 5 * 2) = čtverec (6) + čtverec (10) = ((6 * 6) + (10 * 10)) = 36 + 100 = 136

Volání podle hodnoty (call-by-value)

Call by value ( anglicky  call-by-value ) je nejrozšířenější výpočetní strategií, lze ji vidět v různých jazycích, od C po Scheme . Při volání hodnotou je argumentový výraz vyhodnocen a výsledná hodnota je spojena s odpovídajícím parametrem formální funkce (obvykle zkopírováním této hodnoty do nového paměťového místa). V tomto případě, pokud jazyk umožňuje funkcím přiřazovat hodnoty jejich parametrům, pak změny ovlivní pouze tyto lokální kopie, ale hodnoty viditelné v místě volání funkce zůstanou po návratu nezměněny.

Ve skutečnosti volání podle hodnoty není jeden konkrétní vzor volání, ale rodina vzorů, ve kterých jsou argumenty vyhodnocovány před předáním do těla funkce. Většina jazyků ( Common Lisp , Eiffel , Java ), které používají volání podle hodnoty, vyhodnocuje argumenty funkce zleva doprava, ale některé je vyhodnocují zprava doleva a některé ( Schéma , OCaml , C ) neurčují pořadí vyhodnocování. .

Skrytá omezení

V některých případech není termín " volání podle hodnoty " zcela správný, protože předaná hodnota není hodnotou proměnné v obvyklém smyslu, ale odkazem na hodnotu, jejíž implementace může být odlišná. V důsledku toho se kód, který vypadá syntakticky jako volání podle hodnoty, může chovat buď jako odkaz podle volání, nebo jako společné použití a chování programu bude záviset na jemných detailech sémantiky jazyka.

Důvodem pro použití volání odkazem je obvykle to, že jazyk technicky neposkytuje možnost pracovat se složitými daty jako jedinou hodnotou – představuje je jako datovou strukturu, i když to vypadá velmi podobně jako hodnota ve zdroji. kód. Určit přesné umístění čáry mezi plnohodnotnou hodnotou a maskující se datovou strukturou může být velmi obtížné. V C je vektor (tj. jednorozměrné pole , jehož speciálním případem je znakový řetězec) datovou strukturou , a proto se s ním zachází jako s odkazem na paměťové místo; struktura je však hodnotou, i když její pole jsou vektory. V Maple je vektor speciálním případem tabulky, a tedy datové struktury; ale seznam (který je vytvořen a indexován přesně stejným způsobem) je hodnota. Tcl zachází s hodnotami dvěma způsoby: reprezentace hodnot se používá na úrovni skriptu a jazyk sám spravuje vhodnou datovou strukturu podle potřeby. Změny provedené ve struktuře dat se projeví v hodnotě a naopak.

Vysvětlení, že jazyk " předává parametry hodnotou, kde hodnota je odkazem " je docela běžné (ale nemělo by být zaměňováno s voláním odkazem); jinak se tomu říká co-use call . Z tohoto důvodu se volání podle hodnoty v Javě a Visual Basic chová výrazně odlišně než volání podle hodnoty v C a Pascalu . V C nebo Pascalu předání masivní datové struktury funkci zkopíruje celou strukturu (pokud argument není ve skutečnosti odkazem na datovou strukturu), což potenciálně výrazně sníží výkon; změny stavu struktury však nebudou v kontextu volání viditelné. V Javě a Visual Basicu se vždy zkopíruje pouze odkaz na strukturu, což je rychlé a změna struktury bude viditelná na místě volání.

Call by reference (call-by-reference)

Při volání-odkazem ( eng.  call-by-reference ) nebo předáváním odkazem ( pass-by-reference ) funkce implicitně obdrží odkaz na proměnnou použitou jako argument, namísto kopie jejího hodnota.

To obvykle znamená, že funkce může upravit (tj. změnit stav ) proměnnou předávanou jako parametr, což bude mít vliv na kontext volání. Proto lze volání odkazem použít k vytvoření komunikačního kanálu mezi volaným a volajícím. Jazyk založený přímo na volání odkazem ztěžuje programátorovi sledovat všechny účinky volání funkce, takže může být chybný .

Mnoho jazyků podporuje call-by-reference v té či oné formě, ale jen málo z nich ji používá ve výchozím nastavení, jako například Perl . Řada jazyků, jako je C++ , PHP , Visual Basic .NET , C# a REALbasic , standardně používá volání podle hodnoty, ale poskytuje speciální syntaxi pro volání odkazem. C++ navíc zavádí jedinečnou strategii volání po odkazu na konstantní .

Typové systémy některých jazyků, které používají volání podle hodnoty a přímo nepodporují volání odkazem, poskytují možnost explicitně definovat odkazy (objekty odkazující na jiné objekty), zejména ukazatele (objekty, které jsou adresami jiných objektů v počítači). Paměť). Jejich použití vám umožňuje simulovat volání odkazem uvnitř sémantiky volání podle hodnoty. Takové řešení se používá například v jazycích C a ML . Nejde o samostatnou hodnotící strategii – jazyk stále volá podle hodnoty – ale někdy se označuje jako „ call-by-address “ ( call-by-address ) nebo „ pass-by-address “ ( pass-by-address ) . V nebezpečných jazycích, jako je C nebo C++ , může vést k chybám přístupu do paměti , jako je dereference nulového ukazatele , což znesnadňuje porozumění programu a zpočátku se jazyk učí. V ML jsou odkazy typu -safe a memory -safe .

Blízký efekt poskytuje také strategie „ call by co-use “ používaná v jazycích jako Java , Python , Ruby .

V čistě funkcionálních jazycích neexistuje žádný sémantický rozdíl mezi voláním odkazem a voláním hodnotou (protože jejich datové struktury jsou neměnné a funkce stejně nemá jak změnit hodnotu svých argumentů), takže jsou obvykle popisovány jako volání podle hodnoty , i když mnoho implementací skutečně používá volání odkazem ke zlepšení efektivity.

Následující příklad ukazuje simulované volání odkazem v jazyce E :

def upravit ( var p, &q ) { p := 27 # parametr předaný hodnotou - změní se pouze lokální hodnota q := 27 # parametr předaný odkazem - změna proměnné použité ve volání }  ? var a := 1 # hodnota: 1  ? var b := 2 # hodnota: 2  ? upravit (a, &b)  ? A # hodnota: 1  ? b # hodnota: 27

Následující příklad ukazuje simulaci volání odkazem v jazyce C. Proměnné typu Integer a ukazatele jsou předávány hodnotou. Ale protože ukazatel obsahuje adresu externí proměnné, její hodnota se změní.

void Upravit ( int p , int * q , int * o ) { // všechny parametry předané hodnotou p = 27 ; // změní se pouze lokální hodnota * q = 27 ; // změní vnější proměnnou, na kterou ukazuje q * o = 27 ; // změna externí proměnné, na kterou ukazuje o } int main () { int a = 1 ; int b = 1 ; int x = 1 ; int * c = & x ; Upravit ( a , & b , c ); // 1. parametr - hodnota proměnné a // 2. parametr - adresa proměnné b // 3. parametr - hodnota proměnné c, což je adresa proměnné x // b a x se mění return ( 0 ); }

Volejte sdílením

call-by-sharing nebo call-with-resource-sharing ( anglicky  call-by-sharing ), také call-by-object ( call-by-object ), také call-by-object-sharing nebo call-with- shared -object ( call-by-object-sharing ), znamená, že hodnoty v jazyce jsou založeny na objektech, a nikoli na primitivních typech , to znamená „ zabalené “ („zabalené“, anglicky  boxed ). Při volání funkcí co-use získá funkce kopii odkazu na objekt . Objekt samotný se nekopíruje – je sdílený nebo sdílený . V důsledku toho přiřazení k argumentu v těle funkce nemá žádný vliv na kontext volání, ale přiřazení ke komponentám tohoto argumentu ano.

Co-use call byl poprvé implementován v CLU v roce 1974 pod vedením Barbary Liskov a dalších [5] .

Tato strategie se používá v Pythonu [6] , Iota [7] , Java (pro objektové odkazy), Ruby , JavaScript , Scheme , Ocaml , AppleScript a mnoha dalších. Terminologie v různých jazykových komunitách se však liší. Například komunita Pythonu používá termín „co-use call“; v komunitách Java a Visual Basic je stejná sémantika často popisována jako „ volání hodnotou, kde „hodnota“ je odkaz na objekt “; v komunitě Ruby říkají, že Ruby " používá volání odkazem " - navzdory skutečnosti, že sémantika volání v těchto jazycích je identická.

U neměnných objektů není žádný rozdíl mezi voláním podle použití a voláním podle hodnoty kromě toho, že tyto objekty jsou identické . Použití co-use volání je alternativou k vstupním/výstupním parametrům [8]  - změna parametru zde neznamená přiřazení k parametru ; parametr se nepřepíše , ale změní stav a zachová si svou identitu.

Například v Pythonu jsou seznamy měnitelné objekty, takže:

def f ( l ): l . připojit ( 1 ) m = [] f ( m ) tisknout m

- vypíše " [1]", protože argument " l" byl změněn.

Následující příklad ukazuje rozdíl mezi změnou a přiřazením . Kód takto:

def f ( l ): l += [ 1 ] m = [] f ( m ) tisk m

- vypíše " [1]", protože operátor " l += [1]" se chová jako " l.extend([1])"; ale podobný kód:

def f ( l ): l = l + [ 1 ] m = [] f ( m ) tisk m

- vypíše " []", protože operátor " l = l + [1]" místo změny argumentu vytvoří novou lokální proměnnou [9] .

Chování následujícího programu demonstruje sémantiku hodnot v rámečku a volání podle použití:

x = [[]] * 4 x [ 0 ] . připojit ( 'a' ) x [ 1 ] . připojit ( 'b' ) x [ 2 ] . připojit ( 'c' ) tisknout ( x ) >> [[ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ]]

Operátor „ x = [[]] * 4“ vytvoří prázdný seznam (říkejme mu „ l“) a poté nový seznam ( sdružený s identifikátorem „ x“) čtyř prvků, z nichž každý je odkazem na „ l“, tedy „ x = [ l, l, l, l ]“. Následná volání různých prvků seznamu „ x“ změní objekt „ l“. Totéž se stane při tisku seznamu „ x“: protože se skládá ze čtyř odkazů na „ l“, vytiskne se složení „ l“ čtyřikrát.

Zavolejte pomocí copy-restore

call - by  -copy-restore , také copy - in copy-out ( copy-in copy-out ), také call-by-value-in-result ( call-by-value-result ) nebo call -by -value -return , jak se tomu říká v komunitě jazyků Fortran , je speciální případ call-by-reference , ve kterém je poskytnutý odkaz jedinečný pro kontext volání. Tato možnost je zajímavá v kontextu víceprocesorových systémů a vzdálených volání procedur : pokud je parametrem funkce odkaz, ke kterému může přistupovat jiný vykonávající proces, pak lze jeho obsah zkopírovat na nový odkaz, který již nebude dostupný; když se funkce vrátí, změněný obsah tohoto nového odkazu bude zkopírován do původního odkazu ("obnoven").

Sémantika call-by-copy-restore se také liší od volání odkazem, pokud jsou dva nebo více argumentů funkce navzájem aliasy, tj. ukazují na stejnou proměnnou v kontextu volání. V případě volání odkazem bude změna jednoho znamenat změnu druhého. Funkce copy-restore-call tomu zabrání předáním různých kopií do funkce, ale výsledek v kontextu volání není definován, protože závisí na tom, zda je zpětné kopírování ve stejném směru (zleva doprava nebo zprava do -vlevo) jako před výzvou.

Pokud je odkaz předán neinicializovaný, může být tato vyhodnocovací strategie nazývána call - by - result . 

Částečné výpočty

S částečným vyhodnocením ( anglicky  dílčí vyhodnocení ) lze provádět výpočty v neaplikované funkci. Všechny podvýrazy, které neobsahují nevázané proměnné, jsou vyhodnoceny a aplikace funkcí se známými argumenty jsou omezeny. Pokud existují vedlejší účinky, může úplné částečné vyhodnocení produkovat nežádoucí výsledky, takže systémy, které podporují částečné vyhodnocení, je provádějí pouze pro čisté výrazy (výrazy bez vedlejších účinků) ve funkcích.

Nepřísné výpočty

Nepřísný  model hodnocení znamená , že argumenty nejsou vyhodnoceny, dokud není jejich hodnota použita v těle funkce.

Nestriktní hodnocení funkcí odpovídá línému hodnocení operátorů v Church notaci , a proto se nestriktní hodnocení často nazývá " líné ".

V řadě jazyků ( C , C++ atd.) mají booleovské výrazy nepřísné pořadí vyhodnocení, které se v ruskojazyčné literatuře nazývá „ zkratové vyhodnocení “ , kde se výpočty zastaví, jakmile se výsledek se stává jednoznačně předvídatelným – např. hodnota „ pravda “ v disjunkci, „ nepravda “ v konjunkci a tak dále. Operátoři větví mají často také línou sémantiku hodnocení, to znamená, že vrátí výsledek celého operátoru, jakmile jej vygeneruje větev s jednou hodnotou.

normální pořadí

Normální pořadí vyhodnocení ( anglicky  Normal order ; také " výpočet zleva doprava, zvenčí dovnitř ", nejvzdálenější zleva ) je výpočetní strategie, ve které je uzavřený výraz zcela zredukován, přičemž se před vyhodnocením argumentů použijí funkce.

Na rozdíl od normálního pořadí strategie volání podle jména nevyhodnocuje argumenty a výrazy ve funkcích, které nejsou volány.

Například hodnota f(5) pro funkci f definovanou dříve , když je vyhodnocena v normálním pořadí, poskytne následující sadu substitucí [4] :

f(5) = součet čtverců (5 + 1, 5 * 2) = čtverec (5 + 1) + čtverec (5 * 2) = ((5 + 1) * (5 + 1)) + (( 5 * 2) * (5 * 2)) = (6 * 6) + (10 * 10) = 36 + 100 = 136

Volání jménem (call-by-name)

Ve strategii volání podle jména nejsou argumenty vyhodnocovány před voláním funkce. Místo toho jsou nahrazeny přímo do těla funkce (pomocí substituce, která zabrání zachycení ), a poté vyhodnoceny místo požadavku. Pokud argument není použit v těle funkce, není vyhodnocen vůbec; pokud je použit vícekrát, je přepočítán při každém výskytu (viz Jensenův trik ).

Volání podle jména je někdy vhodnější pro volání podle hodnoty. Pokud argument není použit v těle funkce, volání podle jména šetří čas tím, že se nevyhodnocuje, zatímco volání podle hodnoty znamená nevyhnutelné vyhodnocení. Pokud je argumentem neukončující hodnocení , přínos je obrovský. Při použití argumentu je však volání jménem často pomalejší, protože vyžaduje vytvoření takzvaného „ thunk “.

Poprvé bylo volání jménem použito v jazyce Algol-60 . Jazyky .NET mohou simulovat call-by-name pomocí delegátů nebo Expression<T>-parametrů. V druhém případě funkce obdrží AST . Eiffelův jazyk implementuje agenty, což jsou operace prováděné na vyžádání.

Volejte podle nutnosti (volejte podle potřeby)

Call -by-need je zapamatovaná varianta volání podle jména , kde je-li argument vyhodnocen ,  jeho hodnota je uložena pro pozdější použití. V případě „ čistoty jazyka “ (bez vedlejších účinků ) to vede ke stejnému výsledku jako volání jménem; a v případech, kdy je argument použit dvakrát nebo vícekrát, je volání z nutnosti téměř vždy rychlejší.

Protože vyhodnocované výrazy mohou být velmi hluboko vnořené, jazyky call-by-need obvykle nepodporují vedlejší efekty (jako jsou změny stavu ) přímo a musí být emulovány monádami (jako v Haskell ) nebo jedinečnými typy jako v Clean jazyk ). To eliminuje jakékoli nepředvídatelné chování líného vyhodnocení, když se hodnoty proměnných změní před jejich použitím.

Nejběžnější implementací sémantiky call-of-need je líné vyhodnocení , i když existují i ​​jiné varianty, jako je optimistické vyhodnocení .

Haskell  je nejznámější jazyk, který používá call-by-need. R také používá druh call-by-need. Jazyky .NET mohou podle potřeby simulovat volání pomocí Lazy<T>.

Výzva k rozšíření maker

Rozšíření Call- by -  macro-expansion je podobné jako call -by-name, ale místo nezachycující substituce používá textovou substituci. Při neopatrném použití může substituce makra vést k zachycení proměnné a nežádoucímu chování programu. Hygienická makra tento problém eliminují kontrolou a v případě potřeby nahrazením stínovaných neparametrových proměnných.

Nedeterministické strategie

Kompletní β-redukce

Při plné β-redukci lze omezit jakoukoli aplikaci funkce (dosazením argumentu do těla funkce, pomocí substituce zabránit zachycení kdykoli. To lze provést i v těle neaplikované funkce .

Volání podle záměru (volání podle budoucnosti)

Call by  future nebo paralelní call -by -name je paralelní vyhodnocovací strategie: hodnoty výrazů jsou vyhodnocovány paralelně se zbytkem programu. V místech, kde je požadována účelová hodnota, se hlavní program zablokuje, dokud není výpočet dokončen, pokud ještě není dokončen.

Tato strategie je nedeterministická, protože výpočty lze provádět kdykoli mezi okamžikem vytvoření záměru (kde je výraz uveden) a okamžikem použití jeho hodnoty. Podobá se call-by-need v tom, že hodnota je vyhodnocena pouze jednou a vyhodnocení může být odloženo do doby, kdy je hodnota skutečně potřebná, ale může začít dříve. Navíc, pokud cílová hodnota již není vyžadována (např. byla vyhodnocena lokální proměnná v těle funkce a funkce ukončena), může být vyhodnocení přerušeno.

Pokud jsou cíle implementovány prostřednictvím procesů a vláken, pak vytvoření cíle v kódu vytvoří nový proces nebo vlákno, přístup k hodnotě jej synchronizuje s hlavním vláknem a dokončení vyhodnocení cíle znamená zabití procesu, který vypočítal jeho hodnotu.

Optimistické výpočty

Optimistické vyhodnocení je další variantou  call-by-need, ve které je argument funkce částečně vyhodnocen po určitou vyhrazenou dobu (kterou lze konfigurovat během provádění programu), poté jsou výpočty přerušeny a funkce je aplikována pomocí call- podle potřeby. Tento přístup snižuje časové prodlevy spojené s líným hodnocením a zároveň poskytuje stejné vlastnosti produktu.

Viz také

Poznámky

  1. Essentials of Programming Languages ​​od Daniela P. Friedmana a Mitchella Wanda, MIT Press 1989-2006
  2. Lambda Calculus (downlink) . Cs.uiowa.edu. Získáno 29. 5. 2014. Archivováno z originálu 14. 12. 2010. 
  3. aplikační redukce řádu definice aplikačního redukce pořadí ve Free Online Encyclopedia . Encyklopedie2.thefreedictionary.com.
  4. 1 2 Harold Abelson a Gerald Jay Sussman s Julií Sussmanovou. 1.1.5 Substituční model pro procedurální aplikaci // Struktura a interpretace počítačových programů. - druhé vydání. - Cambridge, Massachusetts, Londýn, Anglie: The MIT Press, 1996.
  5. Barbara Liskov , Russ Atkinson, Toby Bloom, Eliot Moss, Craig Schaffert, Craig Scheifler, Alan Snyder. Referenční příručka CLU  (anglicky)  (nedostupný odkaz) . Laboratoř informatiky . Massachusetts Institute of Technology (1979). Datum přístupu: 29. května 2014. Archivováno z originálu 22. září 2006.
  6. Fredrik Lundh. Call By Object  (anglicky)  (downlink) . effbot.org . Získáno 29. května 2014. Archivováno z originálu dne 23. listopadu 2019.
  7. Definice jazyka Iota . CS 412/413 Úvod do kompilátorů . Cornell University (2001). Získáno 29. 5. 2014. Archivováno z originálu 23. 9. 2015.
  8. CA1021: Vyhněte se parametrům
  9. Na rozdíl od C nejsou zápisy " l += x" a " l = l + x" v Pythonu ekvivalentní - první je sémanticky změna , nikoli přiřazení . Navíc „ l += x“ není syntaktický ekvivalent „ l.extend(x)“ kvůli pravidlům rozlišení viditelnosti : „ l += x“ vyžaduje, aby „ l“ bylo v místním rozsahu, zatímco „ l.extend(x)“ odpovídá i uzavřeným oborům.

Odkazy