Esimerkki epäsuorasta rekursiosta

Lukuohje

Jos et ymmärrä esimerkkiä, ei kannata huolestua.

Edellä mainittiin, että epäsuora rekursio tarkoittaa sitä, että funktio A kutsuu funktiota B, ja B kutsuu A:ta.

Esimerkkinä epäsuorasta rekursiosta tarkastellaan lausekkeen jäsentämistä ja evaluointia (eli lausekkeen arvon laskemista). Jäsentämällä (parse) tarkistetaan, että lauseke on oikean muotoinen eli että se noudattaa annettua kielioppia.

Lausekkeen kielioppi

Lauseke voidaan esittää kieliopilla:

<Expr> ::= <Term> + <Expr> | <Term> - <Expr> | <Term>
<Term> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ( <Expr> )

Yllä oleva kielioppi on kirjoitettu BNF-muotoon (Backus-Naur Form). Siinä välisymbolit (nonterminal) kirjoitetaan kulmasulkeiden sisään, ja loppusymbolit (terminal) sellaisenaan.

Kielioppi sallimia loppusymboleita ovat yksinumeroiset luvut (0 - 9), operaattorit + ja - sekä sulut. Kielioppi sallii esimerkiksi lausekkeet:

2+3
2+3-4
1-(2+3)
(2+3)
((2+3))
1+(6-7)-(2+4+5+(2+3))

Jos annettu lauseke noudattaa kielioppia, niin lauseke voidaan voidaan johtaa (derive) kieliopista soveltamalla jompaakumpaa kieliopin sääntöä riittävän monta kertaa. Esimerkiksi lauseke 1 - (2 + 3) voidaan johtaa seuraavasti:

<Expr>
     -> <Term> - <Expr>
     -> 1 - <Expr>
     -> 1 - <Term>
     -> 1 - ( <Expr> )
     -> 1 - ( <Term> + <Expr> )
     -> 1 - ( 2 + <Expr> )
     -> 1 - ( 2 + <Term> )
     -> 1 - ( 2 + 3 )

Ohjelmakoodi

Tarkasteltava ohjelma löytyy hakemistosta examples/02/expression/. Ohjelma jäsentää annetun lausekkeen, ja jos lauseke noudattaa kielioppia, niin ohjelma laskee sen arvon.

Ohjelmassa on pääohjelmafunktion lisäksi kaksi muuta funktiota kumpaakin kieliopin sääntöä varten: parse_term ja parse_expr. Nämä kumpikin kutsuvat toisiansa, eli kyseessä on epäsuora rekursio. Tästä syystä tarvitaan ennakkoesittely ainakin toiselle funktiolle.

Rekursio nähdään myös kieliopissa: <Expr>-säännössä esiintyy <Term>, ja <Term>-säännössä esiintyy <Expr>.

Ohjelman funktiot on toteutettu hyvin suoraviivaisesti kieliopin perusteella. Esimerkiksi funktiossa parse_term tarkastellaan ensin tapauksia, joissa termi on jokin numero. Sen jälkeen käsitellään tapaus, jossa termi on sulkujen sisällä oleva lauseke. Tällöin ohitetaan vasen sulkumerkki, ja sen jälkeen kutsutaan rekursiivisesti funktiota parse_expr. Lopuksi ohitetaan vielä oikea sulkumerkki.

Vastaavasti funktiossa parse_expr tarkastellaan kieliopin tapaukset erikseen. Lauseke voi olla pelkkä termi (esimerkiksi vain jokin luku vaikkapa 2), tai lauseke voi koostua kahdesta termistä, joiden välissä on plus-merkki (esimerkiksi 2+3), tai lauseke voi koostua kahdesta termistä, joiden välissä on miinus-merkki (esimerkiksi 2-3). Joka tapauksessa siis lausekkeessa on ensin termi, ja siksi heti aluksi kutsutaan rekursiivisesti funktiota parse_term. Sen jälkeen tarkistetaan, tuleeko vielä operaattori (+ tai -) ja lasketaan lausekkeen arvo sopivalla tavalla.

Rekursion hyödyllisyydestä

Tämän tyylistä ohjelmaa olisi vaikea toteuttaa muuten kuin rekursiivisesti. Rekursio on siis hyödyllinen asia osata. Ohjelma olisi hyvin helppo laajentaa esimerkiksi käsittelemään myös kerto- ja jakolaskun. Tai jos kieliopissa olisi enemmän sääntöjä, niillekin voitaisiin hyvin suoraviivaisesti toteuttaa omat funktionsa.