Értékelési stratégia - a programozási nyelv szemantikai szabályai, amelyek meghatározzák, hogy egy függvény ( módszer, művelet, kapcsolat) argumentumait mikor kell értékelni, és milyen értékeket kell átadni . Például a call-by-worth/pass-by-reference stratégia azt diktálja , hogy a hívott függvény törzsének végrehajtása előtt ki kell értékelni az argumentumokat, és minden argumentumhoz két lehetőséget kell adni: az aktuális érték beolvasása, ill. megváltoztatása a hozzárendelés operátorával [1] . Ez a stratégia hasonló a lambda-kalkulus redukciós stratégiájához , de vannak eltérések.
A gyakorlatban sok ipari nyelv ( Java , C# ) számítási modellje egy „ megemlítéskor/hivatkozási hívás ” stratégiára vezethető vissza . Néhány régebbi nyelv, különösen a nem biztonságos nyelvek, mint például a C++ , több különböző hívási mintát kombinálnak. Történelmileg a " hívás érték szerint " és a " hívás név szerint " az 1950 -es évek végén létrehozott Algol-60- ra nyúlik vissza . Csak a tiszta funkcionális nyelvek, mint például a Clean és a Haskell használják a " szükség szerint hívást ".
Megjegyzés - az orosz nyelvű irodalomban a számítási stratégiát " paraméterátadási módszernek ", " számítási modellnek " vagy " hívási modellnek " is nevezik. Azutolsó opció összetévesztheti a hívási egyezményt . A " paraméterek átadása " kifejezés sok számítási stratégia esetében helytelen.
A szigorú értékelési modell azt jelenti, hogy az argumentumok mindig teljes mértékben kiértékelésre kerülnek, mielőtt a függvényt alkalmaznák rájuk.
Az egyházi jelölésekben az állítások lelkes értékelése a funkciók szigorú értékelésének felel meg, ezért a szigorú értékelést néha " buzgónak " nevezik. A legtöbb létező nyelv szigorú függvényértékelést használ.
Az alkalmazói sorrend , más néven „ balról jobbra, belülről kifelé ”, ( bal legbelső ) [2] [3] , olyan számítási stratégiát jelent, amelyben az alulról felfelé haladó AST balról jobbra csökkentett kifejezésekben értékeli az argumentumokat.
Az érték szerinti hívástól eltérően a kiértékelés applikatív sorrendje a lehető legnagyobb mértékben csökkenti a függvénytörzsben szereplő kifejezéseket, mielőtt alkalmazná.
Ahhoz, hogy egy példát tekintsünk az alkalmazói sorrendben végzett számításokra, több függvényt definiálunk [4] :
négyzet(x) = x * x négyzetek_összege(x, y) = négyzet(x) + négyzet(y) f(x) = négyzetek_összege (x + 1, x * 2)Az f(5) értékének kiszámításakor a következő helyettesítéseket kapjuk:
f(5) = négyzetek_összege(5 + 1, 5 * 2) = négyzet(6) + négyzet(10) = ((6 * 6) + (10 * 10)) = 36 + 100 = 136A Call by value ( angolul call-by-value ) a legszélesebb körben használt számítási stratégia, számos nyelven látható, a C - től a Scheme -ig . Érték általi meghívásakor a rendszer kiértékeli az argumentumkifejezést, és az eredményül kapott értéket társítja a megfelelő formális függvényparaméterhez (általában úgy, hogy az értéket egy új memóriahelyre másolja). Ebben az esetben, ha a nyelv lehetővé teszi, hogy a függvények értéket rendeljenek a paramétereikhez, akkor a változások csak ezeket a helyi másolatokat érintik, de a függvényhívás helyén látható értékek visszatéréskor változatlanok maradnak.
Valójában az érték szerinti hívás nem egy adott hívási minta, hanem minták egy családja, amelyben az argumentumok kiértékelésre kerülnek, mielőtt átadnák a függvénytörzsnek. A legtöbb nyelv ( Common Lisp , Eiffel , Java ), amely érték szerinti hívást használ, balról jobbra értékeli a függvényargumentumokat, de vannak olyanok, amelyek jobbról balra értékelik őket, és vannak olyanok ( Séma , OCaml , C ), amelyek nem adják meg a kiértékelés sorrendjét. .
Rejtett korlátozásokEgyes esetekben a " call-by-value " kifejezés nem teljesen helytálló, mivel az átadott érték nem a szokásos értelemben vett változó értéke, hanem hivatkozás az értékre, amelynek megvalósítása eltérő lehet. Ennek eredményeként az a kód, amely szintaktikailag hívásonkénti értékként néz ki, hívás szerinti hivatkozásként vagy társhasználatként viselkedhet, és a program viselkedése a nyelv szemantikájának finom részleteitől függ.
A hívás hivatkozással használatának oka általában az, hogy a nyelv technikailag nem biztosítja az összetett adatok egyetlen értékként történő kezelését - adatszerkezetként jeleníti meg, bár nagyon hasonlít a forrásban lévő értékre. kód. A teljes érték és a maszlagos adatstruktúra közötti vonal pontos helyének meghatározása nagyon nehéz lehet. C-ben egy vektor (vagyis egy egydimenziós tömb , amelynek speciális esete a karakterlánc) adatstruktúra , ezért egy memóriahelyre való hivatkozásként kezelik; azonban egy struktúra akkor is érték, ha mezői vektorok. A Maple -ben a vektor egy tábla speciális esete, tehát egy adatstruktúra; azonban egy lista (amely pontosan ugyanúgy épül fel és indexelhető) egy érték. A Tcl kétféleképpen kezeli az értékeket : az értékmegjelenítést szkript szinten használják, és a nyelv maga kezeli a megfelelő adatstruktúrát szükség szerint. Az adatszerkezetben végrehajtott változtatások tükröződnek az értékben, és fordítva.
Az a magyarázat, hogy a nyelv " értékenként adja át a paramétereket, ahol az érték referencia " elég gyakori (de nem szabad összetéveszteni a hivatkozáson keresztüli hívással); egyébként társhasználati hívásnak hívják . Emiatt az érték szerinti hívás a Java és a Visual Basic programban jelentősen eltérően viselkedik, mint a C és Pascal nyelven . C vagy Pascal nyelven egy hatalmas adatstruktúra függvénynek való átadása a teljes struktúrát másolja (kivéve, ha az argumentum valójában az adatszerkezetre utal), ami potenciálisan jelentősen csökkenti a teljesítményt; a struktúra állapotában bekövetkezett változások azonban nem lesznek láthatók a hívó kontextusban. Java-ban és Visual Basic-ben mindig csak a struktúrára való hivatkozás kerül másolásra, ami gyors, és a hívás helyén látható lesz a szerkezetváltozás.
Amikor a függvény hívott-by-reference ( eng. call-by-reference ), vagy passing-by-reference ( pass-by-reference ), a függvény implicit módon hivatkozást kap az argumentumként használt változóra, ahelyett, hogy annak másolata lenne. érték.
Ez általában azt jelenti, hogy a függvény módosíthatja (vagyis megváltoztathatja az állapotát ) a paraméterként átadott változót, és ez hatással lesz a hívási kontextusra. Ezért a referencia-hívás felhasználható kommunikációs csatorna létrehozására a hívott fél és a hívó fél között. A közvetlenül a hivatkozáson alapuló híváson alapuló nyelv megnehezíti a programozó számára a függvényhívás összes hatásának nyomon követését, így az hibás lehet .
Sok nyelv támogatja a call-by-referenciát ilyen vagy olyan formában, de kevesen használják alapértelmezés szerint, például a Perl . Számos nyelv, például a C++ , a PHP , a Visual Basic .NET , a C# és a REALbasic alapértelmezés szerint a hívást használja, de speciális szintaxist biztosít a hivatkozással történő híváshoz. A C++ ezenkívül egy egyedi call-by-reference-to- constans stratégiát vezet be .
Egyes nyelvek típusrendszerei, amelyek az érték szerinti hívást használják, és közvetlenül nem támogatják a hivatkozáson alapuló hívást, lehetővé teszik hivatkozások (más objektumokra utaló objektumok), különösen mutatók (olyan objektumok, amelyek más objektumok címei a számítógépben ) kifejezett meghatározását. memória). Használatuk lehetővé teszi a hívás szimulálását referencia alapján a híváson belüli érték szerinti szemantikán. Ilyen megoldást használnak például C és ML nyelvekben . Ez nem egy önálló kiértékelési stratégia – a nyelv továbbra is érték szerint hív –, hanem néha " hívási címként " ( hívási címként ) vagy " áthaladási címként " ( pass-by-címként ) hivatkoznak rá. . Nem biztonságos nyelveken, mint például a C vagy C++ , memória-hozzáférési hibákhoz vezethet , például nulla mutatóhivatkozáshoz , ami megnehezíti a program megértését és a nyelv kezdeti megtanulását. Az ML -ben a hivatkozások típus- és memóriabiztosak .
Közeli hatást biztosít az olyan nyelvekben használt " hívás együtthasználattal " stratégia is, mint a Java , Python , Ruby .
A tisztán funkcionális nyelvekben nincs szemantikai különbség a hivatkozás általi hívás és az érték szerinti hívás között (mivel adatstruktúrájuk megváltoztathatatlan, és a függvénynek amúgy sincs módja megváltoztatni az argumentumait), ezért általában hívás értékre hívásként írják le őket. , annak ellenére, hogy sok implementáció ténylegesen hivatkozik a hívásra a hatékonyság javítása érdekében.
A következő példa egy szimulált hívást mutat be hivatkozással az E nyelven :
def modify( var p, &q ) { p := 27 # érték által átadott paraméter - csak a helyi érték változik q := 27 # hivatkozással átadott paraméter - a hívásban használt változó megváltoztatása } ? var a := 1 #érték: 1 ? var b := 2 #érték: 2 ? módosít(a, &b) ? a #érték: 1 ? b #érték: 27A következő példa egy hívás szimulációját mutatja be hivatkozással C nyelven . Az egész típusú változók és mutatók érték szerint kerülnek átadásra. De mivel a mutató tartalmazza a külső változó címét, az értéke megváltozik.
void Módosítás ( int p , int * q , int * o ) { // p = 27 értékkel átadott összes paraméter ; // csak a helyi érték változik * q = 27 ; // megváltoztatja a q * o = 27 által mutatott külső változót ; // az o által mutatott külső változó módosítása } int main () { int a = 1 ; int b = 1 ; int x = 1 ; int * c = & x ; Módosít ( a , & b , c ); // 1. paraméter - a változó értéke // 2. paraméter - b változó címe // 3. paraméter - c változó értéke, amely az x változó címe // b és x megváltozik return ( 0 ); }call-by-sharing vagy call-with-resource-sharing ( angolul call-by-sharing ), szintén call-by-objektum ( call-by-object ), szintén call-by-object-sharing vagy call-with- share -object ( call-by-object-sharing ), azt jelenti, hogy a nyelv értékei objektumon alapulnak, nem pedig primitív típusokon , azaz „ csomagolt ” („csomagolt”, angol dobozos ). Ha társhasználattal hívják meg, a függvény megkapja az objektumhivatkozás másolatát . Maga az objektum nem másolódik – meg van osztva vagy megosztva . Következésképpen egy függvény törzsében egy argumentumhoz való hozzárendelésnek nincs hatása a hívó kontextusban, de az argumentum összetevőihez való hozzárendelésnek igen.
A co-use felhívást először a CLU -ban valósították meg 1974 - ben Barbara Liskov és mások [5] irányításával .
Ezt a stratégiát a Python [6] , Iota [7] , Java (objektumhivatkozásokhoz), Ruby , JavaScript , Scheme , Ocaml , AppleScript és még sokan mások használják. A terminológia azonban a különböző nyelvi közösségekben eltérő. Például a Python közösség a "co-use call" kifejezést használja; a Java és a Visual Basic közösségekben ugyanazt a szemantikát gyakran úgy írják le, mint " hívás érték szerint, ahol az "érték" egy objektumhivatkozás "; a Ruby közösségben azt mondják, hogy Ruby " hivatkozásos hívást használ " - annak ellenére, hogy ezekben a nyelvekben a hívásszemantika azonos.
A megváltoztathatatlan objektumok esetében nincs különbség a használatonkénti és az érték szerinti hívás között, kivéve , hogy ezek az objektumok azonosak . A co-use hívás a bemeneti/kimeneti paraméterek alternatívája [8] - a paraméter megváltoztatása itt nem jelenti a paraméterhez való hozzárendelést ; a paraméter nem íródik felül , hanem megváltoztatja az állapotot , megtartva az azonosságát.
Például a Pythonban a listák változtatható objektumok, így:
def f ( l ): l . hozzáfűz ( 1 ) m = [] f ( m ) nyomtatás m- kiírja a következőt: " [1]", mert a " " argumentum lmegváltozott.
A következő példa bemutatja a változás és a hozzárendelés közötti különbséget . Ilyen kód:
def f ( l ): l += [ 1 ] m = [] f ( m ) print m- kiírja: " [1]", mivel a " " operátor l += [1]úgy viselkedik, mint " l.extend([1])"; de hasonló kód:
def f ( l ): l = l + [ 1 ] m = [] f ( m ) print m- kiírja a " []" karaktert, mert a " " operátor l = l + [1]új helyi változót hoz létre az argumentum megváltoztatása helyett [9] .
A következő program viselkedése bemutatja a bekeretezett értékek és a felhasználási hívás szemantikáját:
x = [[]] * 4 x [ 0 ] . hozzáfűzi ( 'a' ) x [ 1 ] . hozzáfűzi ( 'b' ) x [ 2 ] . hozzáfűzés ( 'c' ) nyomtatás ( x ) >> [[ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ], [ 'a' , 'b' , 'c' ]]A " x = [[]] * 4" operátor létrehoz egy üres listát (nevezzük " l"), majd egy új listát ( az " " azonosítóhoz társítva ) négy elemből, amelyek mindegyike a " "-re való hivatkozás, azaz " ”. A lista „ ” különböző elemeinek ezt követő hívásai megváltoztatják a „ ” objektumot . Ugyanez történik a „ ” lista kinyomtatásánál is: mivel négy hivatkozásból áll a „ ”-re, a „ ” összetétele négyszer kerül kinyomtatásra. xlx = [ l, l, l, l ]xlxll
call - by -copy-restore , szintén másolás - in copy-out ( bemásolás kimásolás ), szintén call-by-value-in-result ( call-by-value-result ), vagy call -by- value A -return , ahogy a Fortran nyelvi közösségben hívják, a call-by-reference egy speciális esete , amelyben a megadott hivatkozás egyedi a hívó kontextusban. Ez az opció a többprocesszoros rendszerek és a távoli eljáráshívások kontextusában érdekes : ha a függvényparaméter egy hivatkozás, amelyhez egy másik végrehajtó folyamat is hozzáférhet, akkor annak tartalma átmásolható egy új hivatkozásra, amely már nem lesz elérhető; amikor a függvény visszatér, az új hivatkozás megváltozott tartalma átmásolódik az eredeti hivatkozásra ("visszaállítva").
A call-by-copy-restore szemantikája akkor is eltér a hivatkozáson alapuló hívástól, ha két vagy több függvényargumentum egymás álneve, azaz a hívási környezetben ugyanarra a változóra mutat. Referenciaalapú hívás esetén az egyik megváltoztatása a másik megváltoztatását jelenti. A másolás-visszaállítás-hívás ezt megakadályozza azzal, hogy különböző másolatokat ad át a függvénynek, de az eredmény a hívó kontextusban nem definiálható, mivel attól függ, hogy a visszamásolás ugyanabba az irányba (balról jobbra vagy jobbra-to -balra), mint a kihívás előtt.
Ha a hivatkozást inicializálás nélkül adják át, akkor ezt a kiértékelési stratégiát hívásonkénti eredménynek nevezhetjük .
A részleges kiértékeléssel ( angol részleges értékelés ) nem alkalmazott függvényben végezhetők számítások. Minden olyan részkifejezés, amely nem tartalmaz kötetlen változókat, kiértékelődik, és az ismert argumentumokkal rendelkező függvények alkalmazásait lecsökkenti. Mellékhatások esetén a teljes részleges kiértékelés nemkívánatos eredményeket hozhat, így a részleges kiértékelést támogató rendszerek csak a függvényekben szereplő tiszta kifejezésekre (mellékhatások nélküli kifejezésekre) hajtják végre ezeket.
A nem szigorú értékelési modell azt jelenti , hogy az argumentumok nem kerülnek kiértékelésre mindaddig, amíg értéküket fel nem használjuk a függvénytörzsben.
A függvények nem szigorú értékelése megfelel az operátorok lusta kiértékelésének a Church jelölésben , ezért a nem szigorú értékelést gyakran " lustának " nevezik.
Számos nyelven ( C , C++ stb.) a logikai kifejezések értékelési sorrendje nem szigorú, ezt az orosz nyelvű szakirodalomban rövidzárlati kiértékelésnek nevezik , ahol a számítások leállnak, amint az eredmény egyértelműen megjósolhatóvá válik – például az „ igaz ” érték diszjunkcióban, „ hamis ” konjunkcióban stb. Az elágazás operátorok gyakran laza kiértékelési szemantikával is rendelkeznek, vagyis azonnal visszaadják a teljes operátor eredményét, amint azt egy egyértékű ág generálja.
A normál kiértékelési sorrend ( eng. Normal order ; még " számítás balról jobbra, kívülről befelé ", bal szélső legkülső ) egy számítási stratégia, amelyben a befoglaló kifejezést teljesen redukálják, függvényeket alkalmazva az argumentumok kiértékelése előtt.
A normál sorrendtől eltérően a hívás-by-name stratégia nem értékeli ki a nem meghívott függvényeken belüli argumentumokat és kifejezéseket.
Például a korábban definiált f függvény f(5) értéke normál sorrendben kiértékelve a következő helyettesítéseket adja [4] :
f(5) = négyzetek összege (5 + 1, 5 * 2) = négyzet(5 + 1) + négyzet (5 * 2) = ((5 + 1) * (5 + 1)) + (( 5 * 2) * (5 * 2)) = (6 * 6) + (10 * 10) = 36 + 100 = 136A hívás-by-name stratégiában az argumentumok nem kerülnek kiértékelésre a függvény meghívása előtt. Ehelyett közvetlenül behelyettesítik őket a függvény törzsébe ( befogását megakadályozó helyettesítéssel ), majd kiértékelik a követelmény helyett. Ha egy argumentumot nem használunk a függvénytörzsben, akkor az egyáltalán nem kerül kiértékelésre; ha többször használjuk, akkor minden előfordulásnál újraszámításra kerül (lásd Jensen trükkje ).
Név szerinti hívás néha előnyösebb, mint érték szerinti hívás. Ha az argumentumot nem használjuk a függvény törzsében, a névvel történő hívás időt takarít meg azzal, hogy nem értékeli ki, míg az értékhívás elkerülhetetlen kiértékelést jelent. Ha az érv egy nem befejező értékelés , az előny óriási. Azonban, ha argumentumot használunk, a névvel történő hívás gyakran lassabb, mivel ehhez egy úgynevezett " thunk " létrehozása szükséges.
Először használtak név szerinti hívást az Algol-60 nyelven . A .NETExpression<T> nyelvek a küldöttek vagy -paraméterek használatával szimulálhatják a hívást név szerint . Ez utóbbi esetben a függvény AST -t kap . Az Eiffel -nyelv ügynököket valósít meg, amelyek igény szerint végrehajtott műveletek.
A Call -by-need egy megjegyzett hívási névváltozat , ahol ha egy argumentumot kiértékelünk , az értéke későbbi felhasználás céljából tárolásra kerül. A " nyelv tisztasága " esetén ( mellékhatások hiányában ) ez ugyanazt az eredményt adja, mint a név szerinti hívás; és olyan esetekben, amikor az argumentumot kétszer vagy többször használjuk, a szükségszerű hívás szinte mindig gyorsabb.
Mivel a kiértékelt kifejezések nagyon mélyen egymásba ágyazódhatnak, a call-by-need nyelvek általában nem támogatják közvetlenül a mellékhatásokat (például állapotváltozásokat ), és ezeket monádokkal (mint a Haskellben ) vagy egyedi típusokkal mint a Clean -ban) emulálni kell. nyelv ). Ez kiküszöböli a lusta kiértékelés minden előre nem látható viselkedését, amikor a változóértékeket felhasználásuk előtt megváltoztatják.
A call-of-need szemantika leggyakoribb megvalósítása a lusta értékelés , bár vannak más változatok is, például az optimista értékelés .
A Haskell a leghíresebb nyelv, amely igény szerinti hívást használ. R is használ egyfajta call-by-need. A .NET nyelvek szükség szerint szimulálhatnak egy hívást a Lazy<T>.
A call- by -macro -expansion hasonló a hívás-by-name-hez, de szöveges helyettesítést használ a nem rögzítő helyettesítés helyett. Gondatlan használat esetén a makró helyettesítése változók rögzítéséhez és nem kívánt programviselkedéshez vezethet. A higiénikus makrók kiküszöbölik ezt a problémát az árnyékolt, nem paraméteres változók ellenőrzésével és szükség esetén cseréjével.
Teljes β-redukció esetén egy függvény bármely alkalmazása csökkenthető (az argumentum behelyettesítésével a függvény törzsébe, helyettesítéssel, hogy megakadályozzuk az rögzítését bármikor. Ez megtehető még egy nem alkalmazott függvény törzsében is .
Jövőbeni hívás vagy párhuzamos hívásnév egy párhuzamos értékelési stratégia: a jövőbeli kifejezések értékei program többi részével párhuzamosan kerülnek kiértékelésre . Azokon a helyeken, ahol célértékre van szükség, a főprogram a számítás befejezéséig blokkol, ha az még nem fejeződött be.
Ez a stratégia nem determinisztikus, mivel a számítások bármikor elvégezhetők a szándék létrehozása (ahol a kifejezés adva van) és az érték felhasználása között. Hasonlít a call-by-need -hez, mivel az érték csak egyszer kerül kiértékelésre, és a kiértékelés elhalasztható addig, amíg az értékre valóban szükség van, de korábban is elkezdhető. Ezen túlmenően, ha a célértékre már nincs szükség (például a függvénytörzsben egy lokális változó kiértékelése megtörtént, és a függvény befejeződött), a kiértékelés megszakadhat.
Ha a célokat folyamatokon és szálakon keresztül valósítják meg, akkor egy cél kódban történő létrehozása új folyamatot vagy szálat hoz létre, az érték elérése szinkronizálja azt a fő szállal, a célkiértékelés befejezése pedig az értékét kiszámoló folyamat megölését jelenti.
Az optimista kiértékelés egy másik változata a call-by-need eljárásnak, amelyben a függvény argumentuma részben kiértékelődik egy meghatározott időtartamra (amely a program végrehajtása során konfigurálható), ezután a számítások megszakadnak, és a függvényt egy hívás segítségével alkalmazzák. szükség szerint. Ez a megközelítés csökkenti a lusta értékelésben rejlő időkéséseket, miközben ugyanazokat a termékjellemzőket biztosítja.