11. gyakorlat
Lusta kiértékelés¶
A mohó kiértékelés (szakirodalomban gyakran eager vagy strict kiértékelésként található meg) esetén a kifejezések azonnal kiértékelődnek, amikor azokat egy változóhoz rendeljük. Tehát az összes kifejezés értékét előre kiszámítjuk, mielőtt a program ténylegesen használná őket.
A lusta kiértékelés azt jelenti, hogy a kifejezéseket csak akkor értékeljük ki, amikor ténylegesen szükség van rájuk. Ha nem használunk fel egy változó értékét a programban, akkor annak kiszámítása elmarad, hiába történt meg a hozzárendelés. Ez segít elkerülni a szükségtelen számításokat, amelyek nem befolyásolják a végső eredményt.
Változók szintjén¶
Scala alapértelmezés szerint mohó (strict) kiértékelést alkalmaz, vagyis a kifejezéseket és argumentumokat azonnal kiértékeli, amikor megtörténik a változóhoz rendelés. A nyelv ugyanakkor lehetőséget kínál lusta (lazy) kiértékelés használatára is, amely esetben a kifejezés csak akkor kerül kiértékelésre, amikor valóban szükség van rá. Ez a megközelítés különösen hasznos lehet teljesítményoptimalizálás vagy végtelen adatszerkezetekkel végzett számítások során.
Vegyük először a következő függvényt, amelyet a könnyebb demonstrálás érdekében kiegészítünk egy mellékhatással:
1 2 3 4 |
|
Ezt a függvényt 3 különböző megközelítéssel fogjuk meghívni három változó segítségével:
-
val
: azonnal kiértékelésre kerül, akár használjuk, akár nem -
lazy val
: a változó értéke csak akkor kerül kiszámításra, amikor először használjuk, utána a cache-ből kerül újra és újra betöltésre -
def
: ez egy újrafelhasználható függvénydefiníció lesz és csak akkor fut le, amikor használjuk - minden egyes használatnál újból kiszámításra kerül az eredmény
1 2 3 4 5 6 |
|
A lazy
kulcsszó csak val
esetén használható, var
esetén nem!
Függvények szintjén¶
A kiértékelés módját függvények esetében is érdemes megvizsgálni. Scala-ban két megközelítés elérhető:
-
call-by-value: Ez a függvényparaméterek kiértékelésének alapértelmezett módja. A függvényargumentum csak egyszer kerül kiszámolásra. Előnye, hogy nagy számításigényű művelet esetén csak egyszer kell végrehajtani a kiértékelést, azonban ha nem használjuk a függvényben az adott paramétert (például egy feltételhez kötött kiértékelés esetén), akkor is kiértékelésre kerül a függvényhívás pillanatában.
-
call-by-name: Ebben az esetben a függvényparaméter nem értékelődik ki azonnal a függvényhíváskor, hanem csak amikor használjuk a függvény törzsében. Ezt a működést külön jeleznünk kell a függvény fejlécében, pl.:
x: => Int
. Ilyen esetbenx
mindig újra és újra kiértékelődik, amikor használni szeretnénk a változót a függvény törzsében.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Hasonlítsuk össze a kimenetét a callFunc(1, tempFunc())
és a callFunc(11, tempFunc())
függvényhívásoknak! Mi történik ha mindkét paramétert érték szerint adjuk át?
Végtelen listák¶
Végezetül nézzük meg a lusta kiértékelés gyakorlati alkalmazását egy, a korábban már látott List-hez hasonló adatstruktúrán, a LazyList-en. Ez a kollekció lehetővé teszi végtelen (vagy nagyon hosszú) listák kezelését, például a Fibonacci-számok, prímszámok vagy tetszőleges számsorozatok generálását.
Fontos megérteni, hogy egy ilyen végtelen lista hagyományos módon (például List.range(start, end)
segítségével) nem hozható létre, hiszen a List megpróbálná az összes elemet azonnal kiszámítani és memóriába tölteni. Ugyanez a probléma jelentkezik akkor is, ha egy ilyen hosszú listát szeretnénk átadni egy másik függvénynek (ahogy láthattuk és ahogy az órán is csináltuk korábban), a call-by-value kiértékelés miatt előbb ki kellene számítani a teljes lista tartalmát, ami idő- és memóriaigényes lehet.
A LazyList azonban ezt a problémát megoldja: elemei lustán, azaz csak szükség esetén értékelődnek ki. Ez azt jelenti, hogy nem kell előre legenerálni és memóriába tölteni az egész listát, még akkor sem, ha az potenciálisan végtelen. Így a LazyList ideális eszköz nagy méretű vagy végtelen sorozatok hatékony, funkcionális kezelésére.
A take(n: Int)
egy új LazyList-et ad vissza, amely tartalmazza az első n elemet szintén lusta kiértékeléssel. Ez azt jelenti, hogy a toList
függvényt is használnunk kell, hogy kikényszerítsük a szükséges mennyiségű elem kiszámítását.
Ahhoz, hogy létre tudjunk hozni LazyList-et, egy új listaépítő operátort is be kell vezetünk, ezt lesz a #::
.
1 2 3 |
|
1 2 3 4 |
|
Feladat
Hozz létre egy függvényt, amely egy végtelen LazyList-et generál adott szabály szerint: minden harmadik számot szorozzon meg kettővel, minden ötödik számhoz adjon hozzá hármat. Kérjük le a lista első 20 elemét.
Feladat
Készíts egy függvényt, amely egy egész számokat tartalmazó végtelen listát, egy szűrési feltételt, valamint két alternatív végrehajtási stratégiát kap paraméterként. A függvény feladata, hogy megtalálja a lista első olyan elemét, amelyre a megadott feltétel (Int => Boolean) teljesül. Amint ez az első találat megvan, a függvény ellenőrizze, hogy a szám páros vagy páratlan:
-
Ha a szám páros, akkor hajtsa végre az első stratégiát
-
Ha páratlan, akkor hajtsa végre a második stratégiát
A két stratégia típusa => Unit legyen, azaz a kifejezések csak akkor értékelődjenek ki, amikor valóban futtatni kell őket.