Ohjelmoinnin peruskurssien laaja oppimäärä Luento 1: SICP luku 1 Riku Saikkonen 1. 11. 2010
Sisältö 1 Kurssijärjestelyitä 2 SICP-kirjasta 3 Häntärekursio 4 Rekursio 5 Funktiot argumentteina 6 Funktiot paluuarvoina
Kurssijärjestelyitä kurssin sivuilla on nyt: SICP-kirjasta käsiteltävät kohdat ohjeita Schemen käytöstä ensimmäiset harjoitustehtävät (8 lyhyttä tehtävää, deadline 9.11.) harjoitustehtävien palautusjärjestelmä (Gobliniin) tulossa, toivottavasti jo huomenna opiskelijahuone tulossa, toivottavasti ensi viikolla hakekaa Niksulan tunnus (vaikka luennon jälkeen) pyyntö: seuratkaa ajankäyttöänne noin tunnin tarkkuudella kumpi tietokoneharjoitusaika? ke 1214 vai pe 1012?
Sisältö 1 Kurssijärjestelyitä 2 SICP-kirjasta 3 Häntärekursio 4 Rekursio 5 Funktiot argumentteina 6 Funktiot paluuarvoina
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 luota siihen, että ne olisi lukenut kurssisivuilla on luettelo kurssin kannalta oleellisista kohdista (muutakin voi toki lukea... ) kirjan alaviitteet ovat usein hyödyllisiä tekstiä ja koodia lukiessa voi miettiä esimerkiksi miten tekisin tämän jollain muulla kielellä? olisinko keksinyt tällaista ratkaisua itse?
Vähän terminologiaa lauseke (expression) tai joskus lause: 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 (expression) ja lause (statement) sekä proseduuri ja funktio
Globaalien ja paikallisten muuttujien määritteleminen Schemessä on monta tapaa määritellä muuttujia ja proseduureja; 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 em. säännöt riittävät käytännön ohjelmointiin
Kirjassa on usein abstraktia koodia (SICP 1.1.8) Neliöjuuren laskeva esimerkki (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)) Tyypillisesti Javalla double sqrt(double x) { double guess = 1.0; // while guess is not // good enough while (Math.abs(guess*guess - x) >= 0.001) // improve the guess guess = (guess + x/guess)/2; return guess; } varmaankin tästä syystä kirjan koodissa on melko vähän kommentteja: koodi yrittää olla itsedokumentoivaa Schemelläkin abstraktiot voisi tietysti kirjoittaa auki
Sisältö 1 Kurssijärjestelyitä 2 SICP-kirjasta 3 Häntärekursio 4 Rekursio 5 Funktiot argumentteina 6 Funktiot paluuarvoina
Silmukat ja häntärekursio (SICP 1.2.1) lausekielissä iteratiivista laskentaa tehdään usein silmukoilla (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 (esim. Javassa pino kasvaisi) Kertoma häntärekursiolla (define (factorial n) (define (iter p c) (if (> c n) p (iter (* c p) (+ c 1)))) (iter 1 1)) Javalla tai C:llä: int factorial(int n) { int p = 1, c = 1; while (c <= n) { p = c * p; c++; } 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 pino enemmän todellisuutta vastaavassa muodossa (tosin se riippuu toteutuksesta... ) Kutsupino: 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: 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 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)))
Sisältö 1 Kurssijärjestelyitä 2 SICP-kirjasta 3 Häntärekursio 4 Rekursio 5 Funktiot argumentteina 6 Funktiot paluuarvoina
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))
Generatiivinen rekursio kirjassa on toinen vastaava esimerkki kohdassa 1.2.2 (Counting change) tällainen päättely on yksi rekursion käyttötarkoitus variaatio siitä on algoritmien suunnittelussa yleinen hajoita ja hallitse -menetelmä (divide and conquer): ratkaistava ongelma jaetaan pienempiin osaongelmiin, joiden ratkaisut yhdistetään Merge sort -harjoitustehtävässä näkyy esimerkki tästä (osaongelma = puolet pienempi järjestettävä lista; yhdistäminen tehdään merge-proseduurilla)
Strukturaalinen rekursio toinen tapa käyttää rekursiota on kun jokin monimutkainen tietorakenne (esim. listarakenne) käydään läpi pala kerrallaan rekursiivisesti esimerkkejä seuraavalla luennolla ja SICP luvussa 2 myös esittelyluennon symbolinen derivoija oli esimerkki tästä tätä rekursion käyttötapaa sanotaan joskus rakenteelliseksi (structural) rekursioksi, ja edellistä generatiiviseksi (generative)
Sisältö 1 Kurssijärjestelyitä 2 SICP-kirjasta 3 Häntärekursio 4 Rekursio 5 Funktiot argumentteina 6 Funktiot paluuarvoina
Esimerkki funktioargumentista (SICP 1.31.3.2) Halutaan arvioida π:n arvoa kaavalla π 8 = 1 1 3 + 1 5 7 + 1 9 11 +. Ratkaisu oman summa-abstraktion avulla { b 0, jos a > b f (n) = f (a) + b n=next(a) f (n) muuten. n=a (define (sum f a next b) (if (> a b) 0 (+ (f a) (sum f (next a) next b)))) (define (pi-f x) (/ 1.0 (* x (+ x 2)))) (define (pi-next x) (+ x 4)) (define (pi-sum a b) (sum pi-f a pi-next b)) Testiajo: (* 8 (pi-sum 1 1000)) 3.139592655589783
Lambda on tapa tehdä nimettömiä funktioita Edelliseltä kalvolta (define (pi-f x) (/ 1.0 (* x (+ x 2)))) (define (pi-next x) (+ x 4)) (define (pi-sum a b) (sum pi-f a pi-next b)) lambda:n avulla (define (pi-sum a b) (sum (lambda (x) (/ 1.0 (* x (+ x 2)))) a (lambda (x) (+ x 4)) b)) apuproseduureja voi tehdä lambdalla keksimättä niille nimiä lambda on historiallinen nimi: loogisempi olisi ehkä make-procedure tms. lambdan vastine löytyy esim. Scalasta, Rubysta, Perlistä, JavaScriptistä, Luasta ja Pythonista (melkein) Schemessä lambda on myös primitiivisin tapa tehdä paikallisia muuttujia (ks. SICP 1.3.2)
Funktioargumentit Javassa 1/2 Java ei tue funktioiden (tai metodien) tallentamista muuttujiin tai antamista argumentiksi moni Javan rajapinta kiertää tätä rajoitusta yksimetodisilla rajapinnoilla; esimerkkejä: GUI-kirjastojen ActionListener (metodi actionperformed) järjestämisfunktioiden Comparator (metodi compare) monisäikeisyyden Runnable-rajapinta (metodi run) etu: luokan ja metodin nimestä saattaa nähdä, mihin tätä argumenttia tullaan käyttämään haittoja: argumenttina annettavan proseduurin koodi päätyy usein kauas kohdasta, jossa sitä käytetään proseduurin ulkopuolisia paikallisia muuttujia on työlästä käyttää proseduurin sisältä Schemessä nämä voisivat olla tavallisia proseduuriargumentteja
Funktioargumentit Javassa 2/2 toinen ongelma Javassa (myös esim. C:ssä) on, että on hankalaa tehdä sisäkkäisiä funktioita, jotka viittaavat ulompana oleviin muuttujiin tätä käytetään paljon hyväksi Schemessä ja funktionaalisessa ohjelmoinnissa rajoituksen syy liittyy siihen, miten funktio (Javassa sisäluokka) sekä paikalliset muuttujat ovat tallessa muistissa
Funktioargumentti vs. olio ja metodi 1/2 eräs funktionaalisen ja olio-ohjelmoinnin paradigmojen ero liittyy siihen, mitä asioita pidetään koodissa yhdessä olio-ohjelmoinnissa on tyypillistä yhdistää olion osien määrittely ja siihen liittyvät toiminnot esim. omaan luokkaan tehdään compare-metodi (implements Comparable), joka kertoo miten luokan alkioita verrataan funktionaalisessa ohjelmoinnissa on tyypillisempää yhdistää toiminto ja sen osatoiminnot esim. järjestämisfunktio ottaa argumentiksi funktion, jolla järjestettäviä alkioita verrataan etu: samat alkiot voi järjestää useaan eri järjestykseen (esim. eri kentän mukaan) antamalla eri funktioargumentin haitta: funktioargumentti tarvitsee tietoa järjestettävien alkioiden sisäisestä rakenteesta
Funktioargumentti vs. olio ja metodi 2/2 tapojen ero ei ole aina kovin selkeä, ja molempia tapoja voi osittain käyttää kummankin ohjelmointityylin seassa (merge-sort lst string<?) eli käytetään tietotyypin valmiiksi määrittelemää funktiota (tässä merkkijonojen vertailu) Javan Collections.sort-metodille voi antaa erillisen Comparator-olion molemmat ovat hiukan erilaisia tapoja abstrahoida koodia toinen vain sopii luontevammin yhteen funktionaalisen, toinen olio-ohjelmoinnin kanssa
Sisältö 1 Kurssijärjestelyitä 2 SICP-kirjasta 3 Häntärekursio 4 Rekursio 5 Funktiot argumentteina 6 Funktiot paluuarvoina
Esimerkki proseduurista paluuarvona (SICP 1.3.4) Yksinkertaista numeerista derivointia (define dx 0.00001) (define (deriv g) (lambda (x) (/ (- (g (+ x dx)) (g x)) dx))) ; Dg(x) = (g(x + dx) g(x))/dx Ajoesimerkkejä (define (cube x) (* x x x)) (define dcube (deriv cube)) (define (dcube x) ((deriv cube) x)) (dcube 5) 75.00014999664018 ((deriv cube) 5) 75.00014999664018 ; tai seuraava rivi:
Funktioiden käyttämisestä paluuarvoina funktioiden käyttö paluuarvoina on hieman harvinaisempaa kuin argumentteina yleisin käyttö lienee toisia funktioita muokkaavat funktiot (kuten deriv-esimerkki edellä) pidemmälle menevässä funktionaalisessa ohjelmoinnissa niitä näkee aina välillä esimerkiksi currying on tekniikka, jolla moniargumenttiset funktiot saa tehtyä yksiargumenttisten avulla samaa ideaa käytetään mm. Haskell-kielessä: jos funktiolle antaa vähemmän argumentteja kuin sen pitäisi saada, kutsu palauttaa funktion, joka ottaa loput argumentit ja kutsuu alkuperäistä esim. (< 0) olisi sama kuin (lambda (x) (< 0 x)), jos Scheme käyttäisi tätä ideaa argumentin ja paluuarvon lisäksi kolmas tapa kuljettaa funktioita on tallettaa niitä tietorakenteisiin myöhempää käyttöä varten