Intro Eftersom Haskell är ett funktionellt språk utan sido effekter så kan man argumentera om IO borde vara möjligt alls, men om vi struntar i det så länge så återstår det fortfarande ett stort problem: Haskell är ett rent och väldigt lat språk! Detta innebär att ett funktionsanrop alltid kommer returnera samma värde för ett visst set med parametrar. Så om vi t.ex. skulle försöka anropa getchar flera gånger med en viss parameter så kommer vi alltid få tillbaka samma karaktär. Och även om vi skulle haft dynamiska returvärden så finns det ingen garanti att vi skulle få tillbaka dem i rätt ordning. Hur skall man då lösa dessa problem på ett sätt som innebär att man inte förstör Haskell på något vis och ändå kan kalla det för ett funktionellt språk utan sido effekter? Svaret kommer i formen av en monad. Vad är en monad? Enkelt uttryckt så är en monad en del av ett program där allt man gör behandlas utifrån ett regelvärk definierat av monaden ( My sandbox, my rules ). I fallet för IO så definierar Haskells IO monad nya regler och beteenden som gör det möjligt att kringgå faktumet att Haskell är rent, saknar sido effekter och ser även till att allting returneras i rätt ordning. För att få en förståelse hur detta går till så kan vi ta en titt på följande exempel: Vi definierar funktionen getchar och get2chars som följer: getchar :: Char get2chars = [getchar,getchar] Om vi skulle anropa get2chars i Haskell som vanligt så skulle Haskell, som det lata språket det är, använda samma returvärde för båda getchar anropen. Skulle det inte göra det så kan vi ändå inte veta i vilken ordning de skulle anropas. Denna definition är alltså inte tillräcklig! Vi försöker på nytt och omdefinierar funktionerna som: getchar :: Int > Char get2chars :: Int > String get2chars _ = [getchar 1, getchar 2] Vi ger funktionerna fake parametrar i form av heltal som vi inte bryr oss om då vi inte kommer använda dem, de gör ingen skillnad för oss helt enkelt. Detta innebär dock för Haskell att båda getchar funktionerna måste anropas eftersom de har olika parametrar. Dock så vet Haskell fortfarande inte exakt i vilken ordning som getchar skall anropas, vi skriver om getchar som: getchar :: Int > (Char, Int)
Vi introducerar ännu ett returvärde i form av ett heltal som vi inte bryr oss om, vi använder sedan detta värde och skickar in det som en parameter till nästa getchar anrop. På så vis måste Haskell utvärdera det första anropet först eftersom det andra anropet beror på ett returvärde från det första, ett anrop skulle nu alltså se ut såhär: get2chars _ = [a,b] where (a,i) = getchar 1 (b,_) = getchar i Vi är dock inte ute ur skogen än så att säga, Haskell kompilatorn är så pass smart att den inte kommer tro på oss då vi säger att fake parametern till get2chars är viktig eftersom vi inte bryr oss om den (se _ ). Detta leder till att Haskell också kommer bortse från fake parameterarna i getchar anropen och köra dem i den ordning den anser bäst. Vi löser detta genom att skicka get2chars fake parameter till getchar anropen, vilket leder till ett anrop som ser ut som följer: get2chars i0 = [a,b] where (a,i1) = getchar i0 (b,i2) = getchar i1 Vi har nu ett (fake)databeroende genom hela anropet som garanterar att alla anrop kommer köras i den ordning vi vill. Dock så lider get2chars av samma problem som getchar gjorde från början, dvs. att vi inte kan garantera att ordningen den anropas i är rätt. Om vi t.ex. skulle definiera en get4chars funktion som: get4chars = [get2chars 1, get2chars 2] Så skulle Haskell inte ha någon aning om i vilken ordning de skulle köras. Men detta löser vi på exakt samma vis som med getchars, vi lägger till ett extra returvärde som vi inte bryr oss om. Definitionen av get2chars blir då: get2chars :: Int > (String, Int) Frågan blir då vad get2chars skall returnera för heltal? Enkelt, vi tar det heltalet som det sista getchar anropet returnerar (som vi ändå inte bryr oss om). Det slutgiltiga anropet blir då: get2chars i0 = ([a,b], i2) where (a,i1) = getchar i0 (b,i2) = getchar i1 I verkligheten så passeras dock inte dessa heltal runt utan Haskell använder sig av ett objekt av typen RealWorld som den passar runt i alla IO funktioner. Alltså ser ovan skrivna exempel egentligen ut som följer: get2chars world0 = ([a,b], i2) where (a,world1) = getchar world0 (b,world2) = getchar world1
Alla IO funktioner tar alltså in ett objekt av typen RealWorld och returnerar sedan sitt beräknade värde och en möjligtvis förändrad RealWorld. Sedan så är Haskells main funktion definierad som: main :: RealWorld > ((), RealWorld) Vilket innebär att man redan där definierar det första RealWorld objektet som sedan kommer passeras vidare till alla IO funktioner. Och det enligt detta tankesätt som hela Haskells IO monad är uppbygd! Vi har nu definierat ett sätt att anropa funktioner på som ger dynamiska returvärden och garanterar att alla anrop sker i rätt ordning. Detta ger oss de verktyg som krävs för att vi skall kunna utföra IO och begränsar dem till en monad så att renheten inte förstörs i resten av programmet. Begränsningen till en monad innebär också att Haskell i sig fortfarande är helt funktionellt och saknar sido effekter, monaden räknas som ett slags specialfall (se imperativt sub språk ). Att utföra IO Notation IO sker i funktionen main main :: IO () När man kompilerar ett haskell program så är det main som blir programmets ingång. Flera IO operationer kan kombineras med hjälp av do notation main = do iofunc1 iofunc2 De kombinerade io funktionerna bildar då en ny IO operation. Det main returnerar, körs. Man kan referera till resultat från io operationer med hjälp av < notation: choice < getchar Notera att vi inte använder let, då vi vill lagra resultatet av att köra funktionen en gång snarare än att sätta ett namn på ett distinkt värde IO kontexten (>>=) :: IO a > (a > IO b) > IO b (>>) :: IO a > IO b > IO b Exempel: main = getline >>= putstrln main = (putstrln What is your name? ) >> getline >>= putstrln
getline och putstrln är ihopsträngade. Det är såhär haskell binder ihop IO actions under huven så att de körs i ordning (t.ex. do notation) Användbara funktioner Grundläggande för att prata med stdinout putchar :: Char :: IO () putstr :: String :: IO () putstrln :: String :: IO () getchar :: IO Char getline :: IO String Man referera till all standard input samtidigt med hjälp av getcontents :: IO String Att läsa från stdin, transformera och skriva ut på stdout är så vanligt att det finns en inbyggt funktion för det: interact :: (String > String) > IO () Man passar alltså in en transformerande funktion, och så tar interact om inläsning och output. Ex: main = interact (map toupper) Gissa vad programmet gör? Funktioner för filhantering openfile :: FilePath > IOMode > Handle hclose :: Handle > IO () hputstr :: Handle > String > IO () hgetline :: Handle > IO String hgetcontents :: Handle > IO String etc Shortcuts readfile :: FilePath > IO String writefile :: FilePath > String > IO () withfile :: FilePath > IOMode > (Handle > IO r) > IO r Fler funktioner finns i System.IO på hackage. Kolla även in monadiska operationer som form, mapm, sequence m.fl. i Control.Monad Separation av pure och impure
Eftersom all IO utgår från main, och alla funktioner som gör IO måste ha det i sin typdeklaration, så är det lätt att spåra vilka funktioner som är orena och vilka som är rena. Resultatet blir en naturlig separation mellan de rena och orena delarna av programmet. Exempelprogram formatering För att tydligare illustrera hur man utför IO i Haskell har vi här ett exempelprogram som tar in en textfil och formaterar om den. cat tomten.txt Midvinternattens köld är hård, stjärnorna gnistra och glimma. Alla sova i enslig gård djupt under midnattstimma. Månen vandrar sin tysta ban, snön lyser vit på fur och gran, snön lyser vit på taken. Endast tomten är vaken. Ovan har vi en textfil som vi vill läsa in i ett program och modifiera. Vi tycker inte att den är så fint formaterad utan vill gärna sätta in lite radbrytningar så man lättare kan se att det är en dikt det rör sig om. För att göra detta skriver vi följande program: readfile import System.IO main = do putstr "\ntext file to format and print: " hflush stdout fname < getline handle < openfile fname ReadMode contents < hgetcontents handle putstr ("\n\n" ++ newlines contents ++ "\n") hclose handle newlines :: [Char] > [Char] newlines [] = [] newlines [x] = [x] newlines (x:y:xs) (x:y:[] == ", " x:y:[] == ". " x:y:[] == "? " x:y:[] == "! ") = x : '\n' : (newlines xs) otherwise = x : (newlines (y:xs)) Vad gör då detta program kan man fråga sig? Jo, det frågar efter namnet på en textfil som det
senare öppnar för inläsning. Inläsningen sker genom att binda ett handle (en referens) till filströmmen med openfile. Därifrån plockas vi ut innehållet i vårt handle med hgetcontents. Därefter skriver vi ut texten efter att den har behandlats med vår funktion newlines. Resultatet av att applicera newlines på våran text är att vi för varje förekomst av interpunktion följt av ett mellanslag byter ut blanksteget mot en newline istället../readfile Text file to format and print: tomten.txt Midvinternattens köld är hård, stjärnorna gnistra och glimma. Alla sova i enslig gård djupt under midnattstimma. Månen vandrar sin tysta ban, snön lyser vit på fur och gran, snön lyser vit på taken. Endast tomten är vaken. Ovan: resulterande utskrift. Sen är det kanske inte mest praktiskt att bara skriva ut texten till stdout, eller för den delen läsa in från en fil angiven från stdin. Vi kan istället välja att dirigera om input och output antingen med något av nedanstående unix kommandon: eller cat tomten.txt./insertnewlines >> nyatomten.txt cat tomten.txt runhaskell insertnewlines.hs >> nyatomten.txt om vi skriver om programmet som nedan: insertnewlines Takes the content given and formats and prints it. Formatting is done by replacing the white space with a newline after a comma, period, question mark or exclamation mark import System.IO main = do contents < getcontents putstr ("\n" ++ newlines contents ++ "\n") newlines :: [Char] > [Char] newlines [] = [] newlines [x] = [x] newlines (x:y:xs)
(x:y:[] == ", " x:y:[] == ". " x:y:[] == "? " x:y:[] == "! ") (x:y:[] == ": " x:y:[] == "; ") = x : '\n' : (newlines xs) otherwise = x : (newlines (y:xs)) Ett mer kompakt alternativ till att använda openfile är withfile som har följande typsignatur: withfile :: FilePath > IOMode > (Handle > IO a) > IO a Med andra ord så tar den en sökväg till en fil, ett IOMode som är en parameter för hur filen ska öppnas (exempelvis ReadMode för att läsa filen, WriteMode för att skriva till filen et.c.) samt en funktion som tar ett handle och ger en IO handling som sedan utförs. Detta ger ett väldigt kompakt syntax och låter oss använda lambda funktioner. I det första givna programexemplet skulle vi genom att använda oss av withfile kunna byta ut följande rader kod: fname < getline handle < openfile fname ReadMode contents < hgetcontents handle putstr ("\n\n" ++ newlines contents ++ "\n") hclose handle mot detta lite tydligare exempel: fname < getline withfile fname ReadMode (\handle > do contents < hgetcontents handle putstr ( \n\n ++ newlines contents ++ \n ) Vilken version man föredrar är lite av en smakfråga, men båda finns att tillgå i språket. När saker går fel Exceptions I/O monaden kan lite slarvigt ses som ett imperativt underspråk till Haskell som tillåter oss att göra saker som vi annars inte får. Det man dock bör komma ihåg är att vi är normalt sett förbjudna att göra dessa saker i normal kontext av en anledning; de kan ge oväntade resultat, helt emot en av de funktionella språkens huvudprinciper. Väl är det att vi har metoder att hantera dessa problem i Haskell, mest klarspråkigt genom exceptions liknande de i objektorienterade språk såsom Java och C++. Dessa kan vi
använda oss av med diverse konstruktioner, utav vilka catch från modulen Control.Exception är den enklaste, men långt ifrån den enda. Nedan har vi ett kort exempel på syntax för catch: main = catch tryme catcher tryme :: IO () tryme = do someio catcher :: IOError > IO () catcher e = putstrln Här gick något snett! Typsignaturen för catch är som följer: catch :: IO a > (IOError > IO a) > IO a I exemplet ovan ser vi att catch tar två argument; det ena argumentet är en IO handling som kommer köras och det andra är en handler, en funktion som anropas med exceptionvärdet om ett sådant skulle uppstå i det första IO blocket. I detta exempel så skulle alla tänkbara exceptions att mönstermatchas till e, det vill säga oavsett vilket exception vi får ut ur tryme kommer vi få samma exceptionhantering, ett beteende som inte alltid är önskvärt. Vill man istället ha olika utskrifter för olika fel finns det andra funktioner än catch vi kan använda. Ett exempel på detta är catchjust, som har följande typsignatur: catchjust:: Exception e => (e > Maybe b) > IO a > (b > IO a) > IO a Skillnaden mellan de två olika funktionerna är att catchjust bara fångar vissa sorters exception. Vilka de är väljs med ett så kallat exception predikat, en funktion som ger de klausuler som ska uppfyllas för att just den handlern ska fånga vårat höjda exception. Nedan har vi ett exempel på ett exception predikat som en lambda funktion: catchjust (\e > if isdoesnotexisterrortype (ioegeterrortype e) then Just () else Nothing) (someio) (\_ > do hputstrln stderr ("Följande fil finns ej: " ++ show f) return "") Om exception predikatet i en catchjust inte matchar mot ett exception som höjs kan omslutande catchjust och catchblock fånga det och försöka sin egen mönstermatchning. Detta låter en ha övergripande catchblock för exceptions som ska hanteras likadant oavsett när i IO blocket de sker. Vill man då ha mer speciella beteenden för att hantera exceptions på enskilda ställen i koden kan man då bruka sig av specifika catchjust block.
För att vidare tydliggöra hur detta skulle fungera i verkligheten kan vi lägga till exception hantering i det tidigare givna kodexemplet för att läsa in en fil och manipulera innehållet. Vi vill särskilt hantera om användaren anger ett ogiltigt filnamn, men även fånga upp andra mer generella fel. Det skulle se ut på detta vis: insertnewlines Takes the content given and formats and prints it. Formatting is done by replacing the white space with a newline after a comma, period, question mark or exclamation mark import System.IO main = catch tryme catcher tryme :: IO () tryme = do fname < getline catchjust (\e > if isdoesnotexisterrortype (ioegeterrortype e) then Just () else Nothing) withfile fname ReadMode (\handle > do contents < hgetcontents handle putstr ( \n\n ++ newlines contents ++ \n )) (\_ > do hputstrln stderr ("Följande fil finns ej: " ++ show fname) return "") putstr ("\n" ++ newlines contents ++ "\n") catcher :: IOError > IO () catcher e = hputstrln stderr Filen fanns men något annat gick snett! newlines :: [Char] > [Char] newlines [] = [] newlines [x] = [x] newlines (x:y:xs) (x:y:[] == ", " x:y:[] == ". " x:y:[] == "? " x:y:[] == "! ") (x:y:[] == ": " x:y:[] == "; ") = x : '\n' : (newlines xs)
otherwise = x : (newlines (y:xs)) Om vi nu anropar programmet med ett felaktigt filnamn kommer vi få en särskilt anpassad utskrift för det fallet, medans andra exceptions hanteras generellt. Det anses god praxis att hantera specifika fel individuellt om man misstänker/vet att/var de kan uppstå och bara se generella catchers som en sista fallback. IORef När saker inte redan gick fel nog Vi använder oss som sagt av dessa exception hanterare för att IO kod inte är funktionellt ren. Ett av de mest grundläggande exempel på funktionell orenhet man skulle kunna tänka sig hade varit en variabel. I Haskell så har vi ju som tur är inga egentliga variabler, men vi har något liknande att tillgå genom språkets IO syntax, nämligen Data.IORef. Man kan se IORef som en variabel i ett annars variabellöst språk. Till skillnad från Haskells inbyggda uttryck är alltså typen IORef mutable, det vill säga vi kan ändra värdet på värdet lagrat under det namnet vi gett den. Om namnet är förvirrande hjälper det kanske att tänka sig IORef som en konstruktion att temporärt flytta data utanför programmets mer strikta uppbyggnad utan att behöva skriva till en extern resurs, exempelvis disk eller nätverksgränssnitt. Vi kommer åt den undangömda datan med funktioner snarlika de vi använder för att läsa från och skriva till andra IO objekt. Till exempel, för att skriva ett värde till, modifiera och sedan läsa från en IORef skulle man bruka följande syntax: writeioref myref Detta värde Kommer att försvinna writeioref myref Istället kommer detta värde ligga här myval < readioref myref myval kommer efter dessa handlingar innehålla den andra strängen. Lägg märke till att om man väljer att använda sig av IORef måste särskilda åtgärder vidtas för att göra ens applikation trådsäker. Dessa finns väl dokumenterade på annat håll och kommer ej tas upp här, då vi mest nämnt IORef som kuriosa, försök helst att finna lösningar ej innehållandes IORef då riskerna med att använda typen sällan bör anses vara värda mödan att hantera. Man får förstås dessutom bara använda sig av typen i main på grund av dess icke funktionella natur. Vi avrundar vår rapport om IO i Haskell med de rekommendationer vi kommit fram till under rapportens gång; använd oförutsägbar kod i låg utsträckning om möjligt, hantera gärna specifika fel och introducera inte osäkra konstruktioner i er kod i onödan!