Föreläsning 2 i Programmeringsparadigm Funktionell programmering := variabler pekare sidoeffekter ;...; Fy, fy => bugfree förståeliga korta inga "satser", bara uttryck! Enjoy Vattenfallsmodellen i funktionella språk Specifikation ofta ekvationer Design funktioner def med ekvationer Kodning funktioner def med ekvationer Högre ordningens funktioner, def med ekvationer Funktioner kan var argument till funktioner Funktioner kan var resultat till funktioner Hög abstraktionsnivå => kraftfulla språk Lat evaluering, ger oändliga datastrukturer Resultat räknas ut fört när de behövs, effektivt => kraftfulla språk T e alla namn referenser, ingen (eplicit) "pekarprogrammering" => kraftfulla språk Haskell logiskt bra typsystem => Hittar många slarvfel Haskell Typklasser => Överlagrade funktioner Haskell Moduler => Modern programutveckling ADT
Historik, geografi Fokker 1.1 C&K 1.1 (Chakravarty & Keller) När man vill tänka imperativt (simulering)? Gräns mellan paradigmer Bra begrepp? Om Hudaks bok, Fokkers bok, Chakravarty & Kellers bok Praktik : Räknedosa, Interpretern hugs ModulenPrelude Räknedose- användning laddas alltid automatiskt och innehåller många fördefinierade funkitoner,..> uttryck operatorer och typer svar skal-anrop Inladdning av skript med ytterligare definitoner, load reload av script quit..>:! emacs..>:l...hs..>:r..>:q Programmering av skript med nya definitoner som sparas på fil hugs definitioner av funktioner konstanter typer moduler klasser emacs Jag rekommenderar användning av interpretatorn hugs. Hos Fokker körs Haskell med en annan interpretator, så avsnitt 1.2 i Fokker är lite annorlunda. I C&K 2.2 används GHCi, som är mycket lik hugs. I labanvisningen för labben "Introduktion till Haskell" finns anvisningar hur man använder hugs. På föreläsningen går jag igenom hur man använder hugs med hjälp av teten och bilderna i labbanvisningen och bilden ovan. Först använder vi hugs som en räknedosa med fördefinierade funktioner och operatorer (från filen Prelude som alltid "laddas in"). Fokker använder ganska avancerade fördefinierade funktioner i 1.2 och 1.3, t e product och operatorn.. i product [1..n] och definierar genast egna funktioner såsom fac och choose och en egen operator!^! i 1, 2 och 1.3. Jag (och C&K) kommer att gå lite försiktigare fram. Men snart är det dags att skriva (definiera) egna funktioner, vilket är "att programmera" i Haskell. För att få godkänt på den här kursen: Gör..>res checkin progp04
Typer och operatorer i Haskell. Översikt av några fördefinierade (dvs de finns i "Preluden", "förspelet") datatyperna och de viktigaste operatorerna med vars hjälp vi kan bilda uttryck (Fokker 1.3.3 1. 3. 4): mod (rest vid heltalsdivision) div (heltalsdivision) + - * ^ + - / * (32 bit) Int (64 bit) Double 2 12-67 2356789-976543 + - / * går i Haskell 23.67-890.78-45.89 56.34E23 && (logiskt och, båda True) < <= > >= == /= Bool True False < <= > >= == /= < <= > >= == /= (lika med, inte lika med) Char A a 7! /65 not (logiskt inte) Det finns fler primitiva datatyper än Int för heltal, bl a Integer med "oändligt antal siffror". Det finns förutom Double ytterligare en primitiva datatyp Float för flyttal ("decimaltal"), det som skiljer är att bara 32 bit tilldelas sådan variabler i minnet i stället för 64 bitar och att antalet signifikanta siffror och största och minsta eponent är därför är mindre. Typen String : type String = [Char] Strängar kan skrivas ".." konkateneras med ++.
Definition och användning av en enkel funktion: Man skriver alltså i hugs uttryck för att få hugs att beräkna något, dvs "köra" eller eekvera. Man använder i Haskell även uttryck (epressions) i högerleden när man definierar egna funktioner. Egna funktioner får namn i en funktionsdefinition. Funktioner är ett slags värden i Haskell. I Haskell kan alla sorters värden ges namn (identifierare) som börjar på liten initial. Värden skrivs med Stor (Versal) initial eller med krumelurer, t e True, False, 12, 3:[], (2. 5). I imperativa spåk ger man ju ofta namn till delar i minnet ("variabler"), som i sin tur kan innehålla olika värden under körningens gång. Detta gör vi aldrig i fuktionell programmering. Namnen står alltid för samma sak, samma värde, hela tiden! Detta är den stora vitsen! Hur gör man om man vill definiera egna funktioner (Java : metoder som returnerar värden), och hur använder man dem, kanske gång på gång? Jo: Java Haskell Matte Definition av f: f() R R class...{ f::double->double static double f(double ) { f = + 3.0 Vi definierar f() = +3 return +3; } (I emacs fönstret) Vi kallar för (formell) parameter, i matte ofta "variabel" som ju betyder något helt annat i imperativ programmering! Märk att man inte behöver skriva parantes kring i f. Användning (anrop, applikation) av f...main(...) { utdata.println( "f(14.0) = " + f(14.0)); f 14 Vad blir f(14)? 17.0 (I hugs fönstret) Vi kallar 14.0 och 2.0 för argument (i vissa böcker aktuell parameter). Märk att man inte behöver skriva parantes kring 14.0 i f 14.0.
Ytterligare ett eempel. Herons formel : Haskell Matte Om en triangel har sidor med längderna a, b och c kan triangelns yta beräknas med Herons formel: area1 ::(Double, Double, Double) -> Double area1(a,b,c) = sqrt (p*(p-a)*(p-b)*(p-c)) where p = (a+b+c) / 2 area1 :: R3 -> R area1(a,b,c) = V p(p-a)(p-b)(p-c) där p = (a+b+c) / 2 Här ser vi på funktionen area1 som en funktion med en parameter som är en tripel med flyttal, dvs av typen (Double, Double, Double) till ett resultat som är ett flyttal, typen Double. I gymnasie-matematiken säger man ofta att area1 är en funktion med tre parametrar ("variabler"), men detta är slarvigt, en funktion har en enda parameter. Körning: Main> area1 (3.0, 4.0, 5.0) Beräkna några trianglars ytor, 6.0 t e den egyptiska triangeln med sidan 3, 4 och 5. Det vanligaste sättet att i Haskell skriva en funktion för Herons formel är dock detta: (Matematikerna Schönfinkel och Haskell Curry uppfann detta bättre sätt på 1920-talet och 1940-talet, men det har ännu inte slagit igenom ännu i matteundervisningen i skolorna): area ::Double -> Double -> Double -> Double area :: R -> R -> R -> R area a b c = sqrt( p*(p-a)*(p-b)*(p-c)) area a b c = V p(p-a)(p-b)(p-c) where p = (a+b+c) / 2 där p = (a+b+c) / 2 Detta sätt att skriva är ganska ovant, och i början kan du se detta som ett sätt att i Haskell skriva att area är en funktion av tre parametrar. Det korrekta är dock egentligen att funktionen area är en funktion med en parameter av typen Double som får som resultat av typen Double -> Double -> Double. Funktioner har nämligen alltid en parameter och kan returnera resultat som är funktioner.
Körning: Main> area 3.0 4.0 5.0 Beräkna några trianglars ytor, 6.0 t e den egyptiska triangeln med sidan 3, 4 och 5. Detta sätt att skriva är ganska ovant, och i början kan du se detta som ett sätt att i Haskell applisera area på tre argument. Det korrekta är dock egentligen att att funktionen area appliseras på ett argument, i vårt fall 3.0, och att resultatet blir en funktion av typen Double -> Double -> Double. Denna nya anonyma funktion kan i sin tur appliseras på en nytt argument av typen Double, i vårt fall 4.0 och vi får ett resultat av typen Double -> Double. Denna nya anonyma funktion kan i sin tur appliseras på en nytt argument av typen Double,i vårt fall 5.0 och vi får ett slutresultat av typen Double, i vårt fall 6.0. I stället för where kan man i definitioner använda let.. in. I vårt fall area a b c = let p = (a+b+c) / 2 in sqrt( p*(p-a)*(p-b)*(p-c)) I båda fallen är namnet p som knyts till värdet av uttrycket (a+b+c) / 2 bara användbart i definitonen av area. Java: import java.io.*; public class Heron { static BufferedReader indata = new BufferedReader(new InputStreamReader(System.in)); public static void main(string[] iargs) throws IOEception { System.out.println("Herons formel beräkning av trianglars ytor"); System.out.print("Ge längden för sida a : "); double a = Double.parseDouble(indata.readLine()); System.out.print("Ge längden för sida b : "); double b = Double.parseDouble(indata.readLine()); System.out.print("Ge längden för sida c : "); double c = Double.parseDouble(indata.readLine()); System.out.println("Triangelns yta = " + area(a, b, c)); } public static double area(double ia, double ib, double ic) { double p = (ia + ib +ic) / 2.0; //Herons formula return Math.sqrt(p*(p-ia)*(p-ib)*(p-ic)); } } /* Körresultat Herons formel för beräkning av trianglars ytor Ge längden för sida a : 3.0 Ge längden för sida b : 4.0 Ge längden för sida c : 5.0 Triangelns yta = 6.0 */
Att definiera en funktion: Studera definitionsområdet! Några eempel: en ekvation med ett fall/ flera ekvationer/ en ekvation med flera fall (vakter/case/if)? --------------------------------------------------------------------- Haskell Matte 1.0 3.0-14.0... 4.0 6.0-11.0... f::double->double f = + 3.0 En ekvation klarar alla värden Vi definierar f() = +3 --------------------------------------------------------------------- e j(b) True False True o False True False o b False True ej::bool-> Bool ej True = False Två ekvationer klarar båda värdena False om b = True ej False = True Mönsterpassning Fokker 1.4.3 C&K implicit ej(b) = True (pattern match) Värden, ej namn i V.L. om b = False f() R B B R Sämre: En ekvation med flera fall: med vakter (guards): Fokker 1.4.2 C&K 3.2.2 ej :: Bool -> Bool ej b b = False otherwise = True med if-uttryck (C&k 3.2.1) ej :: Bool -> Bool - ej b = if b then False else True med case-uttryck ej :: Bool -> Bool ej b = case (b) of True -> False False -> True
Haskell Matte --------------------------------------------------------------------- positive() 1.0 True 3.0-14.0 True -6.0 False False positive::double->bool positive En ekvation med vakter True om >= 0 >= 0 = True klarar alla värden positive() = otherwise = False False i övriga fall R B Mönsterpassning med flera ekvtioner fungerar ej. Däremot möjligt men sämre med if eller case. --------------------------------------------------------------------- Rekursion. Om definitionsmängden består av ett ändligt antal värden så kan man helt enkelt skriva en ekvation för varje värde. Det var det vi gjorde när vi definierade ej. Men med ett definitionsområde med oändligt antal värden fungerar det ej, men ofta kan man då använda ett namn i vänsterledet och uttrycka resultatet med ett uttryck som använder detta namn i högerleetd. Ibland blir det ett enda fall i bland flera. I högerledetsuttrycket är är det tillåtet att använda ett anrop till den funktion som vi just definierar. Detta är ofta användbart. Fokker behandlar detta i 1.4.4. (Tillämpningen på listor på sid 13 kan vi ej än), C&K i kapitel 4. Hur kan detta fungera? Jo precis som vanligt! hugs förenklar utrycket som förut tills vi får ett slutresultat som skrivs ut. Det som krävs är att vi för eller senare hamnar i ett icke rekursivt fall, ett basfall.
Ett eempel: Låt os definera power(m, n)= mn Haskell Matte ---------------------------------------------------------------------- 2 3 4 5 0 3 6-14 -6 1 27 729... error "" I, N I power(m, n) m n 1 om n=0 power (m, n) = odefinierat om n<0 m*power(m, n-1) power1 :: Int -> Int -> Int -- vakter för n< 0 Bäst! power1 _ 0 = 1 -- Fokker 1.4.2 power1 m n n<0 = error " second argument till power >= 0" otherwise = m * power1 m (n-1) Eller om vi antar att vi ej skall använda negativa argument (I matte skulle man säga att definitionsmängden är de naturliga talen N. Denna datatyp finns inte färdig i Preluden i Haskell.) power :: Int -> Int -> Int -- rekursiva lösningar för n>0 power _ 0 = 1 -- Fokker 1.4.3 1.4.4 power m n = m * power m (n-1) Enraderskommentarer skrivs med -- i Haskell, // i Java. Flerraderskommentarer omges med {- -} i Haskell, /* */ i Java.
Hur fungerar power? Haskell-system förenklar helt enkelt uttryck (med kontroll av typerna coh använding av definitionerna). Jfr imperativa språk där man för att förstå semantiken måste tänka sig en vonneuman-maskin. power 3 6 blir 3 * power 3 5 blir 3 * (3 * power 3 4) blir 3 * (3 * (3 * power 3 3)) blir 3 * (3 * (3 * (3 * power 3 2))) blir 3 * (3 * (3 * (3 * (3 * power 3 1)))) blir 3 * (3 * (3 * (3 * (3 * (3 * power 3 0))))) blir 3 * (3 * (3 * (3 * (3 * (3 * 1))))) blir 3 * (3 * (3 * (3 * (3 * 3)))) blir 3 * (3 * (3 * (3 * 9))) blir 3 * (3 * (3 * 27)) blir 3 * (3 * 81) blir 3 * 243 blir 729 Minnesåtgång kan bli stor. (En rättfram rekursiv lösning på labbens Fibonacchi-uppgift blir mycket ineffektiv, varför?). En bättre lösning är : power :: Int -> Int -> Int -- accumulerande lösningar med where power _ 0 = 1 power m n n<0 = error " second argument till power >= 0" otherwise = pw m n 0 1 where -- layout Fokker 1.4.5 pw :: Int->Int->Int->Int-> Int pw m n i acc i==n = acc otherwise = pw m n (i+1) (acc*m) power 3 6 blir -- Körning, dvs förenkling eller reduktion pw 3 6 0 1 blir -- Kallas även evaluering, eng evaluation pw 3 6 1 3 blir pw 3 6 2 9 blir pw 3 6 3 27 blir pw 3 6 4 81 blir pw 3 6 5 243 blir pw 3 6 6 729 blir 729 Minneseffektiv. Även i funktionella språk måste man (förstås) tänka ibland. Att denna lösning "inte sväller ut" beror på att det rekursiva anropet av pw ej ingår i något uttryck. Man säger att pw är svansrekursiv."imperativa programmerare" lär sig att skirva "loopar" i dessa fall och att bara tillgripa rekursion när det är svårt att lösa iterativt. (Mer om effektivitet i Fokker kap 5, fast svårläst än så länge) Lösningar med accumulatorer (och, kommer senare, until och foldl men inte foldr) är svansrekursiva.
Egna operatorer. Man kan i Haskell lika lätt definera egna operatorn som funktioner. Om vi vill kunna skriva power 3 6 som 3 ^* 6 (svar 729) kan man göra detta med denna definition: (^*) = power Man kan ocså skriva 3 power 6 Om namn på operator Fokker p 6. ( ) gör funktioner av operatorer Fokker 2.1.1. gör operatorer av funktioner. "Skåpregler" I lösningen ovan definieras hjälpfunktionen pw som en lokal funktion till power med hjälp av where. Man skulle också kunna använda let.. in. I båda fallen kan namnet pw bara användas i definitonen av power, "lokalt scope", lokalt giltighetområde. Vid programutveckling är det ofta enklast att definera power och pw som power :: Int -> Int -> Int -- accumulerande lösningar med where power _ 0 = 1 power m n n<0 = error " second argument till power >= 0" otherwise = pw m n 0 1 pw :: Int->Int->Int->Int-> Int pw m n i acc i==n = acc otherwise = pw m n (i+1) (acc*m) Namnet kan då användas "överallt i modulen", "globalt scope", globalt giltighetområde, och kan användas i provkörningar från hugs. Men i större program anses det ofta som mindre snyggt att låta namn som används lokalt vara globala. Fast Haskell-program blir inte så ofta stora..
Layoutregler. Efter where och let måste "syskon-definitioner" stå rakt under varandra. Detta gäller även "globala" definitioner, eftersom varje fil inleds med en definition module.. where (se labhandleningen). Börjar man längre till vänster på en rad uppfattas detta som man slutat med "syskondefinitioner", börjar man längre till höger som att man använder flera rader för en definition. Detta är mycket praktiskt, men ovant i början. Gör man fel skriver hugs ofts något med att den saknar ;. Se Fokker 1.4.5 C&K 3.6 Att definiera nya (egna) typer. Moduler och ADT. Föresläsningen avslutades med 20 minuter med en snabbkurs i datatypdefiniering, för att komma fram till listor som finns lite av i lab1. Kommer att repetera detta i föreläsning 3. Jag följde delar av kapitel 8 i C& K. De som köpt Fokker kan istället läsa utdraget ur A Gentle Introduction to Haskell 98, sid 13 i labbhäftet. Hit hann vi! Fler eempel mm. På /info/progp02/haskelllectures/f2 finns Date.hs Fallanalys.hs JavaPower.java Power.hs Skatt.hs. Power.hs innehåller totalt 15 olika sätt att definiera power! Jag hoppas att Fokker 1.1 1.3 (dock ej 1.3.5 och 1.3.6), 1.4, 2.1.1 nu till stora delar skall vara lättförståeligt. 2.1.2 och 2.1.3 och 2.14 handlar om ytterligare mekanismer när man definierar operator, bla om prioitets och assocvitet så att man kan undvika parateser i yttryck med många operatorer. C&K Ch 1, Ch3.1 3.2, 3.3 Ch 4.1 bör också vara förståeligt. Märk att funktionsapplikation har högsta prioritet i Haskell! Det vi behandlat på denna föreläsning finns sammanfattat i "att definiera en funktion i Haskell" i labhäftet! Lämpliga övningsuppgifter: i Fokker: 1.4 1.8 1.9 del av 1.10, 2.2. i C&K CH 1.3 alla, Ch 4.5 E 3.