07 - imperatív for ciklusok átírása tail recursive függvényekké

Az előző összegző függvényünk most már tudjuk, hogy helyes és láttuk, hogy a Scalás implementáció tényleg nem tölti a stacket. A kérdés az, hogy hogyan tudunk készíteni ilyen függvényeket?

Valamiből persze ki kell indulni, nincs általános megoldó gép, de egy példán keresztül megnézzük, hogy ha pl. C-ben tudnánk valamire egy for ciklust írni, akkor abból hogyan lehet ,,automatikusan'' transzformálni egy tail rekurzív függvényt.

Vegyük megint az összegzést: adjuk össze a számokat n-től m-ig C-ben:

1
2
3
4
5
int sum(int n, int m) {
  int s = 0;
  for( int i = n; i <= m; i++ ) s+=i;
  return s;
}

Mondjuk, egy ilyet meg tudnánk írni C-ben, és az a kérdés, hogy lesz ebből tail rekurzív kód. Konkrétabban a ciklus részéből. Nem kell mást tennünk, mint

  • kigyűjteni minden változót, melyet a ciklusban látunk,
  • írni egy olyan (rekurzív) függvényt, ami ezeket mindet megkapja paraméterként,
  • a rekurzív hívásban pedig a következő iterációnak megfelelően módosítjuk az értékeket.

Konkrétabban: itt a for ciklus, amit rekurzióval akarunk kódolni, látja az i, n, m és s változókat (a ciklusváltozó is számít, a feltételekben szereplő és használt változók is, meg minden ,,kintebb'' deklarált változó is, melyet akár írunk, akár olvasunk). Tehát ebből első körben készíthetünk egy négyváltozós függvényfejlécet:

1
def sum(i: Int, n: Int, m: Int, s: Int): Int = ???

A visszatérési értéken elmélázunk majd kicsit később, hogy mi is kéne legyen a típusa, most legyen az a szándékunk, hogy a ciklust ,,implementáló'' függvényünk adja vissza az s értékét.

A ciklus inicializálásával nem kell ekkor még foglalkoznunk, majd úgy hívjuk meg a függvényt, hogy jó legyen; a feltétellel kell foglalkoznunk - ha a feltétel hamis, akkor állunk meg, és ekkor vissza kell adjuk az eredményt, ami, hát az s ,,változó'' értéke ebben az esetben:

1
2
3
def sum(i: Int, n: Int, m: Int, s: Int): Int = {
  if( i > m ) s else ???
}

Alakul, ha a feltétel hamis, visszaadjuk az s értékét, amibe elvileg legyűjtöttük az összeget. Különben mit is kell tennünk? Ha a ciklusmagban, amit szimulálunk éppen, van valami mellékhatás, amit szeretnénk is elvégezni, tegyük meg (most nincs; korábban a kiíratásnál volt egy printf-ünk) aztán hívjuk rekurzívan a függvényünket úgy, hogy aktualizáljuk az i (eggyel nő), n, m (nem változnak), s értékeket (ez a legutóbbi meg i-vel nő, hiszen az utasítás az eredeti C kódban s+=i volt). Presto:

1
2
3
def sum(i: Int, n: Int, m: Int, s: Int): Int = {
  if( i > m ) s else sum(i+1, n, m, s+i)
}

Az eredeti függvényünk persze nem kapta meg sem az i-t, sem az s-et, ez a kód még csak a ciklust ,,szimulálja''; ebből úgy lesz egy n-t és m-et kapó függvényünk, hogy beleégetjük a két értékadást, ami a C kódban eredetileg szerepelt a ciklusmagba első belépés előtt: az s = 0; értékadás a kódban és az int i = n értékadás a ciklus inicializálásában:

1
def sum(n: Int, m: Int): Int = sum(n, n, m, 0)

Tulajdonképpen kész is vagyunk. Persze ezek után optimalizálhatunk is a kódon. Ha jobban megnézzük, az n paramétert nem is használjuk semmire a kódban, csak változtatás nélkül átadjuk a következő hívásnak - ennek nincs haszna, törölhetjük is a négyváltozós függvényünk második paraméterét:

1
2
3
4
def sum(i: Int, m: Int, s: Int): Int = {
  if( i > m ) s else sum(i+1, m, s+i)
}
def sum(n: Int, m: Int): Int = sum(n, m, 0)

Persze a második kódban még látunk egy n-t, ő az i=n inicializálásból származik.

Kérdések, feladatok

  • Számoljuk végig az operatív szemantikával, hogy a sum függvény fenti rekurzív implementációi tényleg helyes értékeket adnak pl. az n=2, m=4 esetre!
  • Igazoljuk is formálisan, hogy a sum(i,n,m,s) függvény az s + i + (i+1) + ... + m intre értékelődik ki!
  • Hajtsuk végre a printelő for cikluson is ugyanezt az átalakítást.
  • Hajtsuk végre a következő C kódon, ami szintén az első n pozitív egész szám összegét számítja ki, ezt az átalakítást:
1
2
3
4
5
int sum(int n) {
  int s = 0;
  for( int i = n; i > 0; i-- ) s += i;
  return s;
}

Hasonlítsuk össze a kapott kódot az első tailrec összegző függvényünkkel.


Utolsó frissítés: 2020-12-22 21:04:26