Ohjelmoinnin peruskurssien laaja oppimäärä Luento 2: Funktioiden käyttöä, lisää Schemestä, listat (mm. SICP 1.31.3.4, osin 22.2.3) Riku Saikkonen 17. 10. 2011
Sisältö 1 Scheme-ohjelmointikäytäntöjä 2 Funktiot argumentteina ja paluuarvoina 3 Funktio vai olio? 4 Gambit-C:n Scheme-debuggeri 5 Linkitetyt listat 6 Listarakenteet
Miten Schemeä käytännössä koodataan? pienten ohjelmien tekemiseen on kolme perustapaa: (näistä tarkemmin kurssisivulla Scheme-ohjeita) 1 pelkän Scheme-tulkin käyttäminen lähinnä kokeiluun (yksirivisille ohjelmille) 2 koodi tiedostossa editorissa ja erillinen Scheme-tulkki (esim. omassa ikkunassa) ohjelman kokeiluun tyypillinen tapa muissa kielissä, erityisesti käännetyissä kielissä samoin monimutkaisemmissa ohjelmissa varsinkin jos ohjelmalla on oma käyttöliittymä sen sijaan että vain kokeillaan proseduureja käsin tulkissa 3 Scheme-tulkki editorin sisällä tulkkiin voi helposti ladata editoitavana olevan koodin yleinen tapa Schemessä pienten ohjelmien ja apuproseduurien tekemisessä joskus käytetään muissakin kielissä (esim. Python-tulkin saa mm. Emacsin sisään)
Miten tulostetaan? jos haluaa tehdä itsenäisiä ohjelmia tai kokeilla niitä ilman että syöttää lausekkeita tulkkiin, ohjelman itsensä pitää tulostaa jotain Schemen tulostusprimitiivit: (display x ) tulostaa lausekkeen x arvon (osaa tulostaa melkein kaikkea paitsi proseduureja) (write x ) samoin, mutta koneluettavassa muodossa (jota read-primitiivi osaa lukea) vrt. Pythonin repr() (newline) tulostaa rivinvaihdon Gambit-C:ssä on myös mm. print ja println tulostaessa tarvitsee lisäksi usein merkkijonoja: "foo" on merkkijonovakio (esim. (display "foo") tulostaa foo) (string-append x y ) yhdistää kaksi merkkijonoa peräkkäin (number->string x ) muuttaa numeron merkkijonoksi muitakin on (ks. R 5 RS-standardin kohta 6.3.5), mutta niitä ei juuri kokeiluissa tarvitse
Esimerkki koodia testaavasta pääohjelmasta Kokonainen ohjelma ;; Testattava koodi (define (fact n) (if (= n 1) ; bugi! 1 (* n (fact (- n 1))))) fact.scm ;; Pääohjelma (nimi main voisi olla joku muukin) (define (main) (define (test i) (display "fact ") (display i) (display " => ") (display (fact i)) (newline)) (test 1) (test 10) (test 5) (test 0)) ;; Ajetaan pääohjelma tiedostoa ladattaessa (main)
Esimerkki tulostamisesta Edellisen luennon robottiesimerkki tulostuksella robotpaths-show.scm (define (numpaths-show f n show-all) ; muutokset punaisella (define (paths-from x y prefix) (cond ((or (>= x n) (>= y n) (= (f x y) 1)) (if show-all (begin (display prefix) (display "#") (newline))) 0) ((= x y (- n 1)) (display prefix) (display "!") (newline) 1) (else (+ (paths-from (+ x 1) y (string-append prefix ">")) (paths-from x (+ y 1) (string-append prefix "v")))))) (paths-from 0 0 "")) cond:ssa voi siis ehdon jälkeen olla monta lauseketta if:n haarassa tarvitaan begin: (begin x 1 x 2 x n ) ajaa kaikki lausekkeet ja palauttaa viimeisen arvon
Sisältö 1 Scheme-ohjelmointikäytäntöjä 2 Funktiot argumentteina ja paluuarvoina 3 Funktio vai olio? 4 Gambit-C:n Scheme-debuggeri 5 Linkitetyt listat 6 Listarakenteet
Esimerkki funktioargumenteista (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 f (n) = f (a) + b n=next(a) f (n) n=a 0, jos a > b muuten. (define (sum f a next b) def sum(f, a, next, b): (if (> a b) if a > b: 0 return 0 (+ (f a) else: (sum f (next a) next b)))) return f(a) + sum(f, next(a), next, b) (define (pi-f x) def pi_f(x): (/ 1.0 (* x (+ x 2)))) return 1.0 / (x*(x+2)) (define (pi-next x) def pi_next(x): (+ x 4)) return x + 4 (define (pi-sum a b) def pi_sum(a, b): (sum pi-f a pi-next b)) return 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)) lambdan 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 tosin Pythonissa lambdan sisältöä on rajoitettu: vain yksi lauseke, jonka eteen tulee automaattisesti return Schemessä lambda on myös primitiivisin tapa tehdä paikallisia muuttujia (ks. SICP 1.3.2)
Esimerkki funktiosta paluuarvona (SICP 1.3.4) Yksinkertaista numeerista derivointia (define dx 0.00001) (define (deriv g) (lambda (x) (/ (- (g (+ x dx)) (g x)) dx))) Ajoesimerkki (define (cube x) (* x x x)) ; Dg(x) = (g(x + dx) g(x))/dx (define dcube (deriv cube)) ; tai seuraava rivi: (define (dcube x) ((deriv cube) x)) (dcube 5) 75.00014999664018 ((deriv cube) 5) 75.00014999664018
Esimerkki abstrahoinnista funktioargumenteilla Abstraktimpi versio robottiesimerkistä robotpaths-abs.scm (define (search-right-and-down start-x start-y invalid-position? at-wall? at-goal?) (define (paths-from x y) (cond ((invalid-position? x y) 0) ((at-wall? x y) 0) ((at-goal? x y) 1) (else (+ (paths-from (+ x 1) y) (paths-from x (+ y 1)))))) (paths-from start-x start-y)) (define (numpaths f n) (search-right-and-down 0 0 (lambda (x y) (or (>= x n) (>= y n))) (lambda (x y) (= (f x y) 1)) (lambda (x y) (= x y (- n 1))))) (listojen avulla voisi abstrahoida myös robotin liikevaihtoehdot) mitä etua tästä abstraktiosta on? entä haittaa?
Sisältö 1 Scheme-ohjelmointikäytäntöjä 2 Funktiot argumentteina ja paluuarvoina 3 Funktio vai olio? 4 Gambit-C:n Scheme-debuggeri 5 Linkitetyt listat 6 Listarakenteet
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 datan rakenne (esim. luokan kentät) ja olioon liittyvät toiminnot esim. uuteen luokkaan tehdään metodi, joka kertoo miten sen luokan alkioita verrataan (Pythonissa esim. lt -metodi, Javassa Comparable-rajapinnan compare-metodi) 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 siis kaksi tapaa: liitetään toiminto (esim. alkioiden vertailu) dataan (olion metodi) tai operaatioon jossa sitä tarvitaan (järjestämisfunktion funktioargumentti) tapojen ero ei ole aina kovin selkeä, ja esim. jokin rajapinta voi tukea kumpaakin tapaa Javan Collections.sort-metodille voi antaa erillisen Comparator-olion Pythonin sorted()-funktio käyttää lt -metodia, tai sille voi antaa funktioargumentin, joka muuntaa olion toiseksi vertailua varten, tai vertailufunktion nämä kaksi ovat hiukan erilaisia tapoja abstrahoida koodia toinen vain sopii luontevammin yhteen funktionaalisen, toinen olio-ohjelmoinnin kanssa
Funktioiden käyttämisestä paluuarvoina funktioiden käyttö paluuarvoina on hieman harvinaisempaa kuin argumentteina käyttökohteita esimerkiksi: toisia funktioita muokkaavat funktiot (kuten deriv edellä) laskennan viivästäminen: jätetään jokin (mahdollisesti tarpeeton) toiminto kesken ja palautetaan funktio, jolla voi tarvittaessa tehdä sen loppuun teoreettisempi esimerkki: currying on tekniikka, jolla moniargumenttiset funktiot saa tehtyä yksiargumenttisten avulla 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
Funktiot Javassa Java-kieli ei suoraan tue funktioiden (tai metodien) tallentamista muuttujiin, palauttamista 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 funktion koodi päätyy usein kauas kohdasta, jossa sitä käytetään tälle funktiolle on hankala antaa käyttöön paikallisia muuttujia siitä kohdasta, jossa se annettiin argumentiksi Schemessä ja Pythonissa nämä voisivat olla tavallisia funktioargumentteja
Funktiot C:ssä ja Javassa C-kieli tukee funktioargumentteja ja -paluuarvoja rajoituksin: C:ssä funktio on vain sen koodi; Schemessä ja Pythonissa siihen kuuluvat myös määrittelykohdan paikallisten muuttujien arvot joten C:ssä funktio ei voi aina käyttää ulompana olevia paikallisia muuttujia (itse asiassa standardi-c ei edes tue sisäkkäisiä funktiomäärittelyjä) Javassa funktioargumenttia ja -paluuarvoa vastaa yksimetodinen sisäluokka, mutta siinäkin on rajoituksia ympäröivän koodin paikallisia muuttujia ei voi kaikissa tilanteissa käyttää molempien rajoitusten syynä on muistinhallinta: C ja Java tallettavat paikalliset muuttujat kutsupinoon, josta ne katoavat kun funktio palaa tästä lisää myöhemmin tulkkien yhteydessä...
Sisältö 1 Scheme-ohjelmointikäytäntöjä 2 Funktiot argumentteina ja paluuarvoina 3 Funktio vai olio? 4 Gambit-C:n Scheme-debuggeri 5 Linkitetyt listat 6 Listarakenteet
Debuggeriin pääseminen Scheme-tulkista Scheme-lauseke (begin (step) (foo)) pysähtyy debuggeriin juuri ennen lausekkeen (foo) suoritusta Ctrl-C (Emacs: C-c C-c) kesken pitkän suorituksen pysähtyy myös debuggeriin samoin virheilmoitus, esim. (/ 1 0) keskellä koodia (break foo) muokkaa päätason proseduuria foo niin että juuri ennen sen kutsua pysähdytään debuggeriin; (unbreak foo) kumoaa debuggeritilan tunnistaa tulkin kehotteesta: > ei debuggeria, 1>, 2> jne. debuggerissa numero kertoo monennessako sisäkkäisessä (!) debuggerissa ollaan
Debuggerikomentoja Ctrl-D tai,d poistuu päällimmäisestä debuggerista,t poistuu kokonaan debuggerista takaisin tulkkiin,? näyttää komentolistauksen,b näyttää kutsupinon,be näyttää kutsupinon muuttujineen, numero menee kutsupinon tiettyyn kohtaan,c jatkaa suoritusta normaalisti,(c x ) jatkaa suoritusta virheen jälkeen: virheen aiheuttanut lauseke palauttaa x,s jatkaa lyhyen askelen eteenpäin,l tekee funktiokutsun ja pysähtyy debuggeriin sen palattua ja debuggerissa voi suorittaa Scheme-lausekkeita normaalisti (esim. katsoa muuttujan arvoa kirjoittamalla sen nimi)
Sisältö 1 Scheme-ohjelmointikäytäntöjä 2 Funktiot argumentteina ja paluuarvoina 3 Funktio vai olio? 4 Gambit-C:n Scheme-debuggeri 5 Linkitetyt listat 6 Listarakenteet
Linkitetyt listat (SICP 2.1.1, 2.2.1) funktionaalinen ohjelmointitapa perustuu usein laskemiseen linkitettyjen listojen käsittelyfunktioilla Schemessä (cons a b) tekee parin, josta muodostetaan listoja (car p) palauttaa parin ensimmäisen alkion (cdr p) toisen (eli listan sen osan, jossa on loput alkiot) nämä nimet ovat historiallisia; esim. make-pair, first ja rest olisivat varmaankin loogisempia linkitetty lista (list) on jono pareja, joka cdr:iä seuraten päättyy tyhjään listaan nil (tai '()): (cons 1 (cons 2 (cons 3 nil))) lista (1 2 3) listoja voi tuottaa myös näin: (list 1 2 3) ja '(1 2 3) huomaa, että (1 2 3) on tapa, jolla listat tulostetaan, mutta koodina se yrittäisi kutsua 1-nimistä proseduuria
Schemen ja muiden kielten listat monissa muissakin kielissä on sisäänrakennetut linkitettyjen listojen vastineet (esim. Javan List-luokka; Pythonin listat on rakennettu hieman eri tavalla taulukkoina) Schemen listat ovat rakenteeltaan mahdollisimman yksinkertaisia jotta niitä voisi käyttää kätevästi esim. rekursiivisesti niiden päälle voi rakentaa monimutkaisempia tietorakenteita esimerkiksi lista on vain yhteen suuntaan linkitetty, jotta osalistoja voisi antaa eteenpäin muille funktioille samanlaiset listat (ja samat listankäsittelyoperaatiot) on muissakin funktionaalisissa kielissä Schemen listoja on tapana käyttää enimmäkseen funktionaalisesti (tehdään uusi lista eikä muuteta olemassaolevaa)
Esimerkki listojen käytöstä Muutama listaoperaatio lists.scm (define (sum-list l) (if (null? l) 0 (+ (car l) (sum-list (cdr l))))) (define (square-list l) (define (square x) (* x x)) (if (null? l) nil ; tai '() (cons (square (car l)) (square-list (cdr l))))) Testiajoja: (sum-list (cons 1 (cons 2 nil))) 3 (sum-list (list 1 10 2 8 5)) 26 (square-list (list 5 11 3 8)) (25 121 9 64) nämä voisi tehdä kätevämmin Schemessä valmiina olevilla listankäsittelyproseduureilla, joista lisää seuraavalla luennolla
Teoriassa listoja ei tarvittaisi (SICP 2.1.3) periaatteessa listoja ei tarvittaisi kieleen sisäänrakennettuna ominaisuutena, vaan ne voisi rakentaa funktioista (samoin muutkin tietorakenteet kuin listat, kunhan niitä ei voi muuttaa) toteutukseksi käyvät mitkä tahansa proseduurit cons, car ja cdr, jotka toteuttavat alla olevat ehdot (sekä tyhjä lista ja null?) käytännössä tämä ei toki olisi kovin tehokasta... Listojen toteutus proseduureina (SICP tehtävästä 2.4) (define (cons x y) ; x ja y jäävät talteen palautettuun proseduuriin (lambda (m) (m x y))) (define (car z) ; toteuttaa ehdon (car (cons a b)) = a (z (lambda (p q) p))) (define (cdr z) ; toteuttaa ehdon (cdr (cons a b)) = b (z (lambda (p q) q))) (define nil 999) ; mikä tahansa tunnistettava vakioalkio käy Testiajo: (car (cdr (cons 1 (cons 2 (cons 3 nil))))) 2
Sisältö 1 Scheme-ohjelmointikäytäntöjä 2 Funktiot argumentteina ja paluuarvoina 3 Funktio vai olio? 4 Gambit-C:n Scheme-debuggeri 5 Linkitetyt listat 6 Listarakenteet
Listarakenteet (SICP 2.2.1, 2.2.2) sisäkkäiset listat sekä listat, jotka eivät pääty nil-alkioon, muodostavat listarakenteen (list structure) lista on listarakenne, joka päättyy tyhjään listaan sisäkkäiset listat toimivat luontevasti: (list (list 1 2 3) (list 4 5) (list 6)) ((1 2 3) (4 5) (6)) listarakenne, joka ei pääty tyhjään listaan, näytetään näin: (cons 1 (cons 2 3)) (1 2. 3) siis pisteen. jälkeen tulostetaan yksi alkio eli viimeisen parin cdr toinen selitys tulostamiselle: (cons a b) tulostetaan (a. b), paitsi (cons a nil) tulostetaan (a) (cons a lista ) tulostetaan (a lista tulostettuna ) ilman näitä lyhennysmerkintöjä (cons 1 (cons 2 nil)) olisi (1. (2. ())) eikä (1 2)
Listarakenteen tulostus koodina Listarakenteen tulostava Scheme-koodi (define (print-contents l) ; apufunktio (print-list-structure (car l)) (cond ((null? (cdr l)) 'done) ((not (pair? (cdr l))) (display ". ") (print-list-structure (cdr l))) (else (display " ") (print-contents (cdr l))))) print-list.scm (define (print-list-structure l) (cond ((null? l) (display "()")) ((not (pair? l)) (display l)) (else (display "(") (print-contents l) (display ")")))) Tulostusesimerkkejä: (list (list 1 2) (list 3 4)) ((1 2) (3 4)) (cons (list 1 2) (list 3 4)) ((1 2) 3 4) (list (cons 1 2) (cons 3 4)) ((1. 2) (3. 4)) (cons (cons 1 2) (cons 3 4)) ((1. 2) 3. 4)