Ohjelmoinnin peruskurssien laaja oppimäärä Luento 4: SICP kohta 3.3.5 ja funktionaalista ohjelmointia Riku Saikkonen 15. 11. 2010
Sisältö 1 Ensimmäisen kierroksen tehtävistä 2 SICP 3.3.5: rajoitteiden vyörytysjärjestelmä 3 Funktionaalista ohjelmointia: virrat 4 Funktionaalista ohjelmointia: ohjelmien todistaminen
Ensimmäisen kierroksen tehtävistä melkein kaikki saivat tehtyä kaikki 8 tehtävää oikein (loistavaa!) tehtävistä 1.7 (merge-sort) ja 1.8 (deep-reverse): (append l1 l2) käy listan l1 läpi ja tekee siitä kopion, cons ei tehokkuusmielessä siis kannattaa käyttää consia silloin kun voi listasta voi tehdä kopion käymällä sen läpi rekursiivisesti (kuten esim. mapin toteutus), sen sijaan häntärekursiivinen lähestymistapa kääntää listan ympäri
Listan läpikäynti Rekursiivinen map (define (my-map-1 f l) (if (null? l) nil (cons (f (car l)) (my-map-1 f (cdr l))))) Häntärekursiivinen map (define (my-map-2 f l) (define (iter result left) (if (null? left) result (iter (cons (f (car left)) result) (cdr left)))) (iter nil l)) Testiajoja (define (square x) (* x x)) (my-map-1 square (list 1 2 3 4)) (1 4 9 16) (my-map-2 square (list 1 2 3 4)) (16 9 4 1) (reverse (my-map-2 square (list 1 2 3 4))) (1 4 9 16)
Sisältö 1 Ensimmäisen kierroksen tehtävistä 2 SICP 3.3.5: rajoitteiden vyörytysjärjestelmä 3 Funktionaalista ohjelmointia: virrat 4 Funktionaalista ohjelmointia: ohjelmien todistaminen
Rajoitteiden vyöyrytysjärjestelmä (SICP 3.3.5) Kirjan kohdan 3.3.5 isompi esimerkki: olioiden käytöstä viestien välitykseen (kutsu olion metodia lähetä oliolle viesti) oman kielen tekemisestä Schemen päälle (joukko proseduureja antaa ohjelmoijalle uuden tavan puhua) joskus laskentaa voi tehdä myös takaperin Perustoiminta järjestelmän käyttäjän näkökulmasta: 1 määritellään matemaattinen kaava, esim. 9C = 5(F 32), järjestelmän omalla kielellä 2 asetetaan osalle muuttujista (C tai F ) alkuarvot 3 kysytään haluttujen muuttujien arvoa (järjestelmä laskee kaavan perusteella kaikki muut arvot) 4 (voidaan käskeä myös unohtamaan annettuja arvoja ja seurata laskennan etenemistä)
Järjestelmän käyttö: rajoiteverkko matemaattinen kaava määritellään rajoiteverkkona, jossa on liitoksia (connector, viivat) ja rajoitteita (constraint, laatikot) liitoksessa on tallessa yksi arvo, jonka voi asettaa ja unohtaa rajoite pakottaa halutut liitokset noudattamaan tiettyjä ehtoja vakiorajoite liittyy yhteen liitokseen ja pakottaa sille tietyn arvon yhteenlaskurajoite liittyy kolmeen liitokseen a, b, c ja pakottaa niille ehdon a + b = c (jos kahdella on arvo, kolmas lasketaan) kertolaskurajoite samoin a b = c järjestelmä vyöryttää (propagate) automaattisesti arvojen muutokset kaikkiin suuntiin rajoiteverkkoa pitkin Esimerkki: Rajoiteverkko yhtälölle 9C = 5(F 32) C m1 * m2 p u p m1 * m2 v a1 + a2 F w 9 5 x y 32
Järjestelmän käyttö: proseduurit järjestelmää käytetään Schemeproseduureilla (kuten kirjastoa tai Schemen päälle rakennettua kieltä) (make-connector) luo liitoksen (multiplier a b c) luo rajoitteen, joka yhdistää liitokset a, b ja c niin että a b = c Lyhyt käyttöesimerkki (define a (make-connector)) (define b (make-connector)) (define c (make-connector)) (adder a b c) (set-value! a 11 'user) (set-value! c 25 'user) (get-value b) 14 (adder a b c) luo rajoitteen, joka yhdistää liitokset a, b ja c niin että a + b = c (constant 3.14 a) luo rajoitteen, joka pakottaa liitokselle a arvon 3.14 (probe nimi a) luo rajoitteen, joka ei vaikuta arvoihin, mutta kertoo aina kun liitoksen a arvo muuttuu (set-value! a 123 'user) asettaa liitoksen a arvoksi 123 (forget-value! a 'user) käskee liitosta a unohtamaan arvonsa (get-value a) kertoo liitoksen a nykyisen arvon
Käyttöesimerkki: CelsiusFahrenheit-muunnos Rajoiteverkko yhtälölle 9C = 5(F 32) C m1 * m2 p u p m1 * m2 v a1 + a2 F w 9 5 x y 32 Rajoiteverkkoa vastaava koodi (define (celsius-fahrenheit-converter c f) (let ((u (make-connector)) (v (make-connector)) (w (make-connector)) (x (make-connector)) (y (make-connector))) (multiplier c w u) (multiplier v x u) (adder v y f) (constant 9 w) (constant 5 x) (constant 32 y) 'ok)) Lyhyempi syntaksi (harjoitustehtävänä) ;; palauttaa f:n (define (c-f-conv c) (c+ (c* (c/ (cv 9) (cv 5)) c) (cv 32)))
Rajoiteverkon käyttäminen Käyttöesimerkki celsius-fahrenheit-converterille (define C (make-connector)) (define F (make-connector)) (celsius-fahrenheit-converter C F) (probe "Celsius temp" C) (probe "Fahrenheit temp" F) (set-value! C 25 'user) Probe: Celsius temp = 25 Probe: Fahrenheit temp = 77 done (set-value! F 212 'user) Error! Contradiction (77 212) (forget-value! C 'user) Probe: Celsius temp =? Probe: Fahrenheit temp =? done (set-value! F 212 'user) Probe: Fahrenheit temp = 212 Probe: Celsius temp = 100 done
Järjestelmän toteutus kaksi oliorajapintaa: connector: viestit has-value?, value, set-value!, forget, connect vain arvon alkuperäinen asettaja (kerrotaan set-value!:ssa) voi saada liitoksen unohtamaan (forget) arvon kaksi set-value!:ta eri arvoilla aiheuttaa koniktin (virheen) levittää uuden arvon eteenpäin muille liitoksen rajoitteille rajapinta constraint: viesti I-have-a-value levittää kaikki arvot uudelleen viesti I-lost-my-value lisäksi käskee ensin unohtamaan tämän rajoitteen asettamat arvot adder, multiplier, constant ja probe toteuttavat constraint-rajapinnan arvojen laskemiseen ja levittämiseen liittyvä logiikka esim. adderissa eri koodit kolmeen suuntaan laskemiseen (c = a + b, b = c a, a = c b) helpomman Scheme-syntaksin saamiseksi myös metodeja vastaavat proseduurit, esim. (has-value? connector )
Miten tämä on keksitty? taustalle tarvitaan ajatus siitä, että kaavoja (tai muita ratkaistavia ongelmia) voi esittää tällaisina rajoiteverkkoina ideaa on laajennettu (harvinaiseksi) ohjelmointiparadigmaksi nimeltä rajoiteohjelmointi (constraint programming), jota käytetään erityisesti logiikkaohjelmoinnin yhteydessä idea lienee peräisin tekoälytutkimuksesta 19601970-luvulta toteutuksessa olioajattelu (minulla on arvo, jonka levitän eteenpäin) toimii hyvin, koska rajoitteet ja liitokset ovat itsenäisiä olennaisin idea lienee se, että liitoksissa on logiikkaa ei siis niin että rajoitteissa olisi suoraan listoja muista rajoitteista, ja ne itse kävisivät listojaan läpi etsien esim. konikteja eikä niin että ulkopuolinen proseduuri tutkisi tietorakenteena esitettyä verkkoa ja päättelisi, minne arvoja pitää levittää toinen olennainen kohta lienee se, miten arvojen unohtaminen toimii (vain arvon asettaja voi pyytää unohtamaan sen) ja ettei tarvitse tehdä ensin omaa kieltä verkkojen esittämiseen
Piirisimulaattoriesimerkki (SICP 3.3.4) kirjan kohdan 3.3.4 digitaalisten piirien simulaattori on samankaltainen kuin rajoitejärjestelmä: eroja: käytetään hyvin samaan tapaan (rajoitteen tilalla esim. or-portti) myös tässä piirien muodostama verkko tehdään suoraan tulkissa Scheme-proseduureja kutsumalla arvot etenevät vain yhteen suuntaan (piireillä on input- ja output-signaaleja) järjestelmä simuloi aikaa eli etenemisviivettä (output-signaali muuttuu vasta vähän sen jälkeen kun input-signaali muuttui)
Sisältö 1 Ensimmäisen kierroksen tehtävistä 2 SICP 3.3.5: rajoitteiden vyörytysjärjestelmä 3 Funktionaalista ohjelmointia: virrat 4 Funktionaalista ohjelmointia: ohjelmien todistaminen
Virrat eli mahdollisesti äärettömät listat (SICP 3.5) virta (stream) on lista, josta mahdollisesti vain osa on valmiina samoin kuin lista, virta muodostuu tyhjästä virrasta the-empty-stream (sama kuin nil) pareista, joita tehdään cons-stream:lla parin osat ovat stream-car ja stream-cdr virta on jono pareja joka päättyy tyhjään virtaan virtojen erikoisuus on se, että virtaparin cdr:ää ei evaluoida paria tehdessä, vaan vasta kun koodi menee sinne joten voi määritellä myös äärettömän virran, jonka cdr:istä löytyy lisää alkioita niin paljon kun niitä jaksaa hakea virrat ovat käytössä muuallakin funktionaalisessa ohjelmoinnissa: Haskell-kielessä kaikki listat toimivat näin virtojen vastine löytyy mm. Scalasta joissain toteutuksissa myös car lasketaan vasta tarvittaessa
Miksi virtoja? (SICP 3.5.1) listankäsittelyoperaatioissa on se ongelma, että (ainakin periaatteessa) niissä tehdään usein pitkiä välituloslistoja ennen lopullisen tuloksen laskemista virrat auttavat tässä: välituloslistoista lasketaan vain niin paljon alkioita kuin jatkoa varten on tarpeen toinen virtojen etu on, että äärettömiä virtoja määrittelemällä ja käyttämällä voi saada aikaan siistimpää koodia kolmas virtojen käyttötarkoitus on muuttuvan tilan mallinnuksessa tallennetaan virtaan peräkkäisiä tiloja sen sijaan että esim. olio muuttaisi omia tilamuuttujiaan ei-funktionaalisesti virroilla voi tehdä myös I/O:ta funktionaalisesti (lisää tästä myöhemmällä luennolla) huono puoli: virtoja käytettäessä on vaikea ennustaa, milloin tietty koodi suoritetaan tästä syystä virtoja käytetään yleensä vain puhtaasti funktionaalisessa koodissa (ei sijoituslauseiden kanssa)
Äärettömät virrat (SICP 3.5.2) rekursion avulla voi määritellä äärettömiä virtoja virta voi myös viitata itseensä cdr:ssä useimmat listojenkäsittelyoperaatiot toimivat samaan tyyliin myös äärettömillä virroilla esim. (stream-map f s) palauttaa uuden äärettömän virran kaikki eivät toimi: esim. virran reverse jäisi etsimään viimeistä alkiota Esimerkkejä (define ones (cons-stream 1 ones)) ; (1 1 1 1 1 1...) (define (integers-starting-from n) (cons-stream n (integers-starting-from (+ n 1)))) (define integers (integers-starting-from 1)) ; (1 2 3...) (define (fibgen a b) (cons-stream a (fibgen b (+ a b)))) (define fibs (fibgen 0 1)) ; (0 1 1 2 3 5 8 13 21...)
Esimerkki: kaikki alkuluvut sisältävä virta (SICP 3.5.2) Esimerkkikoodi (define (divisible? x y) (= (remainder x y) 0)) (define (integers-starting-from n) (cons-stream n (integers-starting-from (+ n 1)))) (define (sieve stream) (cons-stream (stream-car stream) (sieve (stream-filter (lambda (x) (not (divisible? x (stream-car stream)))) (stream-cdr stream))))) (define primes (sieve (integers-starting-from 2))) stream-filter on samanlainen kuin filter, mutta virroille (kirjassa on samoin stream-map jne.)
Esimerkki: neliöjuuren laskenta (SICP 3.5.3) Esimerkkikoodi (define (sqrt-improve guess x) (average guess (/ x guess))) (define (sqrt-stream x) (define guesses (cons-stream 1.0 (stream-map (lambda (guess) (sqrt-improve guess x)) guesses))) guesses) Testiajo: (sqrt-stream 2) (1. 1.5 1.416667 1.414216...) kirjan alussa (luku 1.1) laskettiin samaa perinteisellä tavalla, ohjelman tilaa häntärekursiolla muuttaen tässä ratkaisussa ei tarvitse miettiä edellistä ja seuraavaa tilaa eikä miten niiden välillä siirrytään: kaikki tilat ovat virrassa tallessa
Sisältö 1 Ensimmäisen kierroksen tehtävistä 2 SICP 3.3.5: rajoitteiden vyörytysjärjestelmä 3 Funktionaalista ohjelmointia: virrat 4 Funktionaalista ohjelmointia: ohjelmien todistaminen
Ohjelmien oikeaksi todistamisesta eräs funktionaalisuuden etu on, että ohjelmista on helpompi todistaa ominaisuuksia, jos ne ovat puhtaasti funktionaalisia imperatiivisen koodin todistamisessa mietitään yleensä ohjelman tilaa tietyillä hetkillä; todistus on usein ketju välitiloja alkutilasta (argumentit) lopputilaan, joka toteuttaa todistettavan ehdon Todistus: kaikille listoille l: (accumulate cons nil l) l (lista = nil:iin päättyvä jono pareja) 1 Jos l = nil: (accumulate cons nil nil) nil (accumulate:n määritelmästä) 2 Jos l = pari (h,t) eli ((car l), (cdr l)): (accumulate cons nil (cons h t)) (cons h (accumulate cons nil t)) (taas määritelmästä, op = cons) Koska t on lyhyempi kuin (cons h t), tämä riittää induktioon.
Listankäsittelyproseduurien ominaisuuksia muutamia vastaavalla tavalla todistettavia ominaisuuksia (kaikki listat ovat nil:iin päättyviä ja argumenttifunktiot päättyviä): (reverse (reverse l)) l (map f (reverse l)) (reverse (map f l)) (map f (append l1 l2)) (append (map f l1) (map f l2)) (map (lambda (x) (f (g x))) l) (map f (map g l)) (filter p (map f l)) (map f (filter (lambda (x) (p (f x))) l)) (fold-right op i l) (fold-left op i l), jos op on assosiatiivinen ja kaikille x: (op x i) x ja (op i x) x (fold-right op i l) (fold-left (flip op) i (reverse l)), missä (define (flip f) (lambda (x y) (f y x))) (rev-1 l) (rev-2 l) (reverse l), missä (define (rev-1 l) (if (null? l) l (append (rev-1 (cdr l)) (list (car l))))) ja (define (rev-2 l) (fold-left (flip cons) nil l))