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ög abstraktionsnivå => kraftfulla språk T e alla namn är referenser, ingen (eplicit) "pekarprogrammering" Haskell logiskt bra typsystem => kraftfulla språk => Hittar många slarvfel Haskell Typklasser => Överlagrade funktioner Högre ordningens funktioner, def med ekvationer => kraftfulla språk Funktioner kan var resultat till funktioner Funktioner kan var argument till funktioner Lat evaluering, ger oändliga datastrukturer Resultat räknas ut fört när de behövs, effektivt Haskell Moduler => Modern programutveckling ADT Jämför Hutton 1.3 Hutton nämner också listomfattning (eng list comprehnsion), och monadprogrammering, som vi skall lära oss, och möjligheten att bevisa att program är korrekta.
Historik, geografi Hutton1.4 Fokker 1.1 C&K 1.1 (Chakravarty & Keller) När man vill tänka imperativt (simulering)? Gräns mellan paradigmer Bra begrepp? 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.. 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"). I funktionella språk skriver man uttryck, och man kan få datorn att beräkna uttryck. Uttrycken kan innehålla operatorer och funktioner från "Prelude". Värdena i uttrycket är "indata", hugs svar "utdata". Snart är det dags att skriva (definiera) egna funktioner, vilket är "att programmera" i Haskell. Man kan även definiera egna värden tillhörande olika typer och gruppera typer. Det är i stort sett altt, det räcker! Det finns inga "satser" (kommandon), inga programmeringsvariabler, mm, mm.. Behövs ej! För att få godkänt på den här kursen: Gör..>res checkin progp05
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 (hutton kap 3): 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(hutton kap 4): 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 i ett skript (i emacsfönstret) som man sedan kan ladda in och använda i hugs. Egna funktioner får namn i en funktionsdefinition. Funktioner är ett slags värden i Haskell, känns ovant i början. 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).T e kan det i ett skript stå nysida = True eller pi = 3.14 eller f = 3.0 *. 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. Hutton gör så i kapitel < 7. Det korrekta är dock egentligen (Hutton 3.6, kapitel 7) 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 Hutton 4.3 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): Hutton 4.3 Fokker 1.4.2 C&K 3.2.2 ej :: Bool -> Bool ej b b = False otherwise = True med if-uttryck (Hutton 4.2, 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. hutton väntar med detta till kapitel 6. 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. 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. (Hutton 2.4.3) 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 ;. Jag hoppas att mycket av Huttons inledande kapitel ungefär (1 <= kapitel <= 4, delar av 6) ska vara begripligt. Vi har dock ännu inte tagit upp t e 3.7, 3.84.6, 4.5, 4.4.1, 4.42, 4.4.3 (4.4.3 tänker jag strunta i), kapitel 5. Efter det vi studerat hur man gör typer ska vi komplettera med överhoppat material. Märk att funktionsapplikation har högsta prioritet i Haskell! Lämpliga övningsuppgifter:i Hutton 1.7.1,1.7.3, 2.6.1, 2.6.3 Det vi behandlat på denna föreläsning finns sammanfattat i "att definiera en funktion i Haskell" i labhäftet! Att definiera nya (egna) typer. Moduler och ADT. Hutton hade i juni ännu intge skrivit sitt typkapitel, så istället läser vi utdraget ur A Gentle Introduction to Haskell 98, som finns i slutet av häfte som börjar med uttdraget från Hutton. 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!