Ohjelmoinnin peruskurssien laaja oppimäärä Luento 3: Funktionaalinen listankäsittely ja listankäsittelyoperaatiot (mm. SICP 22.2.3) Riku Saikkonen 31. 10. 2011
Sisältö 1 Linkitetyt listat 2 Listarakenteet 3 Listankäsittelyoperaatioita 4 Accumulate
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
Box-and-pointer-kuva (SICP 2.2.1) Lista eli (list 3) kuvana: (cons 3 nil) 3
Box-and-pointer-kuva (SICP 2.2.1) Lista eli (list (cons 2 (cons 3 nil)) 2 3) kuvana: 2 3
Box-and-pointer-kuva (SICP 2.2.1) Lista (cons 1 (cons 2 (cons 3 nil))) eli (list 1 2 3) kuvana: 1 2 3
Box-and-pointer-kuva (SICP 2.2.1) Lista (cons 1 (cons 2 (cons 3 nil))) eli (list 1 2 3) kuvana: a 1 2 3 (define a (list 1 2 3))
Box-and-pointer-kuva (SICP 2.2.1) Lista (cons 1 (cons 2 (cons 3 nil))) eli (list 1 2 3) kuvana: a b 1 2 3 (define a (list 1 2 3)) (define b (cdr a))
Schemen ja muiden kielten listat monissa muissakin kielissä on sisäänrakennetut linkitettyjen listojen vastineet (esim. Javan List-luokka) olio tms., jossa on ensimmäisen alkion arvo ja osoitin seuraavaan listan osaan joskus myös esim. linkit taaksepäin (doubly-linked list) Pythonin listat on rakennettu hieman eri tavalla taulukkoina (lisää niistä myöhemmin) 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 loppulistaa voisi käsitellä irrallaan koko listasta samanlaiset listat (ja samat listankäsittelyoperaatiot) on muissakin funktionaalisissa kielissä Schemen listoja on tapana käyttää enimmäkseen funktionaalisesti (eli ei muuteta olemassa olevia pareja vaan tehdään uusi lista)
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 yhtä hyvin '() (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)
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 Linkitetyt listat 2 Listarakenteet 3 Listankäsittelyoperaatioita 4 Accumulate
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 näitä käytetään lähinnä ei-listamaisissa tietorakenteissa toinen selitys tulostamiselle: (cons a b) tulostetaan (a. b), paitsi (cons a nil) tulostetaan (a) (cons a pari ) tulostetaan (a pari tulostettuna ) ilman näitä lyhennysmerkintöjä (cons 1 (cons 2 nil)) olisi (1. (2. ())) eikä (1 2)
Esimerkkejä listarakenteista Miten tällainen listarakenne tehdään ja tulostetaan? 1 4 2 3
Esimerkkejä listarakenteista Miten tällainen listarakenne tehdään ja tulostetaan? 1 4 2 3 (list 1 (list 2 3) 4) (tai (cons 1 (cons (cons 2 (cons 3 nil)) (cons 4 nil)))) tulostettuna (1 (2 3) 4) eli ulomman 3-alkioisen listan toisena alkiona on 2-alkioinen lista.
Esimerkkejä listarakenteista Miten tällainen listarakenne tehdään ja tulostetaan? 1 4 3 4 2 3 1 2 (list 1 (list 2 3) 4) (tai (cons 1 (cons (cons 2 (cons 3 nil)) (cons 4 nil)))) tulostettuna (1 (2 3) 4) eli ulomman 3-alkioisen listan toisena alkiona on 2-alkioinen lista.
Esimerkkejä listarakenteista Miten tällainen listarakenne tehdään ja tulostetaan? 1 4 3 4 2 3 (list 1 (list 2 3) 4) (tai (cons 1 (cons (cons 2 (cons 3 nil)) (cons 4 nil)))) tulostettuna (1 (2 3) 4) eli ulomman 3-alkioisen listan toisena alkiona on 2-alkioinen lista. 1 2 (cons (cons 1 2) (cons 3 4)) tulostettuna ((1. 2) 3. 4) (Tuloste on tässä sekava: ulompana on 2-alkioinen lista, joka päättyy nil:n sijaan 4:ään.)
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)
Sisältö 1 Linkitetyt listat 2 Listarakenteet 3 Listankäsittelyoperaatioita 4 Accumulate
Tavallisimpia Schemen listaoperaatioita (SICP 2.2.1, 2.2.3) (length l) alkioiden määrä (list-ref l i) tietty alkio (i = 0 ensimmäinen) (map f l) tekee (a b c):sta uuden listan (f (a) f (b) f (c)) (filter p l) palauttaa uuden listan, jossa on l:stä vain ne alkiot x, joille (p x) palauttaa toden (eli muun kuin #f:n) (append l1 l2) kerää listojen alkiot peräkkäin yhteen listaan kopioi l1-listan niin että kopion viimeisen parin cdr on l2 eikä nil argumentteja voi olla enemmänkin kuin 2 Esimerkkejä näistä (length (list 1 2 3)) 3 (filter odd? (list 1 2 3 4 5)) (1 3 5) (map length (list (list 1 2) (list 3) (list 4 5))) (2 1 2) (append (list 1 2) (list 3 4)) (1 2 3 4) (cons (list 1 2) (list 3 4)) ((1 2) 3 4) (define (square-list l) (map square l)) ; sama oli edellä ilman map:iä
Lisää listaoperaatioita (SICP 2.2.1, 2.2.3) (pair? l) onko l pari? (vakioaikainen kuten null?) (list? l) onko l tyhjään listaan päättyvä lista? (käy l:n läpi) (apply f l) kutsuu f:ää kerran argumentteina listan alkiot (for-each f l) kutsuu f:ää yksi kerrallaan kaikille listan alkioille (reverse l) lista toisinpäin (list-tail l k) ensimmäiset k alkiota pois (siis k kpl cdr:iä) lyhennysmerkintä: (caddr l) (car (cdr (cdr l))) yms. Esimerkkejä (reverse (list 1 2 3 4)) (4 3 2 1) (list-tail (list 1 2 3 4) 2) (3 4) (apply + (list 1 2 3 4)) 10 eli (+ 1 2 3 4) (cadr (list 1 2 3 4)) 2
Laajennettu map (SICP 2.2.1) (map f l) siis tekee (a b c):sta (f (a) f (b) f (c)):n Schemen map on todellisuudessa monikäyttöisempi: (map f l1 l2) tekee (a b c):sta ja (d e f ):stä (f (a, d) f (b, e) f (c, f )):n listoja voi olla niin monta kuin haluaa (f :lle annetaan jokaisesta listasta yksi argumentti), mutta niiden pitää olla saman pituiset esimerkki: (map (lambda (x y) (+ x (* 2 y))) (list 1 2 3) (list 4 5 6)) (9 12 15)
Listaoperaatiot olisi helppo määritellä itse (SICP 2.2.1, 2.2.3) Esimerkkeinä list-ref ja filter (define (list-ref items n) (if (= n 0) (car items) (list-ref (cdr items) (- n 1)))) (define (filter predicate sequence) (cond ((null? sequence) nil) ((predicate (car sequence)) (cons (car sequence) (filter predicate (cdr sequence)))) (else (filter predicate (cdr sequence))))) filter ei kuulu standardi-schemeen, joten sen joutuu määrittelemään itse (määritelmä löytyy mm. SICPistä)
Miksi nämä ovat tärkeitä? samoja tai melkein samoja listankäsittelyoperaatioita käytetään muissakin kielissä kuin Schemessä varsinkin funktionaalisissa kielissä erityisesti mapia, filteriä ja seuraavana esiteltävää accumulatea melko monimutkaisiakin asioita voi laskea pelkästään näitä listankäsittelyoperaatioita yhdistelemällä (sen sijaan että kävisi listoja läpi käsin omassa koodissa) myöhemmillä luennoilla lisää esimerkkejä ja: miten näiden avulla voi tehdä rinnakkaistuvaa koodia vähän siitä, miten näitä käyttävää koodia voi todistaa oikeaksi list comprehension -niminen lyhennysmerkintä map- ja filter-ketjuille (ei Schemessä)
Sisältö 1 Linkitetyt listat 2 Listarakenteet 3 Listankäsittelyoperaatioita 4 Accumulate
Apuproseduuri accumulate eli fold-right (SICP 2.2.3) hyvin yleiskäyttöinen listaoperaatio nimeltä accumulate yhdistää listan alkiot halutulla operaatiolla (accumulate op init (list 1 2 3 4)) ( op 1 ( op 2 ( op 3 ( op 4 init )))) ( op x y) yhdistää x:n aiemman op :n tulokseen y (accumulate + 0 (list 1 2 3 4)) (+ 1 (+ 2 (+ 3 (+ 4 0)))) 10 (accumulate cons nil (list 1 2 3 4)) (1 2 3 4) (kopioi listan) accumulate:n määritelmä (ei tule mukana Schemessä) (define (accumulate op initial sequence) (if (null? sequence) initial (op (car sequence) (accumulate op initial (cdr sequence)))))
accumulate:n käyttöesimerkkejä (SICP 2.2.3) Parittomien lukujen neliöiden tulo (define (pr-sq-odd s) (accumulate * 1 (map square (filter odd? s)))) Ajoesimerkki: (pr-sq-odd (list 1 2 3 4 5)) 225 Korkein ohjelmoijan palkka (vain osittainen koodi) (define (salary-of-highest-paid-programmer records) (accumulate max 0 (map salary (filter programmer? records)))) Listana esitettyjen vektorien pistetulo (define (dot-product v w) (accumulate + 0 (map * v w))) Ajoesimerkki: (dot-product (list 1 2 3) (list 4 5 6)) 32
Vielä yksi apuproseduuri: flatmap (SICP 2.2.3) flatmap on kuin map, mutta litistää palautetusta listasta yhden tason pois (map f (list 1 2)) (list (f 1) (f 2)) (flatmap f (list 1 2)) (append (f 1) (f 2)) (map list (list 1 2)) ((1) (2)) (flatmap list (list 1 2)) (1 2) flatmap:lle annetun funktion pitää aina palauttaa lista Määritelmä ja käyttöesimerkki (define (flatmap proc seq) (accumulate append nil (map proc seq))) (define (around x) (list (- x 1) x (+ x 1))) (map around (list 2 15 22)) ((1 2 3) (14 15 16) (21 22 23)) (flatmap around (list 2 15 22)) (1 2 3 14 15 16 21 22 23)
accumulate:n variaatiot accumulate:sta eli fold-right:sta on peilikuva fold-left: (fold-right + 0 (list 1 2 3 4)) (+ 1 (+ 2 (+ 3 (+ 4 0)))) 10 (fold-left + 0 (list 1 2 3 4)) (+ (+ (+ (+ 0 1) 2) 3) 4) 10 ks. kirjan tehtävä 2.38 molemmista on vielä versiot, joissa ei ole init -alkiota, vaan sellaisena käytetään listan ensimmäistä tai viimeistä alkiota (reduce-right + (list 1 2 3 4)) (+ 1 (+ 2 (+ 3 4))) 10 (reduce-left + (list 1 2 3 4)) (+ (+ (+ 1 2) 3) 4) 10 funktionaalisessa ohjelmoinnissa tietoa käsitellään usein usein näiden perusprimitiivien avulla varsinkin map, filter ja accumulate:n eri versiot erityisesti Haskell-, ML- ja Ocaml-kielissä
Miksei accumulate:n sijaan käytetä apply:ä? siis (accumulate + 0 (list 1 2 3 4)) 10 entä (apply + (list 1 2 3 4)) (+ 1 2 3 4) 10? apply ei toimi tasan kaksi argumenttia haluavilla operaatioilla (esim. cons tai jokin itse määritelty 2-argumenttinen funktio) monissa Scheme-toteutuksissa on maksimimäärä proseduurikutsun argumenteille (Gambit-C:ssä 8192), joten apply ei toimisi pitkille listoille eräs tapa ajatella: accumulate on apuproseduuri, joka tekee 2-argumenttisesta proseduurista kokonaista listaa käsittelevän proseduurin apply taas on vain tapa antaa lista sellaiselle proseduurille, joka jo ennestään ymmärtää yli kahta argumenttia
Funktionaalista listankäsittelyä Pythonissa Esimerkki: parittomien lukujen neliöiden summa (accumulate + 0 (map (lambda (x) (* x x)) (filter odd? (enumerate-interval 1 100)))) 166650 ;; Sama Pythonilla: reduce(lambda x,y: x+y, map(lambda x: x*x, filter(lambda x: x%2==1, range(1, 100)))) 166650
Funktionaalista listankäsittelyä Pythonissa Esimerkki: parittomien lukujen neliöiden summa (accumulate + 0 (map (lambda (x) (* x x)) (filter odd? (enumerate-interval 1 100)))) 166650 ;; Sama Pythonilla: reduce(lambda x,y: x+y, map(lambda x: x*x, filter(lambda x: x%2==1, range(1, 100)))) 166650 sum([ x*x for x in range(1, 100) if x%2==1 ]) 166650 alin rivi tekee saman Pythonin list comprehension -syntaksilla lyhennysmerkintä useimmille map- ja filter-ketjuille (Pythonin lisäksi esim. Haskell- ja Scala-kielissä, ei Schemessä) tästä lisää myöhemmin... lyhennysmerkintä ei auta reduce:en; yllä käytetty sum on vain Pythonin valmis funktio joka tekee saman kuin juuri tämä reduce