Utvärderingsstrategi - regler för programmeringsspråkssemantik som bestämmer när argumenten för en funktion ( metod, operation, relation) ska utvärderas och vilka värden som ska passeras . Till exempel dikterar strategin call-by-worth/pass-by-reference att argumenten måste utvärderas innan kroppen av den anropade funktionen exekveras, och att den måste ges två möjligheter för varje argument: läsning av det aktuella värdet och ändra det med uppdragsoperatören [1] . Denna strategi liknar reduktionsstrategin i lambdakalkylen, men det finns skillnader.
I praktiken kokar beräkningsmodellen för många industrispråk ( Java , C# ) ner till en strategi för " samtal-at-nämna/pass-by-referens " . Vissa äldre språk, särskilt osäkra som C++ , kombinerar flera olika anropsmönster. Historiskt sett går " anrop efter värde " och " samtal efter namn " tillbaka till Algol-60 , skapad i slutet av 1950 -talet . Endast rena funktionella språk som Clean och Haskell använder " kalla av nödvändighet ".
Notera - i den ryskspråkiga litteraturen kallas beräkningsstrategin även " parameterpasseringsmetod ", " beräkningsmodell " eller " anropsmodell ". Detsista alternativet kan orsaka förvirring med anropskonventionen . Termen " parameterpassering " är felaktig för många beräkningsstrategier.
Den strikta utvärderingsmodellen innebär att argument alltid utvärderas fullt ut innan funktionen appliceras på dem.
I kyrkans notation motsvarar ivriga utvärdering av uttalanden strikt utvärdering för funktioner, och av denna anledning kallas strikt utvärdering ibland " ivrig ". De flesta befintliga språk använder strikt utvärdering för funktioner.
Tillämplig ordning , även " vänster-till-höger, inifrån och ut ", ( längst till vänster ) [2] [3] , betyder en beräkningsstrategi där AST :en nedifrån och upp utvärderar argument från vänster till höger i reducerade uttryck.
Till skillnad från call by value reducerar den applicerande utvärderingsordningen termerna i funktionskroppen så mycket som möjligt innan den tillämpas.
För att betrakta ett exempel på beräkningar i applikativ ordning, definierar vi flera funktioner [4] :
kvadrat(x) = x * x summa_of_squares(x, y) = kvadrat(x) + kvadrat(y) f(x) = summa_of_squares(x + 1, x * 2)När vi beräknar värdet på f(5) får vi följande uppsättning substitutioner:
f(5) = summa_of_squares(5 + 1, 5 * 2) = square(6) + square(10) = ((6 * 6) + (10 * 10)) = 36 + 100 = 136Call by value ( engelska call-by-value ) är den mest använda beräkningsstrategin, den kan ses på en mängd olika språk, från C till Scheme . När det anropas av värde, utvärderas argumentuttrycket och det resulterande värdet associeras med motsvarande formella funktionsparameter (vanligtvis genom att kopiera det värdet till en ny minnesplats). I det här fallet, om språket tillåter funktioner att tilldela värden till sina parametrar, kommer ändringarna endast att påverka dessa lokala kopior, men de värden som är synliga på platsen för funktionsanropet kommer att förbli oförändrade vid återkomst.
I själva verket är call by value inte ett särskilt anropsmönster, utan en familj av mönster där argument utvärderas innan de skickas till funktionskroppen. De flesta språk ( Common Lisp , Eiffel , Java ) som använder call by value utvärderar funktionsargument från vänster till höger, men vissa utvärderar dem från höger till vänster och vissa ( Scheme , OCaml , C ) anger inte utvärderingsordningen .
Dolda begränsningarI vissa fall är termen " call-by-value " inte helt korrekt, eftersom det passerade värdet inte är variabelns värde i vanlig mening, utan en referens till värdet, vars implementering kan vara annorlunda. Som ett resultat kan kod som syntaktiskt ser ut som call-by-value bete sig som antingen call -by-reference eller co-use , och programmets beteende kommer att bero på subtila detaljer i språkets semantik.
Anledningen till att använda anrop genom referens är vanligtvis för att språket inte tekniskt ger möjligheten att arbeta på komplexa data som ett enda värde - det representerar det som en datastruktur, även om det får det att likna ett värde i källan. koda. Att bestämma den exakta platsen för linjen mellan ett fullvärdigt värde och datastrukturen som maskerar sig som det kan vara mycket svårt. I C är en vektor (det vill säga en endimensionell array , av vilken en teckensträng är ett specialfall) en datastruktur och behandlas därför som en referens till en minnesplats; dock är en struktur ett värde även om dess fält är vektorer. I Maple är en vektor ett specialfall av en tabell, och därför en datastruktur; dock är en lista (som är byggd och indexerad på exakt samma sätt) ett värde. Tcl behandlar värden på två sätt: värderepresentationen används på skriptnivå, och språket själv hanterar lämplig datastruktur efter behov. Ändringar i datastrukturen återspeglas i värdet och vice versa.
Förklaringen att språket " skickar parametrar efter värde, där värdet är en referens " är ganska vanlig (men bör inte förväxlas med call by reference); annars kallas det sambrukssamtal . På grund av detta beter call by value i Java och Visual Basic betydligt annorlunda än call by value i C och Pascal . I C eller Pascal kommer att överföra en massiv datastruktur till en funktion kopiera hela strukturen (såvida inte argumentet faktiskt är en referens till datastrukturen), vilket potentiellt minskar prestandan avsevärt; Ändringar av strukturens tillstånd kommer dock inte att vara synliga i anropssammanhanget. I Java och Visual Basic kopieras alltid endast en referens till strukturen, vilket är snabbt, och strukturändringen kommer att synas på anropsplatsen.
När call-by -reference ( sv. call-by-reference ), eller passing-by-reference ( pass-by-reference ), får funktionen implicit en referens till variabeln som används som argument, istället för en kopia av dess värde.
Detta innebär vanligtvis att funktionen kan modifiera (det vill säga ändra tillståndet för ) variabeln som skickas som en parameter, och detta kommer att ha en effekt i anropssammanhanget. Därför kan samtal genom referens användas för att upprätta en kommunikationskanal mellan den som ringer och den som ringer. Ett språk baserat direkt på anrop genom referens gör det svårt för programmeraren att spåra alla effekter av ett funktionsanrop, så det kan vara buggy .
Många språk stöder call-by-referens i en eller annan form, men få använder det som standard, som Perl . Ett antal språk, som C++ , PHP , Visual Basic .NET , C# och REALbasic , använder call by value som standard, men tillhandahåller speciell syntax för call genom referens. C++ introducerar dessutom en unik call-by-reference-to- konstant -strategi .
Typsystemen för vissa språk som använder call by value och som inte direkt stöder call by reference ger möjlighet att explicit definiera referenser (objekt som hänvisar till andra objekt), i synnerhet pekare (objekt som är adresser till andra objekt i datorn ) minne). Genom att använda dem kan du simulera ett samtal genom referens inuti call by value-semantik. En sådan lösning används till exempel i C- och ML-språk . Det är inte en fristående utvärderingsstrategi - språket ringer fortfarande efter värde - utan kallas ibland för " samtal-för-adress " ( samtal-för-adress ) eller " pass-för-adress " ( pass-by-adress ) . I osäkra språk, som C eller C++ , kan det leda till minnesåtkomstfel , såsom noll pointer dereference , respektive, vilket gör det svårt att förstå programmet och initialt lära sig språket. I ML är referenser typ -säker och minnessäker .
En nära effekt tillhandahålls också av strategin " anrop genom samanvändning " som används i språk som Java , Python , Ruby .
I rena funktionella språk finns det ingen semantisk skillnad mellan call by reference och call by value (eftersom deras datastrukturer är oföränderliga och en funktion inte har något sätt att ändra värdet på sina argument ändå), så de beskrivs vanligtvis som call by value , även om många implementeringar faktiskt använder anrop genom referens för att förbättra effektiviteten.
Följande exempel visar ett simulerat anrop genom referens på E-språket :
def modify( var p, &q) { p := 27 # parameter passerad av värde - endast det lokala värdet ändras q := 27 # parameter skickad genom referens - ändra variabeln som används i anropet } ? var a := 1 # värde: 1 ? var b := 2 # värde: 2 ? modifiera(a, &b) ? a # värde: 1 ? b # värde: 27Följande exempel visar simuleringen av ett anrop genom referens på C-språket . Variabler och pekare av heltalstyp skickas av värde. Men eftersom pekaren innehåller adressen till den externa variabeln kommer dess värde att ändras.
void Ändra ( int p , int * q , int * o ) { // alla parametrar som skickas av värdet p = 27 ; // endast det lokala värdet ändras * q = 27 ; // ändrar den externa variabeln som pekas på av q * o = 27 ; // ändra extern variabel pekade på av o } int main () { int a = 1 ; int b = 1 ; int x = 1 ; int * c = & x ; Ändra ( a , & b , c ); // 1:a parametern - värdet av variabel a // 2:a parametern - adressen till variabeln b // 3:e parametern - värdet på variabeln c, som är adressen till variabeln x // b och x ändras retur ( 0 ); }call-by-sharing eller call-with-resurs-sharing ( engelska call-by-sharing ), även call-by-object ( call-by-object ), även call-by-object-sharing eller call-with- shared -object ( call-by-object-sharing ), antyder att värdena i språket är baserade på objekt, och inte på primitiva typer , det vill säga " wrapped " ("packad", eng. boxed ). När den anropas av samanvändning får funktionen en kopia av objektreferensen . Själva objektet kopieras inte - det delas eller delas . Som en konsekvens har en tilldelning till ett argument i en funktions brödtext ingen effekt i anropskontexten, men en tilldelning till komponenterna i det argumentet gör det.
Samanvändningen implementerades först i CLU 1974 under ledning av Barbara Liskov och andra [5] .
Denna strategi används i Python [6] , Iota [7] , Java (för objektreferenser), Ruby , JavaScript , Scheme , Ocaml , AppleScript , och många andra. Terminologin i olika språkgemenskaper skiljer sig dock åt. Till exempel använder Python-communityt termen "samanvändning"; i Java- och Visual Basic -gemenskaperna beskrivs samma semantik ofta som " anrop för värde, där "värde" är en objektreferens "; i Ruby-communityt säger de att Ruby " använder call by reference " - trots att samtalssemantiken på dessa språk är identisk.
För oföränderliga objekt finns det ingen skillnad mellan call-by-use och call-by-value förutom att dessa objekt är identiska . Användningen av ett sambruksanrop är ett alternativ till in-/utdataparametrar [8] - att ändra en parameter här betyder inte att tilldela en parameter ; parametern skrivs inte över utan ändrar tillstånd och behåller sin identitet.
Till exempel, i Python är listor föränderliga objekt, så:
def f ( l ): l . lägg till ( 1 ) m = [] f ( m ) skriv ut m- kommer att skriva ut " [1]", eftersom argumentet " l" har ändrats.
Följande exempel visar skillnaden mellan förändring och tilldelning . Kod så här:
def f ( l ): l += [ 1 ] m = [] f ( m ) skriv ut m- skriver ut " [1]", eftersom operatorn " l += [1]" beter sig som " l.extend([1])"; men liknande kod:
def f ( l ): l = l + [ 1 ] m = [] f ( m ) skriv ut m- skriver ut " []", eftersom operatorn " l = l + [1]" skapar en ny lokal variabel istället för att ändra argumentet [9] .
Beteendet för följande program visar semantiken för boxade värden och call-by-use:
x = [[]] * 4 x [ 0 ] . lägg till ( 'a' ) x [ 1 ] . lägg till ( 'b' ) x [ 2 ] . lägg till ( 'c' ) skriv ut ( x ) >> [[ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ]]Operatören “ x = [[]] * 4” skapar en tom lista (låt oss kalla den “ l”), och sedan en ny lista ( associerad med identifieraren “ x”) med fyra element, som vart och ett är en referens till “ l”, det vill säga “ x = [ l, l, l, l ]”. Efterföljande anrop till olika element i listan “ x” ändrar objektet “ l”. Samma sak händer när du skriver ut listan " x": eftersom den består av fyra referenser till " l" skrivs sammansättningen av " l" ut fyra gånger.
call - by -copy-restore , även copy - in copy-out ( copy-in copy-out ), även call-by-value-in-result ( call-by-value-result ), eller call -by-value -return , som det kallas i Fortran -språkgemenskapen , är ett specialfall av call-by-reference , där den angivna referensen är unik för anropssammanhanget. Det här alternativet är intressant i sammanhanget med flerprocessorsystem och fjärrproceduranrop : om funktionsparametern är en länk som kan nås av en annan exekveringsprocess, kan dess innehåll kopieras till en ny länk som inte längre kommer att vara tillgänglig; när funktionen återkommer kommer det ändrade innehållet i denna nya länk att kopieras till den ursprungliga länken ("återställt").
Semantiken för call-by-copy-restore skiljer sig också från call by reference om två eller flera funktionsargument är alias för varandra, dvs pekar på samma variabel i anropskontexten. I fallet med ett anrop genom referens innebär att ändra det ena att ändra det andra. Copy-restore-anropet förhindrar detta genom att skicka olika kopior till funktionen, men resultatet i anropssammanhanget är odefinierat, eftersom det beror på om kopieringen är i samma riktning (vänster-till-höger eller höger-till) -vänster) som tidigare utmaning.
Om referensen skickas oinitierad kan denna utvärderingsstrategi kallas call- by - result .
Med partiell utvärdering ( engelska partial evaluation ) kan beräkningar göras i en otillämpad funktion. Alla underuttryck som inte innehåller obundna variabler utvärderas och tillämpningar av funktioner med kända argument reduceras. När det finns biverkningar kan fullständig partiell utvärdering ge oönskade resultat, så system som stöder partiell utvärdering utför dem endast för rena uttryck (uttryck utan biverkningar) i funktioner.
Den icke-strikta utvärderingsmodellen innebär att argument inte utvärderas förrän deras värde används i funktionskroppen.
Icke-strikt utvärdering av funktioner motsvarar lat utvärdering av operatorer i kyrkans notation , och därför kallas icke-strikt utvärdering ofta " lat ".
På ett antal språk ( C , C++ , etc.) har booleska uttryck en icke-strikt utvärderingsordning, som kallas "kortslutningsutvärdering " i den ryskspråkiga litteraturen , där beräkningarna stoppas så snart resultatet blir otvetydigt förutsägbart - till exempel värdet " true " i disjunktion, " false " i konjunktion, och så vidare. Filialoperatörer har ofta också lat utvärderingssemantik, det vill säga de returnerar resultatet av hela operatören så fort en enkelvärdig gren genererar det.
Den normala utvärderingsordningen ( eng. Normal ordning ; även " beräkning från vänster till höger, från utsidan till insidan ", ytterst till vänster ) är en beräkningsstrategi där det omslutande uttrycket reduceras helt och tillämpar funktioner innan argument utvärderas.
Till skillnad från den normala ordningen utvärderar strategin call-by-name inte argument och uttryck inom funktioner som inte anropas.
Till exempel kommer värdet f(5) för funktionen f definierad tidigare , när det utvärderas i normal ordning, att ge följande uppsättning substitutioner [4] :
f(5) = summan av kvadrater (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 utvärderas inte argument innan funktionen anropas. Istället ersätts de direkt i funktionens kropp (med substitution som förhindrar att ) hämtas och utvärderas sedan i stället för kravet. Om ett argument inte används i funktionskroppen utvärderas det inte alls; om den används flera gånger räknas den om vid varje händelse (se Jensens trick ).
Call by name är ibland att föredra framför call by value. Om argumentet inte används i funktionens brödtext sparar anrop med namn tid genom att inte utvärdera det, medan anrop med värde innebär en oundviklig utvärdering. Om argumentet är en icke-avslutande utvärdering , är fördelen enorm. Men när ett argument används går det ofta långsammare att anropa med namn, eftersom det kräver att man skapar en så kallad " thunk ".
För första gången användes ett namnupprop på Algol-60- språket . .NET -språk kan simulera call-by-name med hjälp av delegater eller Expression<T>-parametrar. I det senare fallet får funktionen en AST . Eiffelspråket implementerar agenter, som är operationer som utförs på begäran.
Call -by-need är en memoiserad call-by- name - variant där , om ett argument utvärderas , dess värde lagras för senare användning. När det gäller " språkets renhet " (i avsaknad av biverkningar ) ger detta samma resultat som att ringa vid namn; och i de fall där argumentet används två eller flera gånger, är anrop av nödvändighet nästan alltid snabbare.
Eftersom utvärderade uttryck kan vara mycket djupt kapslade, stöder samtal-för-behov-språk vanligtvis inte bieffekter (som tillståndsändringar ) direkt, och måste emuleras med monader (som i Haskell ) eller unika typer som i Clean språk ). Detta eliminerar allt oförutsägbart beteende av lat utvärdering när variabelvärden ändras innan de används.
Den vanligaste implementeringen av call-of-need-semantik är lat utvärdering , även om det finns andra varianter, till exempel optimistisk utvärdering .
Haskell är det mest kända språket som använder call-by-need. R använder också ett slags call-by-need. .NET -språk kan simulera ett samtal efter behov med hjälp av Lazy<T>.
Call - by -macro-expansion liknar call-by-name, men använder textsubstitution istället för icke-fångande substitution. Om det används slarvigt kan makrosubstitution leda till variabel fångst och oönskat programbeteende. Hygieniska makron eliminerar detta problem genom att kontrollera och, om nödvändigt, ersätta skuggade icke-parametervariabler.
I full β-reduktion kan varje tillämpning av en funktion reduceras (genom att ersätta argumentet i funktionens brödtext, använda substitution för att förhindra att när som helst. Detta kan göras även i kroppen av en otillämpad funktion .
Call by future eller parallell call- by -name är en parallell utvärderingsstrategi: värden framtida uttryck utvärderas parallellt med resten av programmet. På platser där ett ändamålsvärde krävs blockeras huvudprogrammet tills beräkningen är klar, om den ännu inte har slutförts.
Denna strategi är icke-deterministisk, eftersom beräkningar kan utföras när som helst mellan det ögonblick då avsikten skapas (där uttrycket ges) och det ögonblick som dess värde används. Det liknar call-by-need genom att värdet endast utvärderas en gång, och utvärderingen kan skjutas upp tills värdet faktiskt behövs, men kan starta tidigare. Dessutom, om destinationsvärdet inte längre krävs (till exempel en lokal variabel i funktionskroppen utvärderades och funktionen avslutades), kan utvärderingen avbrytas.
Om mål implementeras genom processer och trådar, skapar ett mål i kod en ny process eller tråd, åtkomst till ett värde synkroniserar det med huvudtråden, och att slutföra en målutvärdering innebär att den process som beräknade dess värde dödas.
Optimistisk utvärdering är en annan variant av call-by-need, där funktionsargumentet delvis utvärderas under en viss tidsperiod (som kan konfigureras under programkörning), varefter beräkningarna avbryts och funktionen appliceras med hjälp av en call- efter behov. Detta tillvägagångssätt minskar tidsfördröjningarna som är inneboende i lat utvärdering samtidigt som det ger samma produktegenskaper.