Ohjelmoinnin peruskurssien laaja oppimäärä Luento 7: Funktionaalista ohjelmointia (mm. SICP 3.5) Riku Saikkonen 13. 11. 2012
Sisältö 1 Laiskaa laskentaa: delay ja force 2 Funktionaalinen I/O 3 Funktionaalista ohjelmointia: hahmonsovitussyntaksi 4 Funktionaalista ohjelmointia: list comprehension -rakenne
Schemen delay ja force laiska laskenta (lazy evaluation tai non-strict): lauseke lasketaan vasta sitten kun (ja jos) sen arvoa tarvitaan tavallisessa Schemessä laiskaa laskentaa voi tehdä delay- ja force-primitiiveillä (delay lauseke ) palauttaa erikoisarvon, joka kuvaa vielä laskematta olevaa lausekketta (force jonkin delayn paluuarvo ) pakottaa lausekkeen laskemisen ja palauttaa sen arvon lauseke evaluoidaan vain kerran: jos forcelle antaa jo kerran lasketun lausekkeen, se palauttaa saman arvon uudelleen virrat on toteutettu delayn ja forcen avulla: (cons-stream a b ) (cons a (delay b )) samantapainen rakenne on muutamissa muissakin kielissä (mm. Scala ja jotkut ML-toteutukset), ja moniin se on helppo toteuttaa itse (esim. Ruby ja Common Lisp) mm. Haskell-kielessä lausekkeet lasketaan aina laiskasti
delayn ja forcen toteuttaminen Schemessä (SICP 3.5.1) jos Schemessä ei olisi delaytä ja forcea, ne voisi toteuttaa muun Schemen avulla: (delay a ):n sijaan sanotaan (memo-proc (lambda () a )) tällöin forcen voi toteuttaa (define (force a) (a)) delaytä ei voi tehdä tähän mennessä opitulla Schemellä, mutta sen voisi tehdä makrona alla olevalla koodilla delayn ja forcen toteuttaminen (define (memo-proc proc) (let ((already-run? false) (result false)) (lambda () (if (not already-run?) (begin (set! result (proc)) (set! already-run? true) result) result)))) (define-macro (delay a) `(memo-proc (lambda (),a))) ; Lisp-makro (define (force a) (a))
Sisältö 1 Laiskaa laskentaa: delay ja force 2 Funktionaalinen I/O 3 Funktionaalista ohjelmointia: hahmonsovitussyntaksi 4 Funktionaalista ohjelmointia: list comprehension -rakenne
I/O:n ongelma (osin SICP 3.5.5) puhtaasti funktionaalisessa ohjelmoinnissa millään operaatiolla ei saisi olla sivuvaikutuksia laskennan tilan ylläpitoa voi tehdä mm. rekursiolla ja virroilla entä interaktio käyttäjän (tai esim. verkon) kanssa? toinen vastaava ongelma on tilallinen funktio, jonka sisäistä tilaa ei haluaisi käsitellä koko ajan: esim. satunnaislukugeneraattori funktionaaliseen I/O:hon ei ole täydellistä ratkaisua (vielä?) perusratkaisu on, että pääohjelma on funktio syötevirralta tulosvirralle (esimerkki kohta) Haskell-kielessä tämän idean päälle on rakennettu koodia siistivä mutta käsitteellisesti monimutkainen abstraktio nimeltä monadi toinen ratkaisu (esim. Clean-kielessä) on tyyppijärjestelmän laajennus niin, että tiettyjä arvoja voi käyttää vain kerran (uniqueness type) tätä ongelmaa tutkitaan vielä... käytännön ratkaisu on usein eristää I/O:ta tekevä koodi muusta enemmän funktionaalisesta koodista
I/O virroilla: toteutus Virta-I/O:n toteuttaminen Schemessä (define (run-io io-proc) (define (get-input-stream) (cons-stream (delay (read)) (get-input-stream))) (define (display-output-stream stream) (if (stream-null? stream) 'end (begin (display (stream-car stream)) (newline) (display-output-stream (stream-cdr stream))))) (display-output-stream (io-proc (get-input-stream)))) interaktiivinen pääohjelma on (run-io io-proc), jossa io-proc on oma puhtaasti funktionaalinen proseduuri, joka ottaa argumentiksi virran käyttäjältä tulevia syötteitä (read:lla luettuja Scheme-arvoja) palauttaa virran tulosteita (tulostettavaksi display:llä) run-io tulostaa tulosvirtaa aina kun sitä on saatavilla, ja tuottaa lisää syötevirtaa io-proc:n sitä pyytäessä
I/O virroilla: käyttöesimerkki Edellisen toteutuksen käyttäminen (define (io-sqrt input-stream) ; palauttaa tulostevirran (cons-stream "Enter a number or q to quit:" (let ((input (force (stream-car input-stream)))) (if (eq? input 'q) the-empty-stream (cons-stream "The square root is:" (cons-stream (sqrt input) (io-sqrt (stream-cdr input-stream)))))))) iosqrt.scm (run-io io-sqrt) ; ''pääohjelman'' käynnistys tulostaa siis tulosvirtaan sekä kehotteita että laskemisen tuloksia tässä esimerkissä virta, jonka car olisi myös viivästetty, toimisi paremmin kuin SICP-kirjan virta: siksi force ja edellä delay
I/O virroilla: tilallinen käyttöesimerkki (osin SICP 3.3.5) Pankkitili, jolta voi nostaa ja tallettaa rahaa iobank.scm (define (io-bank balance input-stream) ; tila on balance:ssa (cons-stream (string-append "Current balance: " (number->string balance) " euros") (let ((input (force (stream-car input-stream)))) (if (eq? input 'q) the-empty-stream (let ((new-balance (+ balance input))) (if (< new-balance 0) (cons-stream "Error: not enough money!" (io-bank balance (stream-cdr input-stream))) (cons-stream (string-append (if (< input 0) "Ok, withdrawed " "Ok, deposited ") (number->string input) " euros.") (io-bank new-balance (stream-cdr input-stream))))))))) (define (io-bank-run input-stream) (io-bank 0 input-stream)) (run-io io-bank-run) ; apuproseduuri käynnistykseen
Sisältö 1 Laiskaa laskentaa: delay ja force 2 Funktionaalinen I/O 3 Funktionaalista ohjelmointia: hahmonsovitussyntaksi 4 Funktionaalista ohjelmointia: list comprehension -rakenne
Hahmonsovitus (pattern matching) funktionaalisissa kielissä suosittu erikoissyntaksi (ei Schemessä) funktion määrittelyssä voi olla argumentin sijasta hahmo, johon argumentin tulee sopia ja joka voi asettaa muuttujille arvoja samalle funktiolle voi olla useampia hahmoja, jotka käydään läpi järjestyksessä esimerkiksi (nämä esimerkit ovat Haskell-kieltä): fact 0 = 1 fact n = n * fact (n-1) hahmonsovitus muuttuu periaatteessa if-lauseiksi, jotka kokeilevat hahmoja (argumentit vasemmalta oikealle, hahmot ylhäältä alas): fact n = if n == 0 then 1 else n * fact (n-1) hahmonsovitus on yleensä mahdollista myös funktion sisällä (esim. match) sekä let- ja lambda-lauseissa vastaava idea säännöllisille lausekkeille löytyy esim. Perl-kielestä tässä hahmoon sovitetaan merkkijonoja eikä tietotyyppejä Perl-esimerkki: ($hour, $min, $sec) = ($time = /([0-9][0-9]):([0-9][0-9]):([0-9][0-9])/);
Hahmonsovitus: rakenteet hahmossa voi olla myös listarakenne: sumlist [] = 0 sumlist (x:xs) = x + sumlist xs tässä [] = tyhjä lista ja (x:xs) = pari, jonka car ja cdr talletetaan muuttujiin x ja xs tai itsemääritellyistä tyypeistä koostuva rakenne: data Tree a = Leaf a Branch (Tree a) (Tree a) fringe (Leaf x) = [x] fringe (Branch left right) = fringe left ++ fringe right erikoisuuksia: _ sopii mihin tahansa arvoon, ei tee muuttujaa; syntaksissa xl@(x:xs) koko lista on xl car (x:_) = x cdr (_:x) = x f xl@(x:xs) = xl ++ xs ++ [x] Testiajo: f [1,2,3] [1,2,3,2,3,1]
Vahdit (guard) hahmon lisäksi sovitukseen voi määritellä ehtoja, joiden on oltava tosia, jotta arvo sopisi hahmoon: sign x x > 0 = 1 x == 0 = 0 x < 0 = -1 viimeinen haara voisi olla myös otherwise = -1 vahdit tarkistetaan varsinaisen sovituksen jälkeen, joten sovituksen määrittelemiä muuttujia voi käyttää (kuten x:ää yllä) Monimutkaisempi Haskell-esimerkki mergenodups [] ys = ys mergenodups xs [] = xs mergenodups xl@(x:xs) yl@(y:ys) x < y = x : mergenodups xs yl x == y = mergenodups xs yl otherwise = y : mergenodups xl ys
Hahmonsovitus käytännössä hahmonsovitus on Haskellin lisäksi käytössä esim. ML:ssä, Ocamlissa, Erlangissa, Prologissa ja (osin) Scalassa säännöllisiin lausekkeisiin perustuva hahmonsovitus monessa muussakin kielessä ehkä hahmonsovitus leviää vähitellen uusiin kieliin? hahmonsovituksen etu: koodi on lyhyempää ja lähempänä matemaattista määritelmää (siis helppolukuisempaa, ainakin matemaatikolle... ) hahmonsovituksen ongelmia: ei toimi hyvin abstraktien tietotyyppien kanssa: hahmoa kirjoittaessa pitää tietää mitä kaikkia osia tietotyypissä on ja missä järjestyksessä syntaksi toimii siististi vain lyhyillä hahmoilla ja vahdeilla (hahmonsovitus on uusi erikseen opeteltava ominaisuus)
Sisältö 1 Laiskaa laskentaa: delay ja force 2 Funktionaalinen I/O 3 Funktionaalista ohjelmointia: hahmonsovitussyntaksi 4 Funktionaalista ohjelmointia: list comprehension -rakenne
Syntaksi lukusarjojen tuottamiseen alla on lyhyt syntaksi yksinkertaisten äärellisten tai äärettömien lukusarjojen tekemiseen toteutetaan muuttamalla syntaksi funktiokutsuksi: esim. SICP-kirjan (enumerate-interval 3 6) tämän kaltainen syntaksi on käytössä joissain ohjelmointikielissä ja matematiikkaohjelmistoissa, esim. Haskell, Matlab, Octave Pythonissa on vain range- ja xrange-funktiot joskus syntaksia voi käyttää numeroiden lisäksi esim. enumeraatioiden kanssa Haskell-esimerkkejä [1..] [1,2,3,4,...] [3..6] [3,4,5,6] [5,10..30] [5,10,15,20,25,30]
List comprehension -syntaksi kätevä tapa koota listoja tietyt ehdot täyttävistä alkioista tai käydä joukko vaihtoehtoja läpi antaa lyhyemmän syntaksin map:n ja filter:n yhdistelmille syntaksi on lainattu matematiikasta: esim. joukko {x y x {1, 2,..., 10}, y {1, 2,..., 10}, x < y} list comprehension -syntaksi muutamassa kielessä: Haskell: [x*y x <- [1..10], y <- [1..10], x<y] Python: [x*y for x in range(1,11) for y in range(1,11) if x<y] Scala: for (x <- List.range(1,10); y <- List.range(1,10) if x < y) yield x*y Pythonissa on lisäksi iterator comprehension (muuten sama mutta tekee iteraattoreita eikä listoja): (x*y for x in range(1,11) for y in range(1,11) if x<y) ehkä nämä tulevat vähitellen muihinkin kieliin, joissa on luontevaa tuottaa listoja tai iteraattoreita?
List comprehensionin toteuttamisesta list comprehension luo kaikki annettujen arvojen yhdistelmät, vasemmalta oikealle, ja poistaa ne, jotka eivät täytä ehtoja Esimerkki list comprehensionin aukikirjoittamisesta [x*y for x in range(1,11) for y in range(1,11) if x<y] ;; Pythonista Schemeksi muunnettuna: (flatmap (lambda (x) (map (lambda (y) (* x y)) (filter (lambda (y) (< x y)) (enumerate-interval 1 10)))) (enumerate-interval 1 10)) Muutama lisäesimerkki Haskell-kielellä [ x x <- [1..9], even x ] [2,4,6,8] [ (x,y) x <- [1..4], y <- [1..4], x < y ] [(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)] factors n = [ i i <- [1..n-1], n `mod` i == 0 ] ;; Viimeinen Schemellä: (define (factors n) (filter (lambda (i) (= (remainder n i) 0)) (enumerate-interval 1 (- n 1))))