Föreläsning 8 i programmeringsparadigm. Kommentarer kring schacklabben. Om man vill kontrollera hur mycket man skrivit i sina Haskell-moduler man köra unix-kommandot wc (word count):..> wc.hs 100 486 3292 Chess.hs 88 474 3432 ChessGraphics.hs 115 688 4839 Chessman.hs 56 245 1850 PlayChess.hs 21 99 653 PolygonGraphics.hs 72 373 2082 Square.hs 9 50 302 Vector.hs 461 2415 16474 total Dvs min lösning har 461 rader, 2415 ord eller 16 474 tecken. Skulle vi kunna gjort på ett bättre sätt, skulle ett kortare program varit bättre? Min lösning är programmerad med moduler som innehåller ADT för modellen av schacket (Square, Chessman, Chess och Vector), funktioner för vyn av typen..>[graphic] (ChessGraphics.hs, PolygonGraphics.hs) huvudfunktionen main::io () (PlayChess.hs) Detta sätt att utforma program/skript i olika paradigm rekommenderas i många böcker, man skriver många moduler som ofta implemterar en ADT och försöker få gränsnitten (engelska interface) så enkla som möjlig för att modulerna skall vara maximalt oberoende och individuellt modifierbara och kunna skrivas av olika personer. Gränsnitten i Haskell programmeras ju i Haskell med exportlistorna, i Javaklasser med private och public. Ju större ett projekt är, desto viktigare är detta arbetsätt. Våra ADT i Haskell implementerar nya typer vars värden vi kan använda i andra moduler. Lite grann kan vi säga att vi skapar ett schack-språk för att kunna programmera schack-problem. En ADT motsvarar i stort sett vad som matematiker kallar en algebra. Även programmering med Java brukar karakteriseras som programmering med abstrakta datatyper, och en java-klass är ju visserligen en typ i Java men instanserna är oftast modifierabara objekt, ej värden. Det blir dock mer att skriva om man gör på detta sätt. Till exempel kunde vi nöjt oss med att låta ett schack vara i stället för en lista med par av typen [(Square, Chessman)] eller t o m [((Int, Int), Chessman)] om vi struntar att skriva ADT Square data Chess = Chess [(Square, Chessman)] -- ev newtype
Programmeringen av många funktioner i Chess skulle då vara enklare, speciellt för den oerfarne, eftersom vi direkt skulle kunns använda alla funktioner för listor och skulle slippa skriva konstruktorn Chess på en massa ställen för skapa ny värden, mönsterpassa eller "plocka ut listan ur typen". Vi skulle slippa sånt som mönster(chess ((sq,cm):sqcms))) lokala bindningar let Chess((sq,cm):sqcms)) = new Chess (Chess...) i högerleden hjälpfunktioner och många slarvfel, parantesfel mm. Vidare skulle också vi kunna skriva alltihop i en enda modul, låta Haskell typa alla funktioner i stället för att själv skriva funktionernas typer, använda lamda-uttryck och stunta i kommentarer och blanka rader. Vårt schackprogram skulledå antagligen kunna skrivas med mindre än 200 rader, kanske mindre än 100 rader. Om man försöker skriva större program med sådan teknik bör man nog vara mycket skärpt och jobba helt ensam med projektet (och slippa jobba med det efter några år då man jobbat med annat). En kompromiss-metod är att använda typsynonymer, t ex type Chess = [(Square, Chessman)] Ett lite svårare exempel. ZipWith. De flesta exempel vi hittills sett har enkelt kunna programmeras med rekursion på ett ganska mekaniskt sätt, med listomfattning eller med lämplig funktion från Preluden. Ibland är det lite svårare, det kan krävas lite mer sofistikerad rekursion till exempel. Se t ex Hutton 6.3, 6.4, 6.5. Låt oss programmera zipwith som parar ihop element från två listor med en funktion. T ex Typen blir zipwith () [1,3,2] [14, 22, 44] blir [15, 25, 46] zipwith :: (a->b->c) -> [a]->[b]->[c] Eftersom två listor, som båda kan konstueras mpå två sätt med : samt [], är inblandade förutom slutresultatlistan får vi 22 = 4 fall i mallen. Sedan vi fyllt i högerleden får vi zipwith _ [] [] = [] zipwith _ (a:as) [] = [] zipwith _ [] (b.bs) = [] zipwith z (a:as) (b:bs) = z a b : zipwith z as bs Efter lite eftertanke kan vi komma fram till Preludens lite kortare definition: zipwith :: (a->b->c) -> [a]->[b]->[c] zipwith z (a:as) (b:bs) = z a b : zipwith z as bs zipwith _ = [] Jag tror faktiskt att Hutton inte tänkt riktig klart vid programmering av zip i avsnitt 6.3 sid 69.
Läsanvisning för funktionell programmering. Hela Hutton-häftet inklusive utdraget ur A gentle Introduction to Haskell ingår i kursen. Det enda som jag inte tycker om i Huttons bok boken är mönster av typen n1. En del av exemplen, ofta i slutet av kapitlen, kan du läsa om du vill få illustrerat hur man tänker, men kan ju också hoppas över eller läsas kursivt, t ex avsnitt 5.5, avsnitt 7.6. Kapitel 12 om lat evaluering kommer jag strax att tala om på ett lite annat sätt än vad Hutton gör, och kommer jag inte att ställa svårare frågor om lat evaluering. Kapitlet kan läsas kursivt. Det jag gjort utöver vad som behandlas i Hutton-häftet är främst IO-programmering (se labhäftet och föreläsning 6) och mer ingående om typklasser (föreläsning 7). När vi senare i kursen studerar syntaxanalys, efter det av vi behandlat logikprogrammering, kommer vi att använda Haskell igen. På tentamen försöker jag ge en del uppgifter som liknar vad man gjort när man labbat. Lat evaluering. (Hutton kapitel 12, Fokker 3.2.5, 3.2.6 sid 48-51). Haskell och "moderna" funktionella språk har lat evaluering. Jag har några ex av Björn vonsydow: Datastrukturer i Haskell" (48 s). Mycket trevlig läsning som bland annat tar upp mer om hur Haskell fungerar, lat evluering och effektivitetssynpunkter. Har används på Chalmers i fortsättningskursen i programmering. I Haskell "körs" programmen genom att vi beräknar ( == ger värden till == evaluerar) uttryck. Uttryck kan skrivas som teckensträngar eller som träd (upp-och-nedvända, praxis i datalogi) : (14 4) (1 22) square (12) 14 4 1 22 $ square 1 2 $ == applikation Evalueringen kan skrivas om omskrivningar/omritningar (==förenklingar, == reduktioner) av deluttryck (ett redex, reducible expression) / delträd. Ibland finns det flera redex att välja på. Två olika strategier: använda "innersta" redex (redex som inte innehåller andra redex) / förenkla trädet från löven använda bästa "yttersta" redex (om möjligt redex inte i redex)/ förenkla trädet från roten. Ett exempel på nästa sida :
square (12) $ square 1 2 För komma vidare måste vi ha def på square (\n -> n n) (12) \n -> n n $ 1 2 Vi räknar ut argumentet (\n -> n n) 3 \n -> n n Applicerar $ 3 3 3 3 Multiplicerar 9 9 3 Innermost evaluation (Function call-by-name) Applicerar.. (12).. 1 2 Vi tvingas addera, strict.. 3.. Applicerar (12) (12) 1 2 1 2 Vi tvingas addera, strict (3) (12) 3 1 2 3 Multiplicerar Vi tvingas addera, strict (3) (3) 9 9 3 3 Multiplicerar 9 Lazy evaluation 9 Outermost evaluation, (Function call-by-name)
Innermost evaluation, som också kallas strikt evaluering eller eager evluation, är det vanligaste i imperativa språk och upplevs nog som det självklara sättet av de flesta. Emmellertid är icke-strirkt evaluering bättre om vissa redex/delträd aldrig behöver evalueras eller vid rekursion som kan ge oändlig rekursion. Outermost evaluation på ett naivt sätt (längst till höger på föregående sida), kan ge ineffektiva program pga att vissa uttyck beräknas flera gånger. Detta kan dock undvikas med lat evaluering; I datorns minne lagras inte uttrycken direkt utan med referenser (pekare) till uttrycken. Referenser till uttryck från på flera ställen refererar bara till ett enda ställe. Krävs förenkling av uttryck förenklas det då bara en enda gång. Uttrycksträdet blir istället en graf. Lat evaulering gör att program som ibland aldig skulle sluta köras, terminera, med innermost evaluation terminera. Med lat evaluering kan man programmera med rekursiva funktioner utan basfall och med potentiellt oändliga datastrukturer. Ett mycket fiffigt och snabbt sätt att lista Fibonacchi-talen: Talföjden (fibs), talföljdens svans (tail fibs) och svansen av talföljdens svans (tail tail fibs) hänger ihop så här: 1 1 2 3 5 8 13 21 fibs 1 2 3 5 8 13 21 34 tail fibs 2 3 5 8 13 21 34 55 tail tail fibs dvs vi kan definiera en konstant fibs som fibs :: [Integer] fibs = 1 : 1: zipwith () fibs (tail fibs) med Prelude-funktionen tail tail (_:xs) :: [a] -> [a] = xs Kör man fibs räknar hugs ut och skriver ut talen i listan i all oänlighet. Vill man ha de 100 första skriver man take 100 fibs Prelude-funktionen take : take :: Int -> [a] -> [a] take n _ n <= 0 = [] take _ [] = [] take n (x:xs) = x : take (n-1) xs Exempel på vad som händer (reservation för fel..)
take 5 fibs för att välja ekvation för take 5 måste vi veta mer om fibs, blir take 5. (:) 1 (:) 1 zipwith (). tail. vi kan välja ekvation för take 5, bindningar take 5. (:) binds till x 1 binds till xs (:) 1 zipwith (). tail. 1: take 4. (:) 1 använder högerledet, blir (:) 1 zipwith (). tail. dags för take 4, vi kan välja ekvation för take 4, bindningar 1: 1:take 3. (:) 1 (:) binds till x 1 binds till xs zipwith () tail.. använder högerledet, blir 1: 1:take 3. (:) 1 (:) 1 zipwith (). tail. 1:1: take 3. (:) 1 för att välja ekvation för take 3 måste vi veta mer om zipwith.., och måste då vet räkna ut tail.., blir (:) 1 zipwith ()..
använder högerledet för zipwith, blir 1:1:take 3. (:) 1 (:) 1 (:) (1 1) zipwith ().. nu kan vi välja ekvation för take 3, använd högerledet, x binds till 1 1 som räknas ut, xs binds till zipwith... 1:1:2:take 2. (:) 1 (:) 1 (:) 2 zipwith ().. för att välja ekvation för take 2 måste vi veta mer om zipwith.., blir... osv blir 1:1:2:3:5 : take 0 1:1:2:3:5 : []. grafen blir Talen som står längst till vänster i bilderna dvs 1, 1 1, 1 1 2 är nog också egentligen pekare in till motsvarande tal i grafen, men jag har skrivit ut dem för att det skall bli tydligare. Märk att användning av tail eller mönsterpassning på mönster av typ x:xs ej innebär att kopior skapas, bara omflyttning av pilar! Dvs minne förbrukas ej! Detta beror på att värden aldrig förändras, vi har ej destruktiv uppdatering i minnet i funktionella språk. Körning av fibs!! 5 skulle fungera på liknande sätt men slutresultatet skulle då bli en enda pekare in i grafen. En del av grafens noder skulle då ej refereras, dvs ej "pekas på", och kan tas bort, varvid ytterligare noder ej refereras, varför de kan tas bort, osv rekursivt. Det görs automatiskt i funktionella språk, liksom i Java, för att spara minne av en skräpsamlare, (engelska garbage collector).
The Awkard Squad: Haskell och kod i andra språk. Överkurs. Verkligheten Verkligheten c o n c u r r e n c y. Interaction ( -> IO a) föreläsning 6 Computational Kernel ("Vanlig Haskell") samverkan med kod skriven i andra språk e c r a r t o c r h Verkligheten Verkligheten För att Haskell skall slå igenom krävs också att de fyra "skalbitarna" i bilden ovan finns. I Haskell löses detta med IO a - programmering. Vill man kompilera Haskellprogram (slutresultatet skall då vara main :: IO(), som kan köras direkt fån operativsystmet) är den vanligaste kompilatorn ghc. Till den finns utvidgningar och bibliotek för de alla skalbitarna. Haskell har en standard (senare versioner av ghc och hugs följer den) för att kommunicera med kod skrivna i andra språk, "Foreign Function Interface 1.0 ". Den som är intressarad kan få mer information här: http://www.cse.unsw.edu.au/~chak/haskell/ffi/ Jag har gjort lite experiment med en tidigare version och fått kod i Haskell att samarbeta med kod i C. Jag har mestadels använt Haskell-kompilatorn ghc/5.04.2, som även kan kompilera c-kod Några axplock bland mina experiment finns på tre underkataloger till /info/progp03/haskellandc/. Kompilerings- och köranvisningar finns i.hs filerna. Kopiera filerna till egen katalog. Vid kompileringarna uppstår en massa nya filer, de körbara filerna blir stora. Lästips : Simon Peyton Jones :Tackling the awkward squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell (länk finns på kursens startsida). Bland de mer lättlästa artiklana om IO a. Handlar om det som brukar saknas i vanliga Haskell-böcker, och delar av Haskell som ofta är viktiga i "verkliga" projekt. För att komma åt artiklen måste man kunna gunzip.
Så här får man tillgång till ghc:..>module add ghc Man kan nu använda ghc interaktivt på detta sätt (mycket likt som man använder hugs, men man kan inte använda SOEGraphics):..>ghci Med ghci kan blanda ett interaktivt körsätt av någon Haskell-module med färdigkompilerad kod för importerade moduler. Vill man göra rena kompileringar skrivar man..>ghc...... Kompilatorn genererar C-kod, som därefter automatiskt kompileras med C-kompilatorn gcc och laddas med ld. Jag har lärt mig att även blanda in kod som skrivits med C, jag kanske skriver mer om detta i kommande föreläsningshäften. Haskell och Tcl/Tk. Överkurs. Tcl/Tk är, tror jag, C-världens sätt att göra GUI program på samma sätt som man i Java kan göra program med knappar (Buttons) och andra grafiska komponenter. I Bremen har man gjord ett paket HTk i Haskell som använder Tcl/Tk. Jag har lyckats kombinera detta med vår schackmodell från labbarna: Interaction main i PlayChessTcl.. changechess i PlayChessTcl modell Square Chess Chessman vy styrning Computational Kernel ("Vanlig Haskell") Shell -> IO a PlayChessTcl innehåller kod för hela vyn och den interaktiva delen av styrningen. Om du har lust att pröva min PlayChessTcl tillsammans med dina Sqaure, Chess och Chessman så finns programmet på /info/progp03/playchesstcl.hs. Man måste ha kompletterat Chess på det sätt som beskrivs i steg 8 och 9, namnen på vissa fuktioner måste ju stämma osv. Kompilering och laddning tar lång tid, den körbara koden blir 8 MByte, det har varit mycket krångel innan vi fick Htk att funka, så lycka till...
Däremot : Den rena Haskell-delen (modellen från SOEGraphics- versionen) var bara att smacka in, det funkade direkt (förståss!?). Hur man kompilerar och laddar står i början av filen Du kan (förhoppningsvis) även köra mitt program som...>setenv LD_LIBRARY_PATH ${LD_LIBRARY_PATH}:/pkg/gnu-libs/1.1/lib...>PlayChessTcl Avancerad kurs i funktionella språk. 2D1456, Advanced Functional Programming, 4 credits, D Why should I renounce for you, dear Haskell, My much yearned for side-effects? - Don Smith Course teacher: Course Description Johan Glimming In this course we study functional programming languages from two points of view: 1) their use as foundations for programming languages such as Java, 2) their use in their own right as one of the most powerful programming paradigms. We will study a range of topics, starting with the foundations of lambda calculus (both untyped, typed, and second-order), type inference and the Hindley-Milner type system, type classes, monads, data structures and algorithms in a (purely) functional setting, and finally program transformations, an excellent formal method for development of correct programs from their specifications. Students will have plenty of opportunity to explore advanced functional programming in their mini-projects, examined non-trivial programming exercises. The underlying theory of functional programming is explained in a series of 14 two-hour lectures. Rekomenderas varm! Haskell, paradigmer och multimedia. I denna roliga bok (som jag använde som kursbok i en 5p-kurs i funktionell programmering, kursbok för 2D1456, Advanced Functional Programming) : Paul Hudak: The Haskell School of Expression, Learning Functional programming through multimedia. ISBN 0-521-64408-9 används Haskell för att göra animeringar och datorspel i Haskell. Animeringarna ses som värden av typ Time -> [Graphic]. Animeringar och datorspel har ju mycket IO och mycket jämlöpande processer, vilket visar att paradigmgänserna är ganska konstlade. Boken innhåller också avsnitt om robotprogrammering och om musik.
Ett lite svårare exempel. Myntväxlingsproblemet. De flesta exempel vi hitills sett har enkelt kunna programmeras med rekursion på ett ganska mekaniskt sätt, med listomfattning eller med lämplig funktion från Preluden. Ytterligare ett exempel. Växla ett belopp till svenska mynt. Exempel på körning av det färdiga programmet : Main> change 2750 "2 tiokronor, en femkrona, 2 kronor och en femtioering" dela upp problemet : change :: Int -> String change = pretty. howmany. (change1 swedcoins) swedcoins :: [Int] swedcoins = [1000, 500, 100, 50] change1 presenterar resultatet på denna form: Main> change1 2750 ger detta resultat [1000, 1000, 500, 100, 100, 50] howmany. change1 presenterar resultatet på denna form: Main> (howmany.change1) 2750 ger detta resultat [(2,1000), (1,500), (2,100), (1, 50)] Delproblem: change1 : change1 :: [Int] -> Int -> [Int] change1 [] amount = [] change1 (c:cs) amount amount >= c = c : change1 (c:cs) (amount - c) otherwise = change1 cs amount
Delproblem: Snyggare resultat med howmany: För att förvandla [1000, 1000, 500, 100, 100, 50] till [(2, 1000), (1, 500), (2,100), (1, 50)] försöker skriver vi en funktion howmany :: [a] -> [(Int, a)] Parametern är en lista; typen lista har två konstuerare : []; vi startar med standardmallen howmany [] = howmany (x:xs) = Icke-rekusiva fallet är lätt, men det rekursiva fallet, vad sakll vi göra med x och rekursionsantagandet (howmany xs)? howmany [] = [] howmany (x:xs) =???? x (howmany xs) Lite svårare! Till exempel ska 100 slås ihop med [(1, 100), (1, 50)] eller 100 slås ihop med [(1, 50)]. Konventionell lösning : Funktionella programmerare definerar funktioner! Definiera en hjälpfunktion addon för????! addon x [] = [(1,x)] addon x ((i,y ) : iys) x == y = (1i, y): iys otherwise = (1, x) : (i,y ) :iys Klart? Nja vid provkörning får vi : ERROR "Money.hs":3 - Cannot justify constraints in explicitly typed binding Expression : howmany Type : [a] -> [(Int,a)] Given context : () Constraints : Eq a Typen är för generell; Vi får byta till howmany :: [a] -> [(Int, a)] howmany :: Eq a => [a] -> [(Int, a)] Klart! Trimmning : Tittar vi på definitionen på howmany ser vi om vi är vana att det är en "vikning", alltså är t ex detta en kortare definition: howmany = foldr addon [] n
En sämre lösning: Om vi vill slippa skriva en ny funktion för att programmera det rekursiva högerledet???? x (howmany xs) i den icke-akumulerande lösningen kan vi göra så här (lite imperativt; "först så gör vi rekusionsantagandet och mönsterpassar så vi får ut de delar vi behöver, och sedan är det lättare att skriva det vi vill ") howmany (x:xs) = let (i, y) : iys = howmany xs in if x == y then (1i, y): iys else (1, x) : (i,y ) :iys Hugs protesterar int men när vi provkör får vi fel (Den som inser felet är duktig)..> howmany [2, 2, 2, 3] Program error: {howmany_v1314 []} Problemet är att när vi har ett lista med ett elemnent ; howmany (2:[])passar (x:xs) med x bundet till 2 och med xs bundet till []. Sedan försöker vi göra let in (i, y) : iys = howmany xs dvs, eftersom howmany [] = [], (i, y) : iys = [] vilket inte går. En korrekt lösning måste ha ett basfall till, vilket är svårt att inse: howmany [] = [] howmany (x:[]) = [(1, x)] howmany (x:xs) = let (i, y) : iys = howmany xs in if x == y then (1i, y): iys else (1, x) : (i,y ) :iys En akumulerande lösning på howmany: howmany list = reverse (hm list [])-- reverse annars 50-oering först hm :: Eq a => [a] -> [(Int, a)] -> [(Int, a)] hm [] acc = acc hm (x:xs) [] = hm xs [(1, x)] hm (x:xs) ((i,y ) : iys) x == y = hm xs ((1i, y):iys) otherwise = hm xs ((1, x) : (i,y ) : iys)
Delproblem: Resultatet som sträng med pretty : För att förvandla [(2, 1000), (1, 500), (2,100), (1, 50)] till " 2 tiokronor, 1 femkrona, 2 kronor och en femtioöring " Hur gör vi då? Hur formaterarar vi strängar? Med standardfunktioner! Första försök : pretty :: [(Int, Int)] -> String pretty ims = concat [show i " " tocoin m ", " (i, m)<-ims] tocoin :: Int -> String tocoin 1000 = "tiokronor" tocoin 500 = "femkrona" tocoin 100 = "kronor" tocoin 50 = "femtioering" tocoin _ = "" Körning: pretty (howmany [1000, 1000, 500, 100, 100, 50]) ger "2 tiokronor, 1 femkrona, 2 kronor, 1 femtioering, " Ett andra försök : pretty :: [(Int, Int)] -> String pretty ims = concatspec [ fix (i, m) (i, m) <- fix :: (Int, Int) -> String fix (1, 1000) = " en tiokrona" fix (i, 1000) = show i " tiokronor" fix (1, 500) = " en femkrona" fix (i, 500) = show i " femkronor" fix (1, 100) = " en krona" fix (i, 100) = show i " kronor" fix (1, 50) = " en femtioering" fix (i, 50) = show i " femtioeringar" fix _ = "" concatspec :: [String] -> String concatspec [] = "." concatspec (x:[]) = x concatspec (x1:x2:[]) = x1 " och " x2 concatspec (x :xs) = x ", " concatspec xs ims] Körning:..> pretty (howmany [1000, 1000, 500, 100, 100, 50]) ger "2 tiokronor, en femkrona, 2 kronor och en femtioering",
Hela programmet: swedcoins :: [Int] swedcoins = [1000, 500, 100, 50] change1 :: [Int] -> Int -> [Int] change1 [] amount = [] change1 (c:cs) amount amount >= c = c : change1 (c:cs) (amount - c) otherwise = change1 cs amount howmany = foldr addon [] n -- ev akumulerande lösning istället where addon x [] = [(1,x)] addon x ((i,y ) : iys) x == y = (1i, y): iys otherwise = (1, x) : (i,y ) :iys pretty ims = concatspec [ fix (i, m) (i, m) <- fix :: (Int, Int) -> String fix (1, 1000) = " en tiokrona" fix (i, 1000) = show i " tiokronor" fix (1, 500) = " en femkrona" fix (i, 500) = show i " femkronor" fix (1, 100) = " en krona" fix (i, 100) = show i " kronor" fix (1, 50) = " en femtioering" fix (i, 50) = show i " femtioeringar" fix _ = "" concatspec :: [String] -> String concatspec [] = "." concatspec (x:[]) = x concatspec (x1:x2:[]) = x1 " och " x2 concatspec (x :xs) = x ", " concatspec xs change :: Int -> String change = pretty. howmany. (change1 swedcoins) ims] För att framhäva hur funktionerna använder varandra skulle man kunna ha lokala definitioner av hjälpfuktioner och få en struktur som change =... where swedcoins =... change1 =... howmany =... where addon =... pretty =... where concatspec =... fix =...