Ohjelmoinnin peruskurssien laaja oppimäärä Luento 1: Rekursiivinen ajattelutapa, Scheme-kielen perusteita (mm. SICP 11.2.4) Riku Saikkonen 10. 10. 2011
Sisältö 1 Kurssijärjestelyitä 2 Perusteita Scheme-kielestä, häntärekursio 3 Rekursio 4 Lukuohjeita SICP-kirjaan
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 21.10. ja 7.11.) hakekaa itsellenne Niksulan tunnus (T-talon huoneesta B210, vaikka luennon jälkeen) tehtäviin saa apua IRC:stä (kurssin kanavalta tai assistentilta) voimme järjestää myös laskuharjoituksia tms., jos niille näyttää olevan tarvetta pyyntö: olisi kiva, jos seuraisitte kurssiin kuluvaa aikaa sillä yritämme saada laajankin version syksyn kurssista mahtumaan 5 op:een eli noin 133 tuntiin
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ä hyvä debuggeri melko helppo liittää muihin ohjelmointikieliin kurssin käyttämä Scheme-toteutus löytyy Niksulasta: use scheme; scheme laajan kurssin sivuilla on ohje sen asentamisesta omalle koneelle
Sisältö 1 Kurssijärjestelyitä 2 Perusteita Scheme-kielestä, häntärekursio 3 Rekursio 4 Lukuohjeita SICP-kirjaan
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 (define (square n) (* n n)) (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. iter(p, c) = iter(c p, c + 1) muuten Scheme (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 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))))) 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 (define (f-iter p c n) (if (> c n) p (f-iter (* c p) (+ c 1) n))) (define (fact n) (f-iter 1 1 n)) nyt (fact 4):ää evaluoidessa menossa on koko ajan vain yksi f-iter:n kutsu 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... ensin (f-iter 1 1), sitten (f-iter 1 2),..., lopuksi (f-iter 24 5), joka palauttaa 5
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 periaatteessa 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))))) do (define (factorial n) (do ((p 1 (* c p)) (c 1 (+ c 1))) ((> c n) p)))
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 4 Lukuohjeita SICP-kirjaan
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 eri 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). 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))
Toinen rekursioesimerkki: Robotit ruudukossa ### # # ### # # # ### eräs tehtävä (tai osa siitä) tämänvuotisesta 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 (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ä)
Sisältö 1 Kurssijärjestelyitä 2 Perusteita Scheme-kielestä, häntärekursio 3 Rekursio 4 Lukuohjeita SICP-kirjaan
Lukuohjeita nopea tapa seurata oppikirjaa (SICP) on jättää väliin: tehtävät (tosin niitä on toki opettavaista miettiä... ) example-kohdat myöhempi teksti ei oleta, että ne olisi lukenut kurssisivuilla on luettelo kurssin kannalta oleellisista kohdista (muutakin voi toki lukea... ) tekstiä ja koodia lukiessa voi miettiä esimerkiksi miten tekisin tämän jollain muulla kielellä? (entä jollain muulla kuin Pythonilla?) olisinko keksinyt tällaista ratkaisua itse? kirjan alaviitteet ovat usein opettavaisia
Kirjassa on usein abstraktia koodia (SICP 1.1.8) Neliöjuuren laskeva esimerkki (define (square x) (* x x)) (define (average x y) (/ (+ x y) 2)) (define (sqrt x) (define (good-enough? guess) (< (abs (- (square guess) x)) 0.001)) (define (improve guess) (average guess (/ x guess))) (define (sqrt-iter guess) (if (good-enough? guess) guess (sqrt-iter (improve guess)))) (sqrt-iter 1.0)) kirjan koodissa on melko vähän kommentteja, sillä se yrittää olla itsedokumentoivaa Schemelläkin abstraktioita voisi toki tehdä vähemmänkin Sama Pythonilla def average(x, y): return (x+y)/2 def my_sqrt(x): def good_enough(guess): return abs(guess*guess-x) < 0.001 def improve(guess): return average(guess, x/guess) guess = 1.0 while not good_enough(guess): guess = improve(guess) return guess Tyypillisemmin Pythonilla def my_sqrt(x): guess = 1.0 # while guess isn't good enough while (abs(guess*guess - x) >= 0.001): # improve the guess guess = (guess+x/guess)/2 return guess
Vähän terminologiaa lauseke (expression) tai joskus lause (statement): 3 (+ x 4) (f 12) (if (> x 0) 3 (- x 4)) (let ((x (f 3))) (display x) (newline)) cond-lausekkeen ehtoseuraus-pari (clause): (cond ((= x 0) 99) ((> x 0) (fact x)) (else -1)) (muuttuja)sidonta (binding): (define z 3) (let ((x 3) (y (f 3)))...) proseduuri (procedure) eli funktio joissain muissa ohjelmointikielissä on erikseen lauseke ja lause sekä proseduuri ja funktio