Strategia de evaluare - reguli de semantică a limbajului de programare care determină când trebuie evaluate argumentele unei funcții ( metodă, operație, relație) și ce valori trebuie transmise . De exemplu, strategia call-by-worth/pass-by-reference dictează că argumentele trebuie evaluate înainte ca corpul funcției apelate să fie executat și că trebuie să i se ofere două posibilități pentru fiecare argument: citirea valorii curente și schimbându-l cu operatorul de atribuire [1] . Această strategie este similară cu strategia de reducere în calculul lambda, dar există diferențe.
În practică, modelul de calcul al multor limbaje industriale ( Java , C# ) se rezumă la o strategie „ call-at-mention/pass-by-reference ” . Unele limbi mai vechi, în special cele nesigure, cum ar fi C++ , combină mai multe modele de apelare diferite. Din punct de vedere istoric, „ apel după valoare ” și „ apel după nume ” se întorc la Algol-60 , creat la sfârșitul anilor 1950 . Numai limbaje funcționale pure , cum ar fi Clean și Haskell , folosesc „ apel prin necesitate ”.
Notă - în literatura în limba rusă, strategia de calcul este numită și „ metoda de trecere a parametrilor ”, „ model de calcul ” sau „ model de apelare ”. Ultimaopțiune poate provoca confuzie cu convenția de apelare . Termenul „ transmitere a parametrilor ” este incorect pentru multe strategii de calcul.
Modelul strict de evaluare înseamnă că argumentele sunt întotdeauna pe deplin evaluate înainte ca funcția să le fie aplicată.
În notația bisericească , evaluarea dornică a enunțurilor corespunde evaluării stricte pentru funcții, iar din acest motiv evaluarea strictă este uneori numită „ dornic ”. Majoritatea limbilor existente folosesc o evaluare strictă pentru funcții.
Ordinea aplicativă , de asemenea „ de la stânga la dreapta, dinăuntru în afară ”, ( stânga cel mai interior ) [2] [3] , înseamnă o strategie de calcul în care AST de jos în sus evaluează argumentele de la stânga la dreapta în expresii reduse.
Spre deosebire de apel după valoare, ordinea aplicativă a evaluării reduce termenii din corpul funcției cât mai mult posibil înainte de a fi aplicat.
Pentru a considera un exemplu de calcule în ordinea aplicativă, definim câteva funcții [4] :
pătrat(x) = x * x suma_pătratelor(x, y) = pătrat(x) + pătrat(y) f(x) = suma_pătratelor(x + 1, x * 2)Când se calculează valoarea lui f(5), obținem următorul set de substituții:
f(5) = suma_patratelor(5 + 1, 5 * 2) = pătrat(6) + pătrat(10) = ((6 * 6) + (10 * 10)) = 36 + 100 = 136Call by value ( în engleză call-by-value ) este cea mai utilizată strategie de calcul, poate fi văzută într-o varietate de limbi, de la C la Scheme . Când este apelată după valoare, expresia argument este evaluată și valoarea rezultată este asociată cu parametrul de funcție formală corespunzător (de obicei prin copierea acelei valori într-o nouă locație de memorie). În acest caz, dacă limba permite funcțiilor să atribuie valori parametrilor lor, atunci modificările vor afecta numai aceste copii locale, dar valorile vizibile la locul apelului funcției vor rămâne neschimbate la întoarcere.
De fapt, apelul după valoare nu este un model de apel anume, ci o familie de modele în care argumentele sunt evaluate înainte de a fi transmise corpului funcției. Majoritatea limbajelor ( Common Lisp , Eiffel , Java ) care folosesc call by value evaluează argumentele funcției de la stânga la dreapta, dar unele le evaluează de la dreapta la stânga, iar unele ( Scheme , OCaml , C ) nu specifică ordinea evaluării .
Restricții ascunseÎn unele cazuri, termenul „ call-by-value ” nu este chiar corect, deoarece valoarea transmisă nu este valoarea variabilei în sensul obișnuit, ci o referire la valoare, a cărei implementare poate fi diferită. Ca rezultat, codul care arată sintactic ca apel după valoare se poate comporta fie ca referință prin apelare, fie ca coutilizare , iar comportamentul programului va depinde de detaliile subtile ale semanticii limbajului.
Motivul utilizării apelului prin referință este, de obicei, deoarece limbajul nu oferă din punct de vedere tehnic capacitatea de a opera pe date complexe ca o singură valoare - o reprezintă ca o structură de date, chiar dacă o face să semene foarte mult cu o valoare în sursă. cod. Determinarea locației exacte a liniei dintre o valoare cu drepturi depline și structura de date mascată deoarece poate fi foarte dificilă. În C, un vector (adică o matrice unidimensională , din care un șir de caractere este un caz special) este o structură de date și, prin urmare, este tratată ca referință la o locație de memorie; cu toate acestea , o structură este o valoare chiar dacă câmpurile sale sunt vectori. În Maple , un vector este un caz special al unui tabel și, prin urmare, o structură de date; totuși, o listă (care este construită și indexată exact în același mod) este o valoare. Tcl tratează valorile în două moduri: reprezentarea valorii este utilizată la nivel de script, iar limbajul însuși gestionează structura de date adecvată după cum este necesar. Modificările aduse structurii datelor sunt reflectate în valoare și invers.
Explicația că limbajul „ transmite parametrii după valoare, unde valoarea este o referință ” este destul de comună (dar nu trebuie confundată cu apelul prin referință); altfel se numește apel de co-utilizare . Din acest motiv, apelul după valoare în Java și Visual Basic se comportă semnificativ diferit față de apelul după valoare în C și Pascal . În C sau Pascal, trecerea unei structuri de date masive unei funcții va copia întreaga structură (cu excepția cazului în care argumentul este de fapt o referire la structura de date), potențial reducând performanța semnificativ; cu toate acestea, modificările aduse stării structurii nu vor fi vizibile în contextul apelării. În Java și Visual Basic, numai o referință la structură este întotdeauna copiată, ceea ce este rapid, iar modificarea structurii va fi vizibilă pe site-ul apelului.
Când apelată prin referință ( eng. call-by-reference ) sau trecerea prin referință ( pass-by-reference ), funcția primește implicit o referință la variabila folosită ca argument, în loc de o copie a acesteia. valoare.
Acest lucru înseamnă de obicei că funcția poate modifica (adică schimba starea ) variabila transmisă ca parametru, iar acest lucru va avea efect în contextul apelării. Prin urmare, apelul prin referință poate fi utilizat pentru a stabili un canal de comunicare între apelat și apelant. Un limbaj bazat direct pe apel prin referință face dificil pentru programator să urmărească toate efectele unui apel de funcție, astfel încât acesta poate avea erori .
Multe limbi acceptă apelul prin referință într-o formă sau alta, dar puține îl folosesc implicit, cum ar fi Perl . Un număr de limbi, cum ar fi C++ , PHP , Visual Basic .NET , C# și REALbasic , folosesc call prin valoare în mod implicit, dar oferă o sintaxă specială pentru apel prin referință. C++ introduce în plus o strategie unică de apel prin referință la constantă .
Sistemele de tip ale unor limbi care folosesc apelul după valoare și nu acceptă direct apelul prin referință oferă capacitatea de a defini în mod explicit referințe (obiecte care se referă la alte obiecte), în special pointeri (obiecte care sunt adrese ale altor obiecte din computer). memorie). Folosirea acestora vă permite să simulați un apel prin referință în interiorul semanticii apel după valoare. O astfel de soluție este folosită, de exemplu, în limbajele C și ML . Nu este o strategie de evaluare de sine stătătoare - limbajul apelează în continuare după valoare - dar uneori este denumită „ call-by-address ” ( call-by-address ) sau „ pass-by-address ” ( pass-by-address ) . În limbajele nesigure, cum ar fi C sau C++ , poate duce la erori de acces la memorie , cum ar fi , respectiv, null pointer dereference , ceea ce face dificilă înțelegerea programului și învățarea inițială a limbajului. În ML , referințele sunt tip -safe și memory -safe .
Un efect apropiat este oferit și de strategia „ call by co-use ” utilizată în limbaje precum Java , Python , Ruby .
În limbajele funcționale pure, nu există nicio diferență semantică între apelul prin referință și apelul după valoare (deoarece structurile lor de date sunt imuabile și o funcție oricum nu are nicio modalitate de a modifica valoarea argumentelor sale), așa că sunt de obicei descrise ca apel după valoare. , chiar dacă multe implementări folosesc de fapt apelul prin referință pentru a îmbunătăți eficiența.
Următorul exemplu demonstrează un apel simulat prin referință în limbajul E :
def modify( var p, &q ) { p := 27 # parametru trecut prin valoare - se modifică doar valoarea locală q := 27 # parametru trecut prin referință - schimbarea variabilei utilizate în apel } ? var a := 1 # valoare: 1 ? var b := 2 # valoare: 2 ? modifica(a, &b) ? A # valoare: 1 ? b # valoare: 27Următorul exemplu demonstrează simularea unui apel prin referință în limbajul C. Variabilele de tip întreg și pointerii sunt transmise după valoare. Dar, deoarece pointerul conține adresa variabilei externe, valoarea acesteia se va schimba.
void Modificare ( int p , int * q , int * o ) { // toți parametrii trecuți prin valoarea p = 27 ; // se modifică doar valoarea locală * q = 27 ; // modifică variabila externă indicată de q * o = 27 ; // schimbă variabila externă indicată de o } int main () { int a = 1 ; int b = 1 ; int x = 1 ; int * c = & x ; Modificați ( a , & b , c ); // Primul parametru - valoarea variabilei a // Al doilea parametru - adresa variabilei b // Al treilea parametru - valoarea variabilei c, care este adresa variabilei x // b și x sunt modificate return ( 0 ); }partajare apel-cu -partajare sau apel-cu-resurse-sharing ( în engleză call-by-sharing ), de asemenea, apel-cu-obiect ( apel-cu-obiect ), de asemenea, apel-cu-obiect-sharing sau apel-cu-partajat -object ( call-by-object-sharing ), implică faptul că valorile din limbaj se bazează pe obiecte, și nu pe tipuri primitive , adică „ wrapped ” („ambalat”, ing. boxed ). Când este apelată prin co-utilizare, funcția primește o copie a referinței obiectului . Obiectul în sine nu este copiat - este partajat sau partajat . În consecință, o atribuire la un argument din corpul unei funcții nu are efect în contextul de apelare, dar o atribuire la componentele acelui argument are.
Apelul de co-utilizare a fost implementat pentru prima dată în CLU în 1974 sub îndrumarea lui Barbara Liskov și alții [5] .
Această strategie este folosită în Python [6] , Iota [7] , Java (pentru referințe la obiecte), Ruby , JavaScript , Scheme , Ocaml , AppleScript și multe altele. Cu toate acestea, terminologia din diferitele comunități lingvistice diferă. De exemplu, comunitatea Python folosește termenul „co-use call”; în comunitățile Java și Visual Basic , aceeași semantică este adesea descrisă ca „ apel după valoare, unde „valoare” este o referință la obiect ”; în comunitatea Ruby se spune că Ruby „ folosește apel prin referință ” - în ciuda faptului că semantica apelului în aceste limbi este identică.
Pentru obiectele imuabile, nu există nicio diferență între apel după utilizare și apel după valoare, cu excepția faptului că aceste obiecte sunt identice . Utilizarea unui apel de co-utilizare este o alternativă la parametrii de intrare/ieșire [8] - modificarea unui parametru aici nu înseamnă alocarea unui parametru ; parametrul nu este suprascris , ci își schimbă starea , păstrându-și identitatea.
De exemplu, în Python , listele sunt obiecte mutabile, deci:
def f ( l ): l . anexează ( 1 ) m = [] f ( m ) imprimă m- va tipări „ [1]„, deoarece argumentul „ l„ a fost schimbat.
Următorul exemplu demonstrează diferența dintre modificare și atribuire . Cod astfel:
def f ( l ): l += [ 1 ] m = [] f ( m ) print m- afișează " [1]", deoarece operatorul " l += [1]" se comportă ca " l.extend([1])"; dar cod similar:
def f ( l ): l = l + [ 1 ] m = [] f ( m ) print m- afișează " []", deoarece operatorul " l = l + [1]" creează o nouă variabilă locală, în loc să schimbe argumentul [9] .
Comportamentul următorului program demonstrează semantica valorilor în casete și a apelului prin utilizare:
x = [[]] * 4 x [ 0 ] . anexează ( 'a' ) x [ 1 ] . anexează ( 'b' ) x [ 2 ] . adăugați ( 'c' ) imprimați ( x ) >> [[ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ]]Operatorul „ x = [[]] * 4” creează o listă goală (să o numim „ l”), apoi o nouă listă ( asociată cu identificatorul „ x”) de patru elemente, fiecare dintre acestea fiind o referință la „ l”, adică „ x = [ l, l, l, l ]”. Apelurile ulterioare la diferite elemente ale listei „ x” schimbă obiectul „ l”. Același lucru se întâmplă și la tipărirea listei „ x”: deoarece constă din patru referințe la „ l”, atunci compoziția lui „ l” este tipărită de patru ori.
call - by -copy -restore , de asemenea , copy - in copy-out ( copy-in copy-out ), de asemenea call-by-value-in-result ( call-by-value-result ) sau call -by-value -return , așa cum este numit în comunitatea lingvistică Fortran , este un caz special de call-by-reference , în care referința furnizată este unică pentru contextul apelului. Această opțiune este interesantă în contextul sistemelor multiprocesor și al apelurilor de procedură la distanță : dacă parametrul funcției este o legătură care poate fi accesată de un alt proces în execuție, atunci conținutul său poate fi copiat într-o nouă legătură care nu va mai fi disponibilă; când funcția revine, conținutul modificat al acestui nou link va fi copiat pe linkul original („restaurat”).
Semantica call-by-copy-restore diferă, de asemenea, de apel prin referință dacă două sau mai multe argumente ale funcției sunt aliasuri unul de celălalt, adică indică aceeași variabilă în contextul apelării. În cazul unui apel prin referință, schimbarea unuia va însemna schimbarea celuilalt. Apelul de copiere-restaurare previne acest lucru prin transmiterea de copii diferite către funcție, dar rezultatul în contextul de apelare este nedefinit, deoarece depinde dacă copierea înapoi este în aceeași direcție (de la stânga la dreapta sau de la dreapta la -stânga) ca înainte de provocare.
Dacă referința este trecută neinițializată, această strategie de evaluare poate fi numită call - by - result .
Cu evaluarea parțială ( evaluare parțială în engleză ) se pot face calcule într-o funcție neaplicată. Orice subexpresii care nu conțin variabile nelegate sunt evaluate, iar aplicațiile funcțiilor cu argumente cunoscute sunt reduse. Când există efecte secundare, evaluarea completă parțială poate produce rezultate nedorite, astfel încât sistemele care acceptă evaluarea parțială le efectuează doar pentru expresii pure (expresii fără efecte secundare) în funcții.
Modelul de evaluare non-strict înseamnă că argumentele nu sunt evaluate până când valoarea lor este utilizată în corpul funcției.
Evaluarea nestrictă a funcțiilor corespunde evaluării leneșe a operatorilor în notația bisericească , și, prin urmare, evaluarea nestrictă este adesea numită " leneș ".
Într-o serie de limbi ( C , C++ , etc.), expresiile booleene au o ordine de evaluare nestrictă, care se numește „ evaluare în scurtcircuit ” în literatura în limba rusă , unde calculele se opresc de îndată ce rezultatul devine fără ambiguitate previzibil - de exemplu, valoarea „ adevărat ” în disjuncție, „ fals ” în conjuncție și așa mai departe. Operatorii de ramură au adesea și o semantică de evaluare leneșă, adică returnează rezultatul întregului operator imediat ce o ramură cu o singură valoare îl generează.
Ordinea normală de evaluare ( ing. Ordine normală ; de asemenea " calcul de la stânga la dreapta, de la exterior la interior ", cea de la stânga la exterior ) este o strategie de calcul în care expresia încadrată este complet redusă, aplicând funcții înainte de evaluarea argumentelor.
Spre deosebire de ordinea normală, strategia de apelare după nume nu evaluează argumentele și expresiile din cadrul funcțiilor care nu sunt apelate.
De exemplu, valoarea f(5) pentru funcția f definită mai devreme , atunci când este evaluată în ordine normală, va da următorul set de substituții [4] :
f(5) = suma pătratelor (5 + 1, 5 * 2) = pătrat(5 + 1) + pătrat(5 * 2) = ((5 + 1) * (5 + 1)) + (( 5 * 2) * (5 * 2)) = (6 * 6) + (10 * 10) = 36 + 100 = 136Într-o strategie de apelare după nume , argumentele nu sunt evaluate înainte ca funcția să fie apelată. În schimb, ele sunt substituite direct în corpul funcției (folosind substituția care împiedică capturarea ) și apoi evaluate în locul cerinței. Dacă un argument nu este folosit în corpul funcției, acesta nu este evaluat deloc; dacă este folosit de mai multe ori, este recalculat la fiecare apariție (vezi trucul lui Jensen ).
Apelul după nume este uneori preferabil apelului după valoare. Dacă argumentul nu este folosit în corpul funcției, apelarea după nume economisește timp prin neevaluarea lui, în timp ce apelarea după valoare înseamnă o evaluare inevitabilă. Dacă argumentul este o evaluare neterminabilă , beneficiul este uriaș. Cu toate acestea, atunci când se folosește un argument, apelarea după nume este adesea mai lentă, deoarece necesită crearea unui așa-numit „ thunk ”.
Pentru prima dată, un apel după nume a fost folosit în limba Algol-60 . Limbile .NET pot simula apelul după nume folosind delegați sau Expression<T>parametrii. În acest din urmă caz, funcția primește un AST . Limbajul Eiffel implementează agenți, care sunt operațiuni efectuate la cerere.
Call -by-need este o variantă de apel-by- name memorată în care , dacă un argument este evaluat , valoarea acestuia este stocată pentru o utilizare ulterioară. În cazul „ purității limbajului ” (în absența efectelor secundare ), aceasta produce același rezultat ca și chemarea după nume; iar în cazurile în care argumentul este folosit de două sau de mai multe ori, apelarea prin necesitate este aproape întotdeauna mai rapidă.
Deoarece expresiile evaluate pot fi imbricate foarte profund, limbajele de apelare după necesitate nu acceptă, de obicei , efecte secundare (cum ar fi schimbările de stare ) în mod direct și trebuie emulate cu monade (ca în Haskell ) sau tipuri unice ca în Clean . limba ). Acest lucru elimină orice comportament imprevizibil al evaluării leneșe atunci când valorile variabilelor sunt modificate înainte de a fi utilizate.
Cea mai comună implementare a semanticii call-of-need este evaluarea leneșă , deși există și alte variații, cum ar fi evaluarea optimistă .
Haskell este cea mai faimoasă limbă care folosește call-by-need. R folosește, de asemenea, un fel de call-by-need. Limbile .NET pot simula un apel după cum este necesar folosind Lazy<T>.
Extinderea apel- by - macro este similară cu apel-by-name, dar folosește substituția textuală în loc de substituția fără captură. Dacă este folosită neglijent, înlocuirea macro poate duce la captarea variabilelor și la un comportament nedorit al programului. Macro- urile igienice elimină această problemă prin verificarea și, dacă este necesar, înlocuirea variabilelor non-parametrice umbrite.
În β-reducere completă, orice aplicare a unei funcții poate fi redusă (prin înlocuirea argumentului în corpul funcției, folosind substituția pentru a preveni capturarea în orice moment. Acest lucru se poate face chiar și în corpul unei funcții neaplicate .
Apel după viitor sau apel în paralel cu nume este o strategie de evaluare paralelă : valorile viitoare sunt evaluate în paralel cu restul programului. În locurile în care este necesară o valoare a scopului, programul principal se blochează până când calculul este finalizat, dacă nu a fost încă finalizat.
Această strategie este nedeterministă, deoarece calculele pot fi efectuate în orice moment între momentul creării intenției (unde este dată expresia) și momentul în care valoarea acesteia este utilizată. Este similar cu call-by-need prin faptul că valoarea este evaluată o singură dată, iar evaluarea poate fi amânată până când valoarea este de fapt necesară, dar poate începe mai devreme. Mai mult, dacă valoarea destinației nu mai este necesară (de exemplu, o variabilă locală din corpul funcției a fost evaluată și funcția sa încheiat), evaluarea poate fi întreruptă.
Dacă țintele sunt implementate prin procese și fire de execuție, atunci crearea unei ținte în cod generează un nou proces sau fir de execuție, accesarea unei valori o sincronizează cu firul principal, iar finalizarea evaluării țintei înseamnă uciderea procesului care i-a calculat valoarea.
Evaluarea optimistă este o altă variantă a call-by-need, în care argumentul funcției este parțial evaluat pentru o anumită perioadă de timp alocată (care poate fi configurată în timpul execuției programului), după care calculele sunt întrerupte și funcția este aplicată folosind un call- prin nevoie. Această abordare reduce întârzierile de timp inerente evaluării leneșe , oferind în același timp aceleași caracteristici ale produsului.