Föreläsning 6 i programmeringsparadigm. Tips kring programmering i Haskell och kring labbarna. Att arbeta med två fönster. Hugs är ju en tolk (interpreter) vilket har stora fördelar vid programutveckling. Man kan ju efter en inledande inladdning till Hugs med :l fortsätta med att göra :r när man gör någon ändring i filen med Haskellkod (spara först!). I Hugs kan man sedan skriva olika uttryck och göra små experiment med sina definierade funktioner och se vad funktionerna får för resultat med olika (enkla) argument. För att få information om något namn som man definierat eller som definierats i preluden, t ex en funktions typ, kan man göra :i <namn> eller : info <namn>: Prelude> :i length length :: [a] -> Int Man kan också helt enkelt skriva funktionsnamnet som ett uttryck (en funktion är ju ett värde, och ett värde är det enklaste formen av uttryck): Prelude> length ERROR - Cannot find "show" function for: *** Expression : length *** Of type : [a] -> Int Värdet skrivs inte ut då funktioner inte ingår i typklassen Show, varför någon funktionen show inte finns för funktionsvärden. Man kan också använda :t (eller :type ) : Prelude> :t length length :: [a] -> Int Att själv skriva typer: Jag rekommenderar att när man skall skriva t ex en ny funktion först tänka ut typen och skriva in den i Haskell-texten, trots att det i regel egentligen inte behövs då Haskell själv härleder typen. Sedan definierar man funktionen / värdet. Protesterar Haskell, t ex om är typen för generell eller om man får något annat typfel, kan man kommentera bort typuttrycket (med en-raders-kommentar : --, OBS mellanslag efter -- ) och försöka igen. Går det bra kollar man vad Haskell föreslår för typ. Sedan tänker man. Observera: I definitioner med flera ekvationer att Haskell bestämmer sig för typen i första ekvationen om man inte typar själv. Hittar hugs en annan typ i andra ekvationer blir det fel. Typiskt är att man stirrar på ekvationen för det rekursiva fallet, och inte kan finna något fel, pga att man skrivit basfallet först med något trivialt fel. Flerraders-kommentarer skivs med {- -}.
Krånglig funktion: Definiera en hjälpfunktion! Om vikten av att definiera funktionerna för hela definitionsområdet: Hugs kollar inte att man definierat en funktion så att alla tänkbara argument "matchar" någon ekvation. Glömda fall resulterar i körfel. Till exempel : f :: Int -> Int Körning: Main> f 14 f 14 = 2 2 Bättre definiton: Main> f 3 Program error: {f 3} f :: Int -> Int f 14 = 2 f i = error ("f " ++ show i ++ " odefinierat") Det är ofta bra att som sista ekvation skriva ett sköns-fall, default-fall, som alltid "matchar", t ex när man håller på med legalmove för ADT Chessman: legalmove =...... legalmove = error "Inte implementerat än i legalmove" När man tycker man är klar kan man byta texten till "Inträffar aldrig i legalmove". Inträffar det i alla fall har man tänkt fel. När man t ex skriver chessquaretographics:: Square -> Chessman -> [Graphic] i steg tre är det viktigt att komma ihåg NoChessman. Hur skriver man en lista med ett enda element (kan vara basfall ibland)? Det finns två sätt: [1] eller 1 :[] är en [Int] med ett enda element med värdet 1. Båda sätten [x] eller (bättre tycker jag) (x :[]) fungerar också i möster ("patterns"). Funktionen concat i steg 6. I steg 6 får man lätt typen[[graphics]] (64 element i listan där elementen består av listor med bilden av en ruta och ibland också bilden av en schackpjäs). I stället vill man ha [Graphics]. Att "platta till" listan gör man med en funktion: concat concat [] :: [[a]] -> [a] = [] concat (x:xs) = x ++ concat xs -- finns förstås färdig i Prelude.
Innehållet i en ADT. Ex i steg 6. En förfrågan : Jag och min labbkompis håller på och kämpar med schacket och har kommit till steg 6, d.v.s vi håller på och implementerar funktionen chesstographics() och har stött på lite patrull. Frågan är hur man fiskar ur (Square, Chessman)-paren från Chess-typen Chess [(Square, Chessman)]? Vi har satsat på att, precis som du tipsar om i kompendiet, att mappa listan i Chess mot chessquaretographics() funktionen via en hjälpfunktion. Problemet är att vi lyckas inte få typerna att matcha. Detta är vår ansats till lösning, som vi tycker ligger närmast till hands: ----------------------------------------------------------- -- Creates a Graphic list for the chessboard. chesstographics :: Chess -> [Graphic] chesstographics chess = map helper chess helper :: (Square, Chessman) -> [Graphic] helper (sq, cman) = chessquaretographics sq cman ----------------------------------------------------------- Svar : Använd mönsterpassning (pattern matchning)! Lösning: chesstographics :: Chess -> [Graphic] chesstographics (Chess squarechessmans) = map helper squarechessmans Kommentar : I de flesta språk "plockar man sönder en ADT" med selektorer (motsatsen till konstruerare) sådana funktioner kan man skriva i Haskell också förstås. För den inbyggda typen listor finns head :: [a] -> a tail:: [a] -> [a] och för par finns fst :: (a,b) -> a snd : (a,b) -> b Det är ck enklare att använda mönsterpassning (pattern match), t ex (för listor [], (x:xs), för par(a,b) ). Detta gör vana Haskell-programmerare alltid i den modul som definierar ADT-en. Vill man använda mönsterpassning i andra moduler (och det vill man nog ofta), måste man exportera konstruerarna för den konkreta datatypen. Datatypen är då inte längre abstrakt tyvärr. Konstruerarna definieras ju så här: data <Nytyp> = <Konstuerare>.. <Konstuerare> Tre sätt att exportera konstruerarna: module <NytypDT> where -- allt exporteras, enklast module <NytypDT> (<här räknar man upp det som skall exporteras inkl <Nytyp> och konstuerare> ) where module <NytypDT> (<här räknar man upp det som skall exporteras inkl <Nytyp> (..)>) where
"uncurring" mm. Ex helper ovan steg 6. Funktionen helper (sq, cman) = chessquaretographics sq cman i teknologlösningen ovan behövs för att chessquaretographics har typen chessquaretographics :: Square -> Chessman -> [Graphic] men skulle kunna funktionen direkt som argument till map om den hade typen chessquaretographics:: (Square, Chessman) -> [Graphic] Finns det andra lösningar än att använda en hjälpfunktion? Jomenvisst : 1. Använder man listomfattning är det inga problem: chesstographics (Chess rl) = concat [ chessquaretographics square cm (square, cm) <- rl] 2. Skriva om chessquaretographics.(tar mindre än en minut.) 3. Vi kan definera helper = \(sq, cman) -> chessquaretographics sq cman Lamda-uttrycket kan då "smackas in direkt" i map-uttrycket (Tar ca 20 sekunder att göra.) chesstographics (Chess squarechessmans) = map ( \(sq, cman) -> chessquaretographics sq cman) squarechessmans 4. Det skulle vara bra att slippa göra sådana här förvandlingar varje gång problemet dyker upp. Hur löser man det? Med funktioner förstås: Två användbara funktioner (finns förstås i Prelude) (överkurs i fjol) : curry :: ((a,b) -> c) -> a -> b -> c curry f x y = f (x, y) uncurry:: (a -> b -> c) -> (a,b) -> c uncurry f (x,y) = f x y Vi kan nu göra sånt som Prelude> uncurry (+) (3, 4) 7 -- Om du tvilar på att det uncurry kan vara så enkel, förenkla detta uttryck! I vårt fall : (tar 5 sekunder om man känner till att uncurry finns i Prelude) chesstographics (Chess squarechessmans) = map (uncurry chessquaretographics) squarechessmans
Interaktiva program. Inför steg 8 och 9. Se först labb-häftet 30 <= sid <=33. Inför steg 8 och 9. Interaction main =... loop =... styrning (control) modell Chess Square Chessman startstatus, vy firstgraphs ChessGraphic i PlayChess Computational "Man ska inte gråta över spilld mjölk" "Tja, man blir inte yngre med åren " Lite mer om IO-programmering. Kernel ("Vanlig Haskell") Shell -> IO a "tiden går" ireversibilitet S > 0 "gjort är gjort och kan inte göras ogjort" World omvärlden När vi programmerade vårt interaktiva skal använde vi ADT IO b med dessa operationer/funktioner: (>>=) :: IO a -> (a -> IO b) -> IO b (>>) :: IO a -> IO b -> IO b -- ofta IO ()->IO () -> IO () return :: a -> IO a -- alla finns i Prelude för IO b (>>) är en variant av (>>=) och används när vi inte behöver det värde som kom ut ur åtgärden IO a. (>>) kan definieras som action1 >> action2 = action1 >>=(\_ -> action2) return används när man vill tillverka en åtgärd, till exempel en hjälpfunktion av typ IO a, där värde med typen a beräknas ur vanliga Haskell- värden. Omvärden är oförändrad efteråt. a a w1:world return w1:world
För att visa att IO-programmering är riktig Haskell används operatorerna, men normalt använder man -syntaxen, då man skriver åtgärder under varandra efter reserverade ordet på ungefär detta sätt: operator-syntax -sekvenser med "satser" e >>= \x -> s x <- e -- x:: a, e :: IO a s -- s oftast typen IO (), -- exvis s1 x e >> s e -- e oftast typen IO() s -- s oftast typen IO() let d in e let d -- d ofta vanlig Haskell e Ett exempel : I vårt Schack-program steg 8 och 9 föreslås hur IO skall programmeras med givna funktioner main och loop.all interaktion med omvärden i loop på dessa ställen (fet stil ); all användning av "vanlig Haskell "i loop, ddvs uttryck med annan typ än IO a, är kursiverat: loop :: Winw -> (Colour, Chess) -> [Graphic] -> IO () loop w (cl, chess) graphs = sequence_ (map (drawinwinw w) graphs) -- rita ändringar (1.a gången allt) (i1, j1) <- getlbp w let sq1 = tosquare (i1, j1) -- flera def efter let OK chessman1 = chessmanat sq1 chess if not (correctcolour cl chessman1) then loop w (cl, chess) (mess1 cl "Wrong chessman colour. ") else sequence_ (map (drawinwinw w) messs) -- rita ny info på skärmen (i2, j2) <- getlbp w -- klick 2 let sq2 = tosquare (i2, j2) -- målrutan chessman2 = chessmanat sq2 chess -- pjäs på målrutan if not... Har man börjat programmera med åtgärder, dvs värde med typ IO b, kan man alltså använda beräkningar i beräkningskärnan som vi gjort ovan, - dels efter let, - dels när vi beräknar argument, men man kan inte få ett slutresultat av annan typ än IO a, eftersom >>, >>= och return alla ger resultat av typen IO, dvs skalet skyddar beräkningskärnan. Om denna funktion unsafeperformio :: IO a -> a skulle finnas i ADT IO i Haskell 98 skulle vi kunna skriva funktioner med sieffekter, världen av typ Word skulle kunna påverka resultatet av typ a. För en imperativ programmerare känns det ovant att man inte kan programmera vanliga funktioner med sieffekter, men det är avsiktligt; det är nämligen detta förhållande som gör att vi inte förstör Haskells funktionella karaktär. (Men funktionen unsafeperformio finns i ett paket man kan importera, så man kan synda, men det skall man naturligtvis inte göra för det är farligt, farligt (unsafe)...).
Man kan programmera vårt exempel med en hjälpfunktion IO :: Winw -> Chess -> [Graphic] -> IO (Square, Chessman) så här : loop :: Winw -> (Colour, Chess) -> [Graphic] -> IO () loop w (cl, chess) graphs = (sq1, chessman1) <- IO w chess graphs if not (correctcolour cl chessman1) then loop w (cl, chess) (mess1 cl "Wrong chessman colour. ") else (sq2, chessman2) <- IO w chess messs if not... För att definiera IO behövs return : IO :: Winw -> Chess -> [Graphic] -> IO (Square, Chessman) IO w chess graphs = sequence_ (map (drawinwinw w) graphs) (i, j) <- getlbp w let sq = tosquare (i, j) chessman = chessmanat sq chess return (sq, chessman) -- :: IO (Square, Chessman) En -sekvensen har den typ som sista åtgärden, "satsen" har. Så fort en åtgärds-sekvens har minst två åtgärder krävs ett. Egentligen är en -sekvens ett uttryck, "satser" saknas alltså i funktionella språk. Paret (i, j) är en parametrar; -syntaxen ovan är en omskrivning av IO w chess graphs = sequence_ (map (drawinwinw w) graphs) >> getlbp w >>= \ (i, j) ->... -- resten av IO kan använda parametern (1,j) I praktiken programmerar man det interaktiva skalet på liknade sätt som i imperativa programmering. Om man använder hugs behövs ju överhuvud taget inget interaktiv skal så länge inte inmatning och utmatning är "inflätade" i varandra eller man behöver använda filer, ska rita, mm. Man försöker förstås göra skalet "tunt", eftersom det är så trevligt att programmera med vanlig Haskell i "beräkningskärnan", "the Computional Kernel". Men när man använder IO-programmering hävdar Pyton-Jones: "In short, Haskell is the world s finest imperative language" Hemligheten med IO-monad-programmering är precis som i vanliga Haskell att ha ordning på typerna. I programfragmentet nedan är den kursiverade texten vanlig Haskell, dvs uttryck som har andra typer än IO b, dvs vi lämnar "skalet" och använder oss av "beräkningskärnan". Typerna för "satserna" i sekvenserna är kommenterade.
loop :: Winw -> (Colour, Chess) -> [Graphic] -> IO () loop w (cl, chess) graphs = (sq1, chessman1) <- IO w chess graphs -- (sq1, chessman1) :: (Square,Chessman) -- IO w chess graphs :: IO (Square,Chessman) if not (correctcolour cl chessman1) -- IO () hela if-uttrycket then loop w (cl, chess) (mess1 cl "Wrong..") -- IO () uttrycket efter then else -- IO () uttrycket efter else (sq2, chessman2) <- IO w chess messs -- (sq1, chessman1) :: (Square,Chessman) -- IO w chess graphs :: IO (Square,Chessman) if not... -- IO () hela if-uttrycket then loop... -- IO() uttrycket efter then else... let newcolor = othercolor cl... in loop.. -- IO () uttrycket efter else Varför värden beter sig korrekt i ADT IO b. enda sättet att kombinera åtgärder, världen förändras stegvis, som den ska Haskell och andra språk. Haskell hade troligen varit ett mer använt språk om det tidigt funnits standard för samverkan med kod skrivit i andra språk. Standard för detta börjar komma först nu. Man kan kanske tycka att språket är så bra att man inte behöver beblanda sig med underlägsna imperativa språk, men de finns personer som anser en sådan inställning orealistisk. Eftersom kod skrivna i andra språk ofta har sieffekter bör Haskell betrakta de flesta importerade funktioner i skrivna i "främmande" språk som funktioner med resultat av typ IO (). Kompilering och interpretering. Hugs är en interpretator, och i början skrev vi uttryck i hugs som hugs då beräknade. Från steg 3 i schacklabben har vi ibland skrivit funktioner av typen IO (), som vi döpt till main. I dessa funktioner har vi kunnat växelverka med omvärden. I definitionen av Haskell står att det alltid skall finnas en funktion main :: IO(), dvs main :: World -> ((), World). Programmet startar med att beräkna main och tar alltså hänsyn till världen och påverkar värden. Hugs klar ju detta, men dessutom kan vi få hugs att beräkna uttryck av godtycklig typ. På liknade sätt som Blue J kan använda java på ett mer avancerat sätt än en java-kompilator, kan hugs mer än en Haskell-kompilator. Den mest använda kompilatorn för Haskell heter ghc och program för ghc skall ha en funktion main :: IO()som startar körningen.