Ohjelmoinnin peruskurssien laaja oppimäärä Luento 6: Rajoite-esimerkki, funktionaalista ohjelmointia (mm. SICP 3.3.5, 3.5) Riku Saikkonen 8. 11. 2012
Sisältö 1 SICP 3.3.5 esimerkki: rajoitteiden vyörytysjärjestelmä 2 Vähän funktionaalisten ohjelmien todistamisesta 3 Funktionaalista ohjelmointia: virrat
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) siitä että 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-luokka (viiva): 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ää aina uuden arvon eteenpäin muille liitoksen rajoitteille rajapinta constraint (laatikko): 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 ovat luokkia, jotka 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? taustalla keksintö, että kaavoja (tai muita ratkaistavia ongelmia) voi esittää tällaisina rajoiteverkkoina tätä 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ä toteutuksen 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: miten arvojen unohtaminen toimii (vain arvon asettaja voi pyytää unohtamaan sen) kolmas: verkkojen esittämiseen ei tarvitse tehdä omaa kieltä
Sisältö 1 SICP 3.3.5 esimerkki: rajoitteiden vyörytysjärjestelmä 2 Vähän funktionaalisten ohjelmien todistamisesta 3 Funktionaalista ohjelmointia: virrat
Ohjelmien oikeaksi todistamisesta eräs funktionaalisuuden etu on, että ohjelmista on helpompi todistaa ominaisuuksia, jos ne ovat puhtaasti funktionaalisia lausekkeita voi muuttaa toisiksi matemaattisesti eli välittämättä ympäröivästä kontekstista tai ohjelman tilasta suoritushetkellä imperatiivisen koodin todistamisessa mietitään yleensä ohjelman tilaa tietyillä hetkillä todistus on usein ketju ohjelman välitiloja alkutilasta (argumentit) lopputilaan, joka toteuttaa todistettavan ehdon suoritusjärjestyksellä on siis koko ajan väliä silmukoille pitää keksiä invariantti: ehto joka on voimassa esim. jokaisen kierroksen alussa ja lopussa toinen funktionaalisuuden etu on, että koodi ja todistus voi olla abstraktimpaa: voidaan tehdä enemmän yleiskäyttöisiä apufunktioita ja todistaa ensin niiden ominaisuuksia
Funktionaalinen todistusesimerkki 1/2 Todistetaan: kaikille listoille l: (accumulate cons nil l) l (lista = nil:iin päättyvä jono pareja) Ensin accumulate:n määritelmä (define (accumulate op initial sequence) (if (null? sequence) initial (op (car sequence) (accumulate op initial (cdr sequence))))) Tai sanallinen määritelmä (jolloin toteutuksen ei tarvitse olla täsmälleen ylläoleva): 1 (accumulate op i nil) i 2 (accumulate op i (cons h t ) ( op h (accumulate op i t ))
Funktionaalinen todistusesimerkki 2/2 Todistetaan: kaikille listoille l: (accumulate cons nil l) l (lista = nil:iin päättyvä jono pareja) Todistus 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 1/2 tässä muutamia vastaavalla tavalla todistettavia ominaisuuksia (kaikki listat ovat nil:iin päättyviä ja argumenttifunktiot päättyviä ja sivuvaikutuksettomia) tarkoittaa, että lausekkeiden arvot ovat samoja suoritusaika tai laskujärjestys voi olla eri reverse:stä: (reverse (reverse l)) l lähinnä map:iin liittyviä: (map f (reverse l)) (reverse (map f l)) (map f (append l1 l2)) (append (map f l1) (map f l2)) (map (compose f g) l) (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))
Listankäsittelyproseduurien ominaisuuksia 2/2 fold:eihin eli accumulate:en liittyviä: (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))) alla olevat kaksi reverse:n toteutusta toimivat eli (rev-1 l) (rev-2 l) (reverse l) Kaksi toteutusta reverse:stä (define (rev-1 l) (if (null? l) l (append (rev-1 (cdr l)) (list (car l))))) (define (rev-2 l) (fold-left (flip cons) nil l))
Sisältö 1 SICP 3.3.5 esimerkki: rajoitteiden vyörytysjärjestelmä 2 Vähän funktionaalisten ohjelmien todistamisesta 3 Funktionaalista ohjelmointia: virrat
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 cdr suoritetaan 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 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. äärettömän 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 primestream.scm (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 sqrtstream.scm (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 häntärekursiolla tässä ratkaisussa ei tarvita lopetusehtoa (haluttua tarkkuutta) kaikki lasketut arvot (eli laskennan välitilat) ovat virrassa tallessa ja virran seuraavan arvon laskemisessa voisi käyttää useampia aiemmista arvoista mutta häntärekursiivisessa tai silmukalla tehdyssä ratkaisussa käytettävissä on vain edellinen tila