Ohjelmoinnin peruskurssien laaja oppimäärä Luento 1: Rekursiivinen ajattelutapa, Scheme-kielen perusteita (mm. SICP 11.2.4) Riku Saikkonen 16. 10. 2012
Sisältö 1 Kurssijärjestelyitä 2 Perusteita Scheme-kielestä, häntärekursio 3 Rekursio
Kurssijärjestelyitä kurssin sivuilla on nyt: SICP-kirjasta käsiteltävät kohdat ohjeita Schemen käytöstä ensimmäiset harjoitustehtävät (2 kierrosta 4:stä, deadlinet 31. 10. ja 12. 11.) tehtävien palautusjärjestelmä tulossa pian tehtäviin voi kysyä apua: IRC:stä (kurssin kanavalta tai assistentilta) milloin tahansa, mutta varsinkin viikottaiseen päivystysaikaan (aika tulee kurssin sivuille) tavallisen kurssin laskuharjoituksissa ti klo 812 Maari-A:ssa (laajan assistentti Miki Tolonen on toinen niiden pitäjistä) ensi viikolla on vakioajan sijaan erillinen laskuharjoitusaika to 25. 10. klo 1214 Maari-A
Kurssin käyttämä Schemen versio syksyn kurssilla käytetään Scheme-kielen R 5 RS-nimistä versiota ei siis uudempaa R 6 RS-versiota käytämme Scheme-toteutusta nimeltä Gambit-C http://www.iro.umontreal.ca/~gambit/ interaktiivinen tulkki ja tehokas kääntäjä sisäänrakennettuna hyvä debuggeri melko helppo liittää muihin ohjelmointikieliin se on valmiiksi asennettuna Niksulassa (ei Aalto-koneissa) voit asentaa sen omalle koneellesi (ohje wikissä) ja/tai hae itsellesi Niksulan tunnus (T-talon huoneesta B210, arkisin klo 12-14, sano että suoritat laajaa) ks. https://wiki.aalto.fi/display/laajaohj12/scheme-ohjeita
Sisältö 1 Kurssijärjestelyitä 2 Perusteita Scheme-kielestä, häntärekursio 3 Rekursio
Scheme ja Python Schemessä ja Pythonissa on paljon samaa, esimerkiksi: dynaaminen tyypitys (tyyppejä ei näy koodissa) samantapainen sisennystyyli (tosin Scheme ei pakota siihen) interaktiivinen tulkki ja Schemessä on erilaista muun muassa: perussyntaksi (esim. sulut) silmukkarakenteiden sijaan käytetään rekursiota muuttujien määrittelytapa ei olioita (ja funktioita käytetään enemmän) Pythonia paljon pienempi kieli monta eri toteutusta, joissa on omat standardikirjastot
Pieni Scheme-esimerkki (SICP 1.2.4) Kaksi potenssiinkorotusalgoritmia b n expt.scm (define (square n) (* n n)) ; apufunktio fast-expt:iä varten (define (slow-expt b n) (if (= n 0) 1 (* b (slow-expt b (- n 1))))) (define (fast-expt b n) (cond ((= n 0) 1) ((even? n) (square (fast-expt b (/ n 2)))) (else (* b (fast-expt b (- n 1)))))) Testiajo: (fast-expt 2 150) 1427247692705959881058285969449495136382746624
Silmukat ja häntärekursio (SICP 1.2.1) lausekielissä käytetään paljon silmukoita (for, while, jne.) Schemessä tyypillisempää on tehdä apufunktio, joka kutsuu itseään häntärekursiivisesti eli niin, että funktiokutsun jälkeen ei tehdä mitään Schemessä tämä on tehokasta (Javassa ja Pythonissa pino kasvaisi) häntärekursiivista koodia { voi lukea matemaattisemmin p jos c > n esim. alla iter(p, c) = iter(c p, c + 1) muuten Scheme fact.scm (define (factorial n) (define (iter p c) (if (> c n) p (iter (* c p) (+ c 1)))) (iter 1 1)) Python rekursiivisesti def factorial(n): def iter(p, c): if c > n: return p else: return iter(c*p, c+1) return iter(1, 1) Python fact.py def factorial(n): p = 1 c = 1 while c <= n: p = c * p c = c + 1 return p
Tavallisessa rekursiossa pino kasvaa (SICP 1.2.1) Rekursiivinen kertomafunktio (define (fact n) (if (= n 0) 1 (* n (fact (- n 1))))) fact.scm esim. (fact 4):ää evaluoidessa pinoon jää lauseke (* 4 ), kun (fact (- 4 1)):tä kutsutaan perustapaukseen päästessä pinossa on (* 4 (* 3 (* 2 (* 1 )))) oikealla on pino enemmän todellisuutta vastaavassa muodossa (tosin se riippuu toteutuksesta) Kutsupino lopussa: Paluuosoite fact:iin fact:n argumentti n=0 Paluuosoite fact:iin fact:n argumentti n=1 Paluuosoite fact:iin fact:n argumentti n=2 Paluuosoite fact:iin fact:n argumentti n=3 Paluuosoite tulkkiin fact:n argumentti n=4...
Häntärekursiossa pinon ei tarvitse kasvaa (SICP 1.2.1) Häntärekursiivinen kertoma fact.scm (define (f-iter p c n) (if (> c n) p (f-iter (* c p) (+ c 1) n))) (define (fact n) (+ 0 (f-iter 1 1 n))) nyt (fact 4):ää evaluoidessa menossa on koko ajan vain yksi f-iter:n kutsu ensin (f-iter 1 1 4), sitten (f-iter 1 2 4),..., lopuksi (f-iter 24 5 4), joka palauttaa 24 Kutsupino lopussa: Paluuosoite fact:iin f-iter:n argumentti p=24 f-iter:n argumentti c=5 f-iter:n argumentti n=4 Paluuosoite tulkkiin fact:n argumentti n=4.. ilman (+ 0...):aa pinoon ei jäisi fact:istakaan mitään.
Muita tapoja tehdä silmukoita Schemessä on myös muutama silmukkarakenne: nimetty let: lyhyempi syntaksi häntärekursiiviselle silmukalle do: yleistetty tavanomainen for- ja while-silmukka nämä on sisäisesti toteutettu funktionaalisesti häntärekursiolla näitä ei kuitenkaan yleensä käytetä (varsinkaan do:ta) lisäksi Schemen makroilla olisi helppo määritellä omia silmukkarakenteita (nämäkin kaksi voi toteuttaa makroilla) Nimetty let (define (factorial n) (let iter ((p 1) (c 1)) (if (> c n) p (iter (* c p) (+ c 1))))) fact.scm do (define (factorial n) (do ((p 1 (* c p)) (c 1 (+ c 1))) ((> c n) p))) fact.scm
Globaalien ja paikallisten muuttujien määritteleminen Schemessä on monta tapaa määritellä muuttujia ja proseduureja, mutta melkein aina kannattaa käyttää alla olevia tapoja globaalit muuttujat (vain päätasolla): globaali proseduuri: (define (f x)...) tai (define f (lambda (x)...)) globaali muuttuja: (define x 3) paikalliset (esim. funktion sisäiset) muuttujat: paikallinen proseduuri: (define (f x)...) (let ((f (lambda (x)...)))...) toimii myös, mutta tällöin funktiossa ei voi viitata itseensä paikallinen muuttuja: (let ((x 3))...) useampi edellisiin viittaava paikallinen muuttuja: (let* ((x 3) (y (+ x 1)))...) eri tapoihin määritellä muuttujia palataan tulkkien yhteydessä, mutta nämä säännöt riittävät käytännön ohjelmointiin
Sisältö 1 Kurssijärjestelyitä 2 Perusteita Scheme-kielestä, häntärekursio 3 Rekursio
Rekursiivinen ajattelutapa Tässä eräs tapa ratkaista ohjelmointiongelma rekursiivisesti (vrt. matemaattinen induktio): 1 Etsi perustapaukset eli triviaaliratkaisu. 2 Keksi tapa ratkaista iso ongelma muokkaamalla pienemmän ongelman ratkaisua tai yhdistämällä useamman pienemmän ongelman ratkaisut. Esimerkki: Kuinka monella eri tavalla annettu luku voidaan jakaa tekijöihin? (ei vain alkulukuihin esim. 12 = 2 2 3 = 2 6 = 3 4 eli 3 tavalla) Helpommin ratkaistava versio: Kuinka monella eri tavalla n voidaan jakaa tekijöihin käyttäen vain m 2:ta tai suurempia lukuja? Perustapaus: 0 tapaa, jos m m > n
Rekursiivinen ajattelutapa: tekijöihinjakoesimerkki Määritellään proseduuria f (n, m) = kuinka monella tavalla n voidaan jakaa tekijöihin käyttäen vain m:ää tai suurempia lukuja? Perustapaus: f (n, m) = 0, jos m m > n Helpompien ongelmien (pienempi n tai isompi m lähestyy perustapausta) ratkaisujen avulla kaksi vaihtoehtoa: jos n ei ole jaollinen m:llä, f (n, m) = f (n, m + 1) (sillä tekijoissä ei ole m:ää) muuten f (n, m) = 1 + f (n/m, m) + f (n, m + 1). (esim. f (12, 2) = 1 + f (6, 2) + f (12, 3) eli 2 6, 6:n tavat, 3) Scheme-koodina (define (divisible? n m) (= (remainder n m) 0)) (define (f n m) (cond ((> (* m m) n) 0) ((divisible? n m) (+ 1 (f (/ n m) m) (f n (+ m 1)))) (else (f n (+ m 1))))) (define (ways-to-factor n) (f n 2)) factor.scm
Toinen rekursioesimerkki: Robotit ruudukossa ### # # ### # # # ### eräs tehtävä (tai osa siitä) viimevuotisesta NCPC-ohjelmointikilpailusta: robotti lähtee ruudukon vasemmasta yläkulmasta ja osaa liikkua vain oikealle tai alas, muttei seinään # voiko se päästä oikeaan alakulmaan, ja montako eri reittiä sinne on? miten ratkaisisit tämän Pythonilla tai Schemellä? rajapinta esim. (numpaths f n), missä n on ruudukon koko (n n) ja (f x y) palauttaa 1 jos ruudussa (x, y) on seinä, muuten 0 (tai Pythonissa numpaths(f, n), ja f voisi toki olla funktion sijaan kaksiulotteinen taulukko) älä mieti tehokkuutta vielä
Ratkaisu rekursiolla Scheme-koodina robotpaths.scm (define (numpaths f n) (define (paths-from x y) (cond ((or (>= x n) ; reunan yli? (>= y n) (= (f x y) 1)) ; seinässä? 0) ((= x y (- n 1)) ; perillä? 1) ; maalista itseensä 1 polku (else (+ (paths-from (+ x 1) y) ; oikealle (paths-from x (+ y 1)))))) ; ja alas (paths-from 0 0)) tällainen rekursiivinen ratkaisu on nopea tehdä, muttei tässä esimerkissä kovin tehokas ei siksi että rekursio olisi sinänsä hidasta, vaan koska apufunktiota kutsutaan monta kertaa samoilla argumenteilla ohjelmointikilpailussa tätä piti optimoida tallentamalla jo laskettuja apufunktion arvoja (eli nk. dynaamisella ohjelmoinnilla)
Generatiivinen ja strukturaalinen rekursio lisäesimerkki rekursiosta kirjan kohdassa 1.2.2 (Counting change) edellisten esimerkkien kaltainen päättely on yksi rekursion käyttötarkoitus rekursiota voi ajatella olevan kahdenlaista: rakenteellinen (structural) rekursio: käydään jokin monimutkainen tietorakenne läpi pala kerrallaan rekursiivisesti (esim. edellisen esimerkin ruudukko parempia esimerkkejä SICP luvussa 2 listojen yhteydessä) generatiivinen (generative) rekursio: ohjelma tuottaa osaongelmia itse (kuten tekijöihinjakoesimerkissä) (nämä termit eivät ole kovin yleisessä käytössä)