Saturday, February 15, 2020

02 - if-else és rekurzió

Nézzük ezt a függvényt (mostantól már rendszerint nem fogom kiírni a Main objectet, ami eddig minden példában szerepelt) és egy meghívását:

def parosE(n: Int) = {
  if (n % 2 == 0) "Páros" else "Páratlan"
}
println(parosE(2))
Ha futtatjuk, kiíródik, hogy "Páros". Amit a függvény törzsében látunk: egy if-else kifejezés, szintaxisa if (B) E else F, ahol E és F kifejezések, B pedig egy Boolean típusú kifejezés.
C-ben megszokhattuk, hogy az intek is jók ifbe, 0 hamis, többi igaz -- Scalában nem így megy, az if feltételébe Boolean kell menjen, az if(n%2) nem fordul. A százalék a C-ben megszokott módon a maradékos osztás maradékát adja eredményül, tehát pl. n%2 a páros számokra 0, a páratlanokra pedig 1 lesz. Az if-else kifejezés kiértékelésének a szabályai:
  • először kiértékeljük a feltételt, lesz belőle true vagy false,
  • ha true, akkor a kifejezést E-re írjuk át, ha false, akkor F-re,
  • és folytatjuk a kiértékelést.
Tehát pl. most a parosE(2) hívásakor a substitution model szerint egy if(2%2==0) "Páros" else "Páratlan", majd egy if(0==0)  "Páros" else "Páratlan", majd egy if(true) "Páros" else "Páratlan", végül a "Páros" string lesz a kifejezésünk, ez a legutóbbi lesz az értéke.
A típusról: az if(B) E else F kifejezés típusa az E és F kifejezések típusának "legspecifikusabb közös őse" lesz - erre még visszatérünk később, mindenesetre ha E és F típusa ugyanaz (mint most, mindkettő String), akkor ez a közös típus lesz az if-else kifejezés típusa is. Mivel pedig emiatt a fordító ki tudja következteni, hogy String a típusa a függvényünk törzsének, ezért nem kell megadnunk, hogy a függvény visszatérési értékének a típusa String lesz.

Amikor a fordító biztos, hogy nem tudja kikövetkeztetni a típust: ha rekurzív függvényt írunk (ami saját magát hívja meg, remélhetőleg más argumentumokkal). Erre a klasszikus példa a faktoriálist számító függvény szokott lenni, amit Scalában pl. így is implementálhatunk:

def fakt(n: Int ): Int = {
  if (n <= 1) 1 else n * fakt(n - 1)
}
Lássuk, mi történik, ha pl. kiértékeljük a fakt(5) kifejezést. Mostantól használni fogjuk a jelet is, amikor példa kiértékelünk: EF jelenti azt, hogy az E kifejezést egy lépésben az F kifejezésre írjuk át. Tehát a fakt(5) kifejezés kiértékelése:
fakt(5)if(51) 1 else 5fakt(51)if(false) 1 else 5fakt(51)5fakt(51)5fakt(4)5(if(41) 1 else 4fakt(41))5(if(false) 1 else 4fakt(41))5(4fakt(41))5(4fakt(3))5(4if(31) 1 else 3fakt(31))5(4if(false) 1 else 3fakt(31))5(4(3fakt(31)))5(4(3fakt(2)))5(4(3if(21) 1 else 2fakt(21)))5(4(3if(false) 1 else 2fakt(21)))5(4(3(2fakt(21))))5(4(3(2fakt(1))))5(4(3(2if(11) 1 else 1fakt(11))))5(4(3(2if(true) 1 else 1fakt(11))))5(4(3(21)))5(4(32))5(46)524120
és így kapjuk meg végül, hogy 5! az bizony 120.

Amire mindenképp fel kell figyelnünk ennél a kiértékelésnél: a kifejezés hossza egyre csak növekszik, az utolsó rekurzív hívásnál, ha fakt(n)-t hívunk, egy n-nel arányos méretű kifejezéssel dolgozunk. Valójában persze a Scala ugyanúgy, mint a C vagy a Java, a függvényhívásokkor a "hívási verembe" (call stack) pusholja le az argumentumokat, és hogy hova kell majd visszaugrani a vezérlésnek, aztán átugrik a meghívott függvény belépési pontjára. Ha ott lesz még egy rekurzív hívás, további argumentumokat pushol a call stackre, majd még továbbiakat, így egy n mélységű rekurziónál a call stack is n-nel arányos ütemben telik be - és ez azért rossz hír, mert a call stack alapból elég kicsi, néhány tízezernél nem fog tudni mélyebb hívást managelni, és ekkor dob egy StackOverflowErrort, ami miatt a JVM kilövi a programunk futását.

Ezzel valamit kezdenünk kell, mert a funkcionális programozás egyik alap építőköve az, hogy ciklusok helyett rekurzióban gondolkodunk (azt lehet immutable értékek mellett is, a ciklushoz meg well, kellene ciklusváltozó). Szerencsére van a rekurziónak egy olyan fajtája, amit "farokrekurziónak" (tail recursion) hívnak és ha sikerül így írjunk programot, akkor attól nem telik a call stack. Ez lesz a következő poszt témája.
folytköv

No comments:

Post a Comment