Evalueringsstrategi - regler for programmeringssprogssemantik, der bestemmer, hvornår argumenterne for en funktion ( metode, operation, relation) skal evalueres, og hvilke værdier der skal bestå . For eksempel dikterer strategien call-by-worth/pass-by-reference , at argumenterne skal evalueres, før kroppen af den kaldte funktion udføres, og at den skal gives to muligheder for hvert argument: at læse den aktuelle værdi og ændre det med tildelingsoperatøren [1] . Denne strategi ligner reduktionsstrategien i lambda-regningen, men der er forskelle.
I praksis bunder beregningsmodellen for mange industrisprog ( Java , C# ) ned til en " opkald-at-omtale/pass-by-reference " -strategi . Nogle ældre sprog, især usikre, såsom C++ , kombinerer flere forskellige opkaldsmønstre. Historisk set går " kald efter værdi " og " kald ved navn " tilbage til Algol-60 , skabt i slutningen af 1950'erne . Kun rene funktionelle sprog som Clean og Haskell bruger " kald af nødvendighed ".
Bemærk - i den russisksprogede litteratur kaldes beregningsstrategien også " parameteroverførselsmetode ", " beregningsmodel " eller " opkaldsmodel ". Densidste mulighed kan forårsage forvirring med kaldekonventionen . Udtrykket " parameteroverførsel " er forkert for mange beregningsstrategier.
Den strenge evalueringsmodel betyder , at argumenter altid evalueres fuldt ud, før funktionen anvendes på dem.
I kirkens notation svarer ivrig evaluering af udsagn til streng evaluering af funktioner, og af denne grund kaldes streng evaluering undertiden " ivrig ". De fleste eksisterende sprog bruger streng evaluering af funktioner.
Applikativ rækkefølge , også " venstre-til-højre, indefra-ud ", ( inderst til venstre ) [2] [3] , betyder en beregningsstrategi , hvor AST -en nedefra og op evaluerer argumenter fra venstre mod højre i reducerede udtryk.
I modsætning til call by value reducerer den applikative evalueringsrækkefølge vilkårene i funktionslegemet så meget som muligt, før det anvendes.
For at betragte et eksempel på beregninger i den applikative rækkefølge, definerer vi flere funktioner [4] :
kvadrat(x) = x * x sum_of_squares(x, y) = square(x) + square(y) f(x) = sum_of_squares(x + 1, x * 2)Når vi beregner værdien af f(5), får vi følgende sæt substitutions:
f(5) = sum_of_squares(5 + 1, 5 * 2) = square(6) + square(10) = ((6 * 6) + (10 * 10)) = 36 + 100 = 136Call by value ( engelsk call-by-value ) er den mest udbredte beregningsstrategi, den kan ses på en række forskellige sprog, fra C til Scheme . Når det kaldes af værdi, evalueres argumentudtrykket, og den resulterende værdi associeres med den tilsvarende formelle funktionsparameter (normalt ved at kopiere denne værdi til en ny hukommelsesplacering). I dette tilfælde, hvis sproget tillader funktioner at tildele værdier til deres parametre, vil ændringerne kun påvirke disse lokale kopier, men værdierne, der er synlige på stedet for funktionskaldet, forbliver uændrede ved tilbagevenden.
Faktisk er kald efter værdi ikke et bestemt kaldsmønster, men en familie af mønstre, hvor argumenter evalueres, før de sendes til funktionslegemet. De fleste sprog ( Common Lisp , Eiffel , Java ), der bruger kald efter værdi, evaluerer funktionsargumenter fra venstre mod højre, men nogle evaluerer dem fra højre mod venstre, og nogle ( Scheme , OCaml , C ) angiver ikke rækkefølgen af evalueringen .
Skjulte begrænsningerI nogle tilfælde er udtrykket " call-by-value " ikke helt korrekt, da den passerede værdi ikke er værdien af variablen i sædvanlig forstand, men en reference til værdien, hvis implementering kan være anderledes. Som et resultat heraf kan kode, der syntaktisk ser ud som call-by-value, opføre sig som enten call -by-reference eller co-use , og programmets opførsel vil afhænge af subtile detaljer i sprogets semantik.
Grunden til at bruge call by reference er normalt fordi sproget ikke teknisk giver mulighed for at operere på komplekse data som en enkelt værdi - det repræsenterer det som en datastruktur, selvom det får det til at ligne en værdi i kilden. kode. Det kan være meget vanskeligt at bestemme den nøjagtige placering af linjen mellem en fuldgyldig værdi og datastrukturen. I C er en vektor (det vil sige en en-dimensional matrix , hvoraf en tegnstreng er et specialtilfælde) en datastruktur og behandles derfor som en reference til en hukommelsesplacering; dog er en struktur en værdi, selvom dens felter er vektorer. I Maple er en vektor et specialtilfælde af en tabel, og derfor en datastruktur; dog er en liste (som er bygget og indekseret på nøjagtig samme måde) en værdi. Tcl behandler værdier på to måder: værdirepræsentationen bruges på script-niveau, og sproget selv administrerer den passende datastruktur efter behov. Ændringer i datastrukturen afspejles i værdien og omvendt.
Forklaringen på, at sproget " overfører parametre efter værdi, hvor værdien er en reference " er ret almindelig (men bør ikke forveksles med call by reference); ellers kaldes det et sambrugsopkald . På grund af dette opfører call by value i Java og Visual Basic sig væsentligt anderledes end call by value i C og Pascal . I C eller Pascal vil overførsel af en massiv datastruktur til en funktion kopiere hele strukturen (medmindre argumentet faktisk er en reference til datastrukturen), hvilket potentielt reducerer ydeevnen betydeligt; dog vil ændringer i strukturens tilstand ikke være synlige i opkaldskonteksten. I Java og Visual Basic kopieres altid kun en reference til strukturen, hvilket er hurtigt, og strukturændringen vil være synlig på opkaldsstedet.
Når call-by-reference ( eng. call-by-reference ), eller passing-by-reference ( pass-by-reference ), modtager funktionen implicit en reference til den variabel, der bruges som argument, i stedet for en kopi af dens værdi.
Dette betyder normalt, at funktionen kan ændre (det vil sige ændre tilstanden af ) den variabel, der sendes som en parameter, og dette vil have en effekt i den kaldende kontekst. Derfor kan opkald ved reference bruges til at etablere en kommunikationskanal mellem den, der ringer og den, der ringer. Et sprog baseret direkte på opkald ved reference gør det vanskeligt for programmøren at spore alle virkningerne af et funktionskald, så det kan være buggy .
Mange sprog understøtter call-by-reference i en eller anden form, men få bruger det som standard, såsom Perl . En række sprog, såsom C++ , PHP , Visual Basic .NET , C# og REALbasic , bruger call by value som standard, men giver speciel syntaks for call ved reference. C++ introducerer desuden en unik call-by-reference-to- konstant -strategi .
Typesystemerne på nogle sprog, der bruger call by value og ikke direkte understøtter call by reference, giver mulighed for eksplicit at definere referencer (objekter, der henviser til andre objekter), især pointere (objekter, der er adresser til andre objekter i computeren ) hukommelse). Ved at bruge dem kan du simulere et opkald ved reference inde i call by value semantik. En sådan løsning bruges for eksempel i C- og ML-sprog . Det er ikke en selvstændig evalueringsstrategi - sproget kalder stadig efter værdi - men omtales nogle gange som " opkald-for-adresse " ( opkald-for-adresse ) eller " pass-by-adresse " ( pass-by-adresse ) . I usikre sprog, såsom C eller C++ , kan det føre til hukommelsesadgangsfejl , såsom nul pointer dereference , henholdsvis, hvilket gør det vanskeligt at forstå programmet og indledningsvis lære sproget. I ML er referencer type- safe og memory- safe .
En tæt effekt er også tilvejebragt af strategien " opkald ved co-brug " brugt i sprog som Java , Python , Ruby .
I rene funktionelle sprog er der ingen semantisk forskel mellem call by reference og call by value (fordi deres datastrukturer er uforanderlige, og en funktion har alligevel ingen mulighed for at ændre værdien af sine argumenter), så de beskrives normalt som call by value , selvom mange implementeringer faktisk bruger call by reference for at forbedre effektiviteten.
Følgende eksempel viser et simuleret opkald ved reference på E-sproget :
def modify( var p, &q) { p := 27 # parameter passeret af værdi - kun den lokale værdi ændres q := 27 # parameter videregivet ved reference - ændring af den variabel, der bruges i opkaldet } ? var a := 1 # værdi: 1 ? var b := 2 # værdi: 2 ? modificere(a, &b) ? -en # værdi: 1 ? b # værdi: 27Følgende eksempel demonstrerer simuleringen af et opkald ved reference på C-sproget . Heltalsvariabler og pointere sendes af værdi. Men da markøren indeholder adressen på den eksterne variabel, ændres dens værdi.
void Modify ( int p , int * q , int * o ) { // alle parametre passeret af værdien p = 27 ; // kun den lokale værdi ændres * q = 27 ; // ændrer den eksterne variabel, der peges på med q * o = 27 ; // skift ekstern variabel peget på af o } int main () { int a = 1 ; int b = 1 ; int x = 1 ; int * c = & x ; Ændre ( a , & b , c ); // 1. parameter - værdi af variabel a // 2. parameter - adresse på variabel b // 3. parameter - værdi af variabel c, som er adressen på variabel x // b og x ændres retur ( 0 ); }call-by-sharing eller call-with-ressource-sharing ( engelsk call-by-sharing ), også call-by-object ( call-by-object ), også call-by-object-sharing eller call-with-shared -objekt ( opkald-for-objekt-deling ), indebærer, at værdierne i sproget er baseret på objekter, og ikke på primitive typer , det vil sige " indpakket " ("packed", eng. boxed ). Når den kaldes af co-use , får funktionen en kopi af objektreferencen . Selve objektet kopieres ikke - det deles eller deles . Som en konsekvens heraf har en tildeling til et argument i en funktions brødtekst ingen effekt i den kaldende kontekst, men en tildeling til komponenterne i det argument gør det.
Sambrugsopkaldet blev første gang implementeret i CLU i 1974 under vejledning af Barbara Liskov og andre [5] .
Denne strategi bruges i Python [6] , Iota [7] , Java (til objektreferencer), Ruby , JavaScript , Scheme , Ocaml , AppleScript , og mange andre. Men terminologien i forskellige sprogsamfund er forskellig. For eksempel bruger Python-fællesskabet udtrykket "co-use call"; i Java- og Visual Basic -fællesskaberne beskrives den samme semantik ofte som " kald efter værdi, hvor 'værdi' er en objektreference "; i Ruby-fællesskabet siger de, at Ruby " bruger opkald ved reference " - på trods af at opkaldssemantikken på disse sprog er identisk.
For uforanderlige objekter er der ingen forskel mellem call-by-use og call-by-value , bortset fra at disse objekter er identiske . Brugen af et co-use-kald er et alternativ til input/output-parametre [8] - at ændre en parameter her betyder ikke, at der tildeles en parameter ; parameteren overskrives ikke , men skifter tilstand og bevarer sin identitet.
For eksempel, i Python er lister foranderlige objekter, så:
def f ( l ): l . tilføj ( 1 ) m = [] f ( m ) print m- vil udskrive " [1]", fordi argumentet " l" er blevet ændret.
Følgende eksempel viser forskellen mellem forandring og tildeling . Kode som denne:
def f ( l ): l += [ 1 ] m = [] f ( m ) print m- udskriver " [1]", da operatoren " l += [1]" opfører sig som " l.extend([1])"; men lignende kode:
def f ( l ): l = l + [ 1 ] m = [] f ( m ) print m- udskriver " []", fordi operatoren " l = l + [1]" opretter en ny lokal variabel i stedet for at ændre argumentet [9] .
Følgende programs opførsel demonstrerer semantikken af indrammede værdier og call-by-use:
x = [[]] * 4 x [ 0 ] . tilføje ( 'a' ) x [ 1 ] . tilføje ( 'b' ) x [ 2 ] . tilføj ( 'c' ) print ( x ) >> [[ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ]]Operatøren “ x = [[]] * 4” opretter en tom liste (lad os kalde det “ l”), og derefter en ny liste ( associeret med identifikatoren “ x”) med fire elementer, som hver er en reference til “ l”, det vil sige “ x = [ l, l, l, l ]". Efterfølgende kald til forskellige elementer på listen “ x” ændrer objektet “ l”. Det samme sker ved udskrivning af listen “ x”: da den består af fire referencer til “ l”, udskrives sammensætningen af “ l” fire gange.
call - by -copy -restore , også copy - in copy-out ( copy-in copy-out ), også call-by-value-in-result ( call-by-value-result ), eller call -by-value -return , som det kaldes i Fortran -sprogsamfundet , er et særligt tilfælde af call-by-reference , hvor den angivne reference er unik for den kaldende kontekst. Denne mulighed er interessant i sammenhæng med multiprocessorsystemer og fjernprocedurekald : hvis funktionsparameteren er et link, der kan tilgås af en anden eksekverende proces, så kan indholdet kopieres til et nyt link, der ikke længere vil være tilgængeligt; når funktionen vender tilbage, vil det ændrede indhold af dette nye link blive kopieret til det originale link ("gendannet").
Semantikken for call-by-copy-restore adskiller sig også fra call by reference, hvis to eller flere funktionsargumenter er aliaser af hinanden, dvs. peger på den samme variabel i den kaldende kontekst. I tilfælde af et opkald ved reference, vil ændring af den ene betyde ændring af den anden. Kopi-gendan-kaldet forhindrer dette ved at sende forskellige kopier til funktionen, men resultatet i opkaldskonteksten er udefineret, da det afhænger af om kopieringen er i samme retning (venstre-til-højre eller højre-til) -venstre) som før udfordring.
Hvis referencen videregives uinitialiseret, kan denne evalueringsstrategi kaldes call - by -result .
Ved delevaluering ( engelsk delevaluering ) kan der foretages beregninger i en uanvendt funktion. Alle underudtryk, der ikke indeholder ubundne variabler, evalueres, og anvendelser af funktioner med kendte argumenter reduceres. Når der er bivirkninger, kan fuld delvis evaluering give uønskede resultater, så systemer, der understøtter delvis evaluering, udfører dem kun for rene udtryk (udtryk uden bivirkninger) i funktioner.
Den ikke-strenge evalueringsmodel betyder , at argumenter ikke evalueres, før deres værdi er brugt i funktionslegemet.
Ikke-streng evaluering af funktioner svarer til doven evaluering af operatorer i kirkenotation , og derfor kaldes ikke-streng evaluering ofte " doven ".
På en række sprog ( C , C++ osv.) har boolske udtryk en ikke-streng vurderingsrækkefølge , som kaldes " kortslutningsevaluering " i den russisksprogede litteratur , hvor beregningerne stopper, så snart resultatet bliver utvetydigt forudsigeligt - for eksempel værdien " sand " i disjunktion, " falsk " i sammenhæng, og så videre. Filialoperatører har ofte også doven evalueringssemantik, det vil sige, at de returnerer resultatet af hele operatøren, så snart en enkelt-værdi gren genererer det.
Den normale evalueringsrækkefølge ( eng. Normal rækkefølge ; også " beregning fra venstre mod højre, udefra til inde ", yderst til venstre ) er en beregningsstrategi, hvor det omsluttende udtryk er fuldstændig reduceret, idet der anvendes funktioner, før argumenter evalueres.
I modsætning til den normale rækkefølge, evaluerer strategien kaldet ved navn ikke argumenter og udtryk i funktioner, der ikke kaldes.
For eksempel vil værdien f(5) for funktionen f defineret tidligere , når den evalueres i normal rækkefølge, give følgende sæt af substitutioner [4] :
f(5) = kvadratsum (5 + 1, 5 * 2) = kvadrat(5 + 1) + kvadrat(5 * 2) = ((5 + 1) * (5 + 1)) + (( 5 * 2) * (5 * 2)) = (6 * 6) + (10 * 10) = 36 + 100 = 136I en call-by-name- strategi evalueres argumenter ikke, før funktionen kaldes. I stedet erstattes de direkte i funktionens krop (ved hjælp af substitution, der forhindrer indfangning ), og evalueres derefter i stedet for kravet. Hvis et argument ikke bruges i funktionslegemet, evalueres det slet ikke; hvis den bruges flere gange, genberegnes den ved hver forekomst (se Jensens trick ).
Kald ved navn er nogle gange at foretrække frem for opkald efter værdi. Hvis argumentet ikke bruges i funktionens brødtekst, sparer det tid ved at kalde ved navn ved ikke at evaluere det, mens opkald med værdi betyder en uundgåelig evaluering. Hvis argumentet er en uafsluttende evaluering , er fordelen enorm. Men når et argument bruges, er det ofte langsommere at kalde ved navn, da det kræver oprettelse af en såkaldt " thunk ".
For første gang blev et opkald ved navn brugt på Algol-60- sproget . .NET -sprog kan simulere call-by-name ved hjælp af delegerede eller Expression<T>-parametre. I sidstnævnte tilfælde modtager funktionen en AST . Eiffel - sproget implementerer agenter, som er operationer, der udføres efter behov.
Call -by-need er en memoiseret call-by- name variant , hvor , hvis et argument evalueres , dets værdi gemmes til senere brug. I tilfælde af " sprogets renhed " (i fravær af bivirkninger ), giver dette det samme resultat som at kalde ved navn; og i tilfælde, hvor argumentet bruges to eller flere gange, er opkald af nødvendighed næsten altid hurtigere.
Fordi evaluerede udtryk kan være meget dybt indlejrede, understøtter call-by-need-sprog normalt ikke bivirkninger (såsom tilstandsændringer ) direkte og skal emuleres med monader (som i Haskell ) eller unikke typer som i Clean sprog ). Dette eliminerer enhver uforudsigelig adfærd ved doven evaluering, når variable værdier ændres, før de bruges.
Den mest almindelige implementering af call-of-need-semantik er doven evaluering , selvom der er andre variationer, såsom optimistisk evaluering .
Haskell er det mest berømte sprog, der bruger call-by-need. R bruger også en slags call-by-need. .NET -sprog kan simulere et opkald efter behov ved hjælp af Lazy<T>.
Call - by - macro-expansion svarer til call-by-name, men bruger tekstsubstitution i stedet for non-capturing substitution. Hvis den bruges skødesløst, kan makrosubstitution føre til variabel optagelse og uønsket programadfærd. Hygiejniske makroer eliminerer dette problem ved at kontrollere og om nødvendigt erstatte skyggede ikke-parametervariabler.
I fuld β-reduktion kan enhver anvendelse af en funktion reduceres (ved at substituere argumentet i funktionens krop ved at bruge substitution for at forhindre indfangning af til enhver tid. Dette kan gøres selv i en ikke-anvendt funktions brødtekst .
Call by future eller parallel call- by -name er en parallel evalueringsstrategi: værdier fremtidige udtryk evalueres parallelt med resten af programmet. På steder, hvor der kræves en formålsværdi, blokerer hovedprogrammet, indtil beregningen er afsluttet, hvis den endnu ikke er afsluttet.
Denne strategi er ikke-deterministisk, da beregninger kan udføres på et hvilket som helst tidspunkt mellem det øjeblik, hensigten skabes (hvor udtrykket er givet) og det øjeblik, dets værdi bruges. Det ligner call-by-need ved, at værdien kun evalueres én gang, og evalueringen kan udskydes, indtil værdien faktisk er nødvendig, men kan starte tidligere. Desuden, hvis destinationsværdien ikke længere er påkrævet (f.eks. blev en lokal variabel i funktionslegemet evalueret og funktionen afsluttet), kan evalueringen blive afbrudt.
Hvis mål implementeres gennem processer og tråde, afføder oprettelse af et mål i kode en ny proces eller tråd, adgang til en værdi synkroniserer den med hovedtråden, og fuldførelse af en målevaluering betyder at dræbe den proces, der beregnede dens værdi.
Optimistisk evaluering er en anden variant af call-by-need, hvor funktionsargumentet evalueres delvist i et vist tidsrum (som kan konfigureres under programafvikling), hvorefter beregningerne afbrydes og funktionen anvendes ved hjælp af en call- ved behov. Denne tilgang reducerer de tidsforsinkelser, der er iboende i doven evaluering, mens den giver de samme produktegenskaber.