Kihagyás

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
  def lazyFunc(x: Int, mode: String): Int = {
    println("Counting " + mode) // MELLÉKHATÁS!
    x * 2
  }

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
  def res_def = lazyFunc(10, "def")
  lazy val res_lazy = lazyFunc(10, "lazy")
  val res_val = lazyFunc(20, "strict")

  println(res_def + res_lazy + res_val)
  println(res_def + res_lazy + res_val)

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 esetben x 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
def tempFunc(): Int = {
  println("Hosszú számítás elindult...")
  Thread.sleep(3000) // Java osztályokhoz továbbra is hozzáférünk
  println("Számítás kész.")
  99
}

def callFunc(x: Int, y: => Int): Int = {
  if x > 10
    then println("Greater..")
    else y
  y
}

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
def fibonacci(a: Int, b: Int): LazyList[Int] = {
    a #:: fibonacci(b, a + b)
  }
1
2
3
4
val lazyNums: LazyList[Int] = LazyList.from(1)
println(lazyNums.take(10))
println(lazyNums.take(10).toList)
println(fibonacci(0, 1).take(10).toList)

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.


Utolsó frissítés: 2025-05-03 15:51:15