Föreläsning 4 (och 5?) i programmeringsparadigm. Högre ordningens funktioner på listor; funktioner som argument: map, filter. (Fokker 3.1.3, C&K p 37, 38) map -- förändra alla element i en lista på samma sätt. (i st f for-loopar). filter -- filtera (välj ut) en del element ur en lista. (i st f for-loopar). Exempel, vår kära power: power :: Int -> Int -> Int -- Om man kan map,[.. ] (i Prelude) power m n = product (map f [1..n]) --, n>0 ger 1 where f _ = m Listomfattning. (List comprehensions, Fokker 3.2.7) I stället för att använda map och filter kan man med listomfattning (List comprehension) skriva väldigt eleganta program, lätta att skriva och förstå. Några delar av Schack-labben blir ännu enklare att skriva med listomfattning än med map och filter. Vi ska ju ofta göra något med en listor, t ex listor med grafik. Exempel, vår kära power: power :: Int -> Int -> Int -- Om man kan listomfattning power m n = product [m x <- [1..n]] Exempel från Fokker Uttrycket [x*x x <- [1..10]] motsvarar let square x = x*x in map square [1..10]] eller med λ-notation (se nedan) map (\x -> x*x) [1..10]] Syntaxen påminner om matematikens syntax för mängder : {x*x x e N, 1 <= x, x <= 10} Även i listomfattningar kan man skriva villkor: map (\x -> x*x) [1..100], x <= 10] (Fånigt exempel) λ-notation (lamda-notation, anonyma funktioner). Fokker 2.3.4. Vi kan om vi vill definiera hjälpkonstanter, t ex i detta uttryck : kommunalskatt inkomst = skattesats * inkomst where skattesats = 0.30 -- skattesats definieras som 0.30. Ev globalt "skåp". Men om vi är för lata för att "döpa" 0.30 till skattesats så kan vi förstås helt enkelt skriva kommunalskatt inkomst = 0.30 * inkomst
Dvs i en definition namngivet begrepp kan ersättas med sin definition. Låt oss nu titta på mapf lista = map f lista where f i = 3*i + b b = 5 -- f definieras som en funktion med högerledet 3*i + b där i är parameter I en vanlig mattebok skulle en definition av f se ut så här f(i) = 3*i + b, vilket ju påminner om vår definition f i = 3*i + b. Men om vill kunna ersätta f i uttrycket mapf lista = map f lista kan vi ju inte bara skriva mapf lista = map (3*i + b) lista --- Fel!!!!!!!! eftersom (3*i + b) är ett uttyck men f är en funktion. I en sorts matematik som kallas λ-kalkyl skriver man definitonen av f som f = λi. 3*i + b Motsvarande sätt att definiera funktionen f går också att använda i funktionella språk: f = \i -> 3*i + b --- <namn> = <värde> som för konstanter (Teoretiskt sett "det riktiga sättet", det sätt vi använt hittills är en eftergift till konventionell matematik) mapf lista = map f lista where f = \i -> 3*i + b b = 5 -- f definieras som en funktion med högerledet 3*i + b där i är parameter Men om vi är för lata för att "döpa" vår funktion till f så kan vi förstås helt enkelt skriva mapf lista = map (\i -> 3*i + b) lista Dvs ett i en definition namngivet begrepp kan ersättas med sin definition. Exempel, vår kära power: power :: Int->Int->Int -- accumulerande lösningar med until, lamda power m n -- Fokker 2.3.2, Fokker 2.3.4 n<0 = error " second argument till power >= 0" otherwise = snd (until (\(i, _) -> ( i == n )) (\(i, acc) -> (i+1, acc*m) ) (0, 1))
För den som är intresserad av teori: Att i en definition namngivet begrepp alltid kan ersättas med sin definition är en mycket viktig egenskap hos matematik och funktionella språk. I imperativa språk gäller inte detta, trots att det borde vara en självklarhet, ett exempel i Java: static int x = 4; int y = f(x) * f(x) ; Efter att kört dessa två satser är inte y säkert samma värde som när man kört detta program static int x = 4; int z = f(x) int y = z * z; Låt t ex f ha denna definition static int f(int u) { int result = 2*u; x = 5; //sidoeffekt return result; } Sidoeffekter ställer lätt till med en massa elände och gör att man i imperativ och objektorienterad programmering måste tänka sig in i vad som händer vid körningen. Sammanhanget (contexten), vad som hänt under körningen och inte bara vad som står i programkoden, spelar ofta roll när man skall tolka något i ett imperativt språk. Ordningen mellan satser spelar (en mycket stor) roll i imperativa språk. Att skriva bra imperativa program innebär att använda minimalt med sidoeffekter (eller i OO använda sidoeffekter på ett kontrollerat sätt) och se till att så få begrepp som möjligt samtidigt är relevanta i något sammanhang. Betydelsen av en funktionsdeklaration i ett funktionellt språk beror inte på vad som händer vid en körnming. Funktionsdeklarationer kan stå i godtycklig ordning. Funktionella språk har inga sidoeffekter, programmeringen innebär att definiera begrepp, dvs lämpliga funktioner och datatyper (och typklasser). Bl a Haskell Curry arbetade med λ-kalkyl på 1930-talet, en gren av matematiken där λ-notationen är ett viktigt begrepp, dvs λ-kalkyl är äldre än datortekniken. Kärnan i funktionell programmering är helt enkelt λ-kalkyl. Haskell Curry har hedrats genom att hans förnamn har namngivit programspråket Haskell och hans efternamn givet namne på en viktig teknik för att skriva funktioner som behandlas i om en stund. Användbara funktioner; until. Fokker 2.3.2 En mängd användbara funktioner finns som redan betonats i Preluden, där de också är definierade, de allra flesta med några få rader Haskell. Att använda dessa funktioner är bekvämt, man slipper göra
ungefär samma sak gång på gång, programmering blir mindre tjatigt och programmeringen går ännu fortare, programmen blir kortare och programmering blir på "högre nivå", bekväm och lustfylld. Man kan naturligtvis själv också göra högre ordningens funktioner, och gör man dem bra (bl a så polymorfa som möjligt) så kan dom vara användbara gång på gång i en mängd olika sammanhang. Men många av dom högre ordningens funktioner som finns i Preluden är väldigt användbara. Ett exempel : until -- förvandla något gång på gång så länge något gäller (~loopar). I Prelude : until :: (a -> Bool) -> (a -> a) -> a -> a until p f x = if p x then x else until p f (f x) snyggare med vakter (tycker jag och Fokker) until p f x p x = x otherwise = until p f (f x) Obs. svansrekursiv. "loopar" (slinger, snurror) i imperativa språk kan programmeras med until, och många av våra lösningar med "ackumulering i parametrar" kan också lösas med until. De "imperativa programmeringsvariablerna" motsvaras av komponenterna i en tuppel, "<status>": Java : Haskell : while <not p> { until p f <status> <f > where p :: <statustyp> -> Bool } p <status> =.. - -funktion som blir True när vi skall sluta f :: <statustyp> -> <statustyp> f <status> =..- -funktion som beskriver "loopens kropp" Exempel, vår kära power: power :: Int -> Int -> Int -- accumulerande lösningar med until, power m n n<0 = error " second argument till power >= 0" otherwise = snd (until p f (0, 1)) where p (i, _) = ( i == n )) f(i, acc) = (i+1, acc*m) I Java : static int power (int m, int n) { //snurra, lokala variabler if (n < 0) { throw new RuntimeException("second argument till power >= 0"); } else { int acc = 1; for (int i = 0; i<n ; i = i+ 1) { acc = acc*m; } return acc; } } Alla sätt att resonera kan fångas med funktioner!
Högre ordningens funktioner, higer-order functions (HOF). Funktioner resultat: Currying. Fokker 2.2.1, 2.2.2, 2.2.3 Låt oss definiera add :: (Int, Int)->Int add (x,y) = x + y -- eller add = \ (x,y) -> x+ y add (3,5) blir 8 dvs vi säger att add har en parameter som är ett heltalspar och lämnar ett heltal som resultat. Detta påminner ju om hur vi skriver i matte brukar skriva add(x,y) = x + y, men det vanliga men egentligen felaktiga språkbruket är att add "är en funktion av två variabler", men Funktioner har alltid bara en enda parameter. När funktioner används har de alltid bara ett enda argument. Låt oss definiera plus :: Int -> (Int->Int) (plus x) y = x + y -- eller plus = \ x -> (\y -> x + y) (plus 3) 5 blir 8 -- ganska löjligt, enklare((+) 3) 5, 3+5 eftersom plus = (+) dvs det korrekta sättet att beskriva plus är att plus har en parameter x som är ett par heltal och lämnar som resultat en anonym funktion (\y -> x + y). Detta syns tydligare om vi läser definitionen av plus med λ-notation. När vi t ex gör (plus 3) får vi som resultat funktionen (\y -> 3 + y). Denna funktion kan nu appliceras på t ex 5, dvs (\y -> 3 + y) 5 vilket blir 8. När funktioner används har de alltid bara ett enda argument. Är resultatet en funktion kan resultatet användas på ytterligare ett argument osv. Funktionsapplikation binder till höger, dvs f g x betyder (f g) x. -> i typutryck binder till höger, dvs Int -> Int ->Int betyder Int -> (Int ->Int). Alltså kan vi skriva på detta sätt plus :: Int -> Int->Int plus x y = x + y -- eller plus = \ x -> (\y -> x + y) plus 3 5 blir 8 vilket vi gjort hela tiden, men inte förklarat egentligen varför vi kan skriva på detta sätt. plus kalls en "a curried funtion", på svenska en partiellt applicerbar funktion. I detta exempel har vi visat att en funktion med ett par som parameter (add) har en motsvarande "curried" funktion (plus), och på liknade sätt kan man resonera för triplar och tupler i allmänhet.
Det är ingen nackdel att använda sådana funktioner i stället för tupler. Fördelen är att man kan ha användning av "mellanfunktionerna". När man använder t ex plus med endast ett argument gör man en partiell applikation. (Fokker kallar det partiell parametrisering). Även operatorer kan partiellt appliceras, vilket kallas operator snitt (operator sections). T ex är följande uttryck olika sätt att skriva samma sak, värdet blir alltid [2, 4, 6] : let f x = 2*x in map f [1, 2, 3] let f = \x -> 2*x in map f [1, 2, 3] map f (\x -> 2*x) [1, 2, 3] map (2*) [1, 2, 3] map (*2) [1, 2, 3] Ett exempel från Schack-labben, steg 3b Polygongraphics ("kanske kusens svåraste uppgift"): Vi har en definitionen vektoradditon från Vector med typen : vadd :: Vector -> Vector -> Vector Antag att vi har en lista med polygonkoordinater poly::[vector] (dvs en pjäs utseende) som skall flyttas till något ställe at::vector (dvs en schackruta). Vi skulle kunna ha användning för något som.. map movedtosq poly... where movedtosq = vadd at -- enklare -- movedtosq coord = vadd at coord -- sämre men det är bekvämt, enklast och bäst att slippa definiera movedtosq.. map (vadd at) poly... vilket är möjligt tack vare att vadd är partiellt applicerbar. Ytterligare exempel från steg 3 i schack- labben: På analogt sätt kan man ha glädje av att multiplikation med skalär mscal :: Float -> Vector -> Vector är partiellt applicerbar när vi skall skala polygoner. Funktionssammansättning. Fokker 2.3.3 Uttrycket f ( g x ) kan skrivas som (f. f) x eller let h = f. g in h x. Exempel: map f (map g (map h lista)) skrivs lämpligen map (f.g.h) lista. (man slipper gå igenom listan gång på gång)
Ett exempel från Schack-labben, steg 3b Polygongraphics ("kanske kusens svåraste uppgift"): Antag att vi har en lista med koordinater (en polygon till exempel) flyttas och skalas och räknas om till pixlar. Skrivs lämpligen som map ( tillpixlarfunktion. flyttfunktion. skalfunktion) lista. Om flyttfunktionen och skalfunktionen talade vi ju om i föregående avsnitt. Märk också likheten mellan Haskells. och i Unix för att göra "pipes". Vad vi nu har gått igenom i Fokker. Nästan allt i kapitel 1, 2, och 3 hoppas jag nu är förståeligt. Det som vi inte har behandlat än är lat evaluering och oändliga listor 3.2.4, 3.25, 3.26. Man behöver inte förstå detta för att klara schacklabben. Lat evaluering och oändliga listor möjliggör dock att det är möjligt att programmera ännu elegantare och effektivare i Haskell, så jag hoppas kunna prata om detta på någon av de sista Haskell-förläsningarna. Vad vi nu har gått igenom i C&K. Nästan allt i kapitel 1, 3, 4, 5, 8, 10. Vi har kvar kapitel 7. Övriga kapitel ej i denna kurs. Hur man ritar bilder m m. Inför Steg 3 och 4 i schacklabben Se Labbhäftet sid 28 <= sid <= 34
Gamla tentatal: 5 a) Definiera en datatyp för binära träd med information i "löven". b) Antag att löven är funktioner. Skriv en funktion app som kan applicera lövens funktioner på ett givet argument. Ange typen för app. c) Skriv en funktion sumtree som summerar alla värden i ett lövträd med heltal i löven. Ledning : Ett delvis skämtsamt exempel på användning av deklarationerna i a), b( och c) (träden ritade på botaniskt i stället för datalogiskt sätt, rötterna nedåt) : \m -> 2* m*m + 3 \m -> 7*m + 15 \m -> 10*m + 7 Vår. Ett äppelträd mytree. Olika äppelsorter inympade. Funktionerna anger hur tunga (i g) tre äpplen blir som funktion av tiden (i månader). 90 57 67 Höst. 6 månader senare. Vikt för äpplenen på träden. app 6 mytree 214 Skördens vikt. sumtree (app 6 mytree)
module Appletree where -- på /info/progp02/haskelllectures/f5/appletree.hs data LeafTree a = Leaf a Node (LeafTree a) (LeafTree a) -- a) deriving Show app :: a -> LeafTree (a->b) -> LeafTree b -- b) app x (Leaf f) = Leaf (f x) app x (Node left right) = Node (app x left) (app x right) sumtree :: LeafTree Int -> Int -- c) sumtree (Leaf i) = i sumtree (Node left right) = sumtree left + sumtree right mytree :: LeafTree (Int -> Int) mytree = Node (Leaf (\m -> 10*m+7)) (Node (Leaf (\m -> 2*m*m+3*m)) (Leaf (\m -> 7*m+15))) {- -} Appletree> app 6 mytree Node (Leaf 67) (Node (Leaf 90) (Leaf 57)) Appletree> sumtree (app 6 mytree) 214 4 Skriv en funktion occurestimes n som lämnar som resultat en lista som innehåller de element som förekommer minst n gånger i list med anropet occurestimes n list. Exempel: occurestimes 2 [, "a", "hej", "Luleå", "a", "b", "hej"] ger svaret ["hej, "a"] occ [] = [] --Tal 4 på tentan occ (x:xs) = add x (occ xs) -- på /info/progp02/haskelllectures/f5/appletree.hs add x [] = [(x, 1)] add x ((y,n):ys) x == y = (x, n+1): ys otherwise = (y, n) : add x ys occurstimes m list = [x (x, n) <- occ list, m == n] {- occ :: Eq a => [a] -> [(a, Int)] occ = foldr add []...> occurstimes 2 ["a", "hej", "Lul", "a", "b", "hej"] ["hej","a"] -}
Quicksort i Haskell: module Quicksort where -- på /info/progp02/haskelllectures/f5/quicksort.hs qsort :: Ord a => [a] -> [a] qsort [] = [] qsort (x:xs) = qsort (filter (<=x) xs) ++ [x] ++ qsort (filter (x<) xs) {- Körning : :s +ts Quicksort> qsort [1,23, 14, 5, 8, 2, 14] [1,2,5,8,14,14,23] (469 reductions, 642 cells) -} {- qsort :: Ord a => [a] -> [a] qsort [] = [] qsort (x:xs) = qsort small ++ [x] ++ qsort large where (small, large) = foldl dir ([], []) xs dir -- (332 reductions, 594 cells) -} (sm, la) y y <= x = (y:sm, la) otherwise = (sm, y:la) Ett försök att motbevisa Fermats stora sats: --på /info/progp02/haskelllectures/f5/quicksort.hs triples = [ (x, y, z) x <- [1.. ], y <- [x..3], z <- [x..3]] fermat n = [ (x, y, z) x <- [1..20], y <- [x.. 20], z <- [x..20], x<=y, z^n == x^n + y^n] fermata n = [ (x, y, z) p <- [2..], x <- [1..p], y <- [x.. p], z <- [x..p], x<=y, z^n == x^n + y^n] fermatb n = take 1 [ (x, y, z) p <- [2..], x <- [1..p], y <- [x.. p], z <- [x..p], x<=y, z^n == x^n + y^n] fermatstorasats = [ ("n = " ++ show n, fermatb n) n <- [2..]] {-...> fermatstorasats [("n = 2",[(3,4,5)]),("n = 3",{Interrupted!} Inget motbevis än...