3. Programmering med textfiler

Aarne Ranta
Datorintroduktion 2009, D och DV, Chalmers & GU

En ny titt på kurswebbsidan

Öppna den här länken

Vad har hänt?

Och vad har hänt här?

Strängoperationer på en fil

Det enklaste sättet att tillämpa en strängfunktion, t.ex. reverse på en fil:

1. Skriv filen Translate.hs med följande innehåll:

    module Main where
  
    main = interact reverse

2. Kör programmet i en Unix-pipe:

    unix$ cat myfile | runghc Translate

In- och utmatning

IO, programmets kommunikation med det övriga datorsystemet.

Exempel:

Typen av main är en IO-typ:

    main :: IO ()

I exakta termer: IO-aktion som returnerar värde av typ ()

(Typen () är Haskells motsvarighet till void i C och Java.)

Preludfunktionen interact

På den här kursen behöver vi ingen annan IO-funktion än

    interact :: (String -> String) -> IO ()

Den här funktionen

  1. läser en sträng från input
  2. applicerar en strängfunktion
  3. skriver resultatet till output

Strängfunktionen kan vara vilken som helst av typen String -> String, t.ex. alla de som definieras i Föreläsning 2.

Hur man skickar indata

Från en fil, med unix-kommandot cat

    unix$ cat myfile.txt | runghc Translate.hs

Som en sträng, med unix-kommandot echo

    unix$ echo "hello world" | runghc Translate.hs

I båda fallen använder man en pipe |, som (som vid det här laget bekant) betyder:

Om ingen pipe följer skrivs resultatet på skärmen.

Lite upprepning av unix-kommandon

echo STRING: skriv ut STRING

cat FILE: skriv innehållet av FILE

grep STRING FILE: skriv ut de rader i FILE som innehåller STRING

wc FILE: skriv ut antalet rader, ord och tecken i FILE

I stället för FILE, kan man i dessa kommandon använda

Exempel: räkna på hur många rader ordet "Haskell" förekommer på den här kursens hittills 3 föreläsningar

    unix$ cat dator-01.txt dator-02.txt dator-03.txt | grep "Haskell" | wc
    22     146    1071

Strömmarna stdin och stdout

Ett (typiskt) unixprogram är ett filter som förvandlar input till output.

Input och output är teckenströmmar, och kan i Haskell ses som listor av tecken. De är, konceptuellt, oändliga.

stdin = standard input, stdout = standard input

För att skriva ett eget program som passar in i den här modellen, är det tillräckligt att skriva en Haskell-fil, säg Translate.hs, i formatet

    module Main where
  
    main = interact translate

(med en egen translate-funktion) och sedan exekvera programmet i Unix:

    unix% runghc Translate.hs

Nu är detta anrop av runghc precis ett filter i Unix-bemärkelsen:

Transformera en fil

Vi ska börja med att använda kommandot

    unix% cat ett.txt | runghc Translate.hs

där ett.txt är det första avsnitten från första kapitlet av August Strindbergs Röda rummet (1879).

Hela boken finns att hämta från Projekt Runeberg.

Vi strukturerar filen så att vi lätt kan definiera nya översättningsfunktioner:

    module Main
  
    main :: IO ()
    main = interact translate
  
    translate :: String -> String
    -- translate = reverse
    translate = initials
  
    initials :: String -> String
    initials s = unwords [take 1 w | w <- words s]

En "sammanfattning" av filen

Första bokstaven ur varje ord:

  unix$ cat ett.txt | runghc Translate.hs
  F K S i f D v e a i b a m D l t p M h ä i b ö f a o r v e u s h a s u 
  g f l o h j p a s s k v f a l p å d ö s v t s u e o p s v p s v f a f 
  g i b m l b ä k i s o k å b s b b s l b m s o g ä h i m t s s s v s g 
  b o d l e o l d a b d o b G h p a s u s s d s g u t p n h d d o s a r 
  f s h d p h f u t s å f s u s p R - o a s d D h b i b o k m s p e b d 
  f h e h s i s d s J i f D v e l o e k

Andra filtransformationer

Prova också, i tur och ordning: reverse, take 2, reverse på varje ord separat, ta bort mellanslag,...

    module Main
  
    main :: IO ()
    main = interact translate
  
    translate :: String -> String
    -- translate = initials
    translate = reverse
    -- translate s = unwords [take 2 w | w <- words s]
    -- translate s = unwords [reverse w | w <- words s]
    -- translate s = [c | c <- s, c /= ' ']

Vi gör detta genom att kommentera bort alla definitioner av translate förutom en i taget.

Högre ordningens funktioner

En högre ordningens funktion är en funktion som tar en funktion som argument.

Vårt första exempel har varit:

    interact :: (String -> String) -> IO ()

Högre ordningens funktioner är en specialitet av Haskell och andra funtionella språk; de är knappast möjliga i Java, och endast med en viss möda i C och C++.

Även i skolmatematik har man högre ordningens funktioner, t.ex. derivatan:

D : (R -> R) -> (R -> R) (funktionen måste vara deriverbar)

Generalisering från ord-för-ord översättning

Vi har skrivit flera gånger uttryck i samma format:

    unwords [reverse w | w <- words s]
    unwords [take 2  w | w <- words s]
    unwords [replace w | w <- words s]

Kan vi inte generalisera från detta? Jo - med en högre ordningens funktion:

    byWords :: (String -> String) -> String -> String
    byWords f s = unwords [f w | w <- words s]

Nu kan vi skriva samma uttryck så här

    byWords reverse
    byWords (take 2)
    byWords replace

Rad-för-rad översättning

Föga oväntat:

    byLines :: (String -> String) -> String -> String
    byLines f s = unlines [f w | w <- lines s]

Jämför följande:

    main = interact translate
  
    -- translate = reverse
    -- translate = byWords reverse
    -- translate = byLines reverse

Vad händer i de olika fallen?

Översättning av ett HTML-dokument

HTML = HyperText Markup Language

Ett HTML-dokument består av text och taggar. Taggarna ger länkar till andra dokument och säger hur texten ska se ut. Taggarna är allt innehåll mellan < och >; allt annat är text. Exempel:

    <b>Obs</b>

har taggarna <b> och </b>. Taggarna ger fetstil ("boldface") till ordet "Obs", vilket syns i webbläsaren som Obs.

När vi översätter ett HTML-dokument vill vi oftast lämna taggarna som de är, för att bevara utseendet och länkarna. Vi kan göra detta med funktionen

    inHTML :: (String -> String) -> String -> String

som definieras i Translate.hs. Till exempel:

    translate = inHTML (byWords reverse)

har skapat den här filen.

Output till en fil

För att se den nya HTML-filen på en webbläsare, måste vi spara den i en fil. Unix erbjuder operationen omdirektion (>) för detta:

    unix% COMMAND > FILE

skickar output från COMMAND till FILE, i stället för skärmen (eller ett annat kommando).

Vi använder detta för att spara översättningsresultatet:

    unix% cat dator-01.html | runghc Translate.hs > new-dator-01.html

Nu kan vi öppna den nya filen i webbläsaren.

Obs. Skriv inte till samma fil! Varför? Kopiera någon fil till file och kör

    unix% cat file >file

Uppgifter

Alla uppgifterna ska implementeras som variationer av translate i Translate.hs.

1. Kör några egna översättningar på några egna filer.

2. Definiera en översättning som upprepar den första bokstaven i varje ord i ett HTML-dokument. Texten Kör några egna översättningar blir således KKör nnågra eegna ööversättningar. HTML-taggarna ska bevaras som de är. Redovisning: kör kommandot på denna föreläsning och visa på en webbläsare.

3. Definiera en översättning som gör samma som unix-kommandot wc: returnerar antalet rader, ord och tecken. Jämför resultatet med det som unix-kommandot ger. Obs. gör detta med Preludfunktionerna även om det blir långsamt. Redovisning: kör kommandot på ett.txt.

Sammanfattning och referens

Unix-kommandon

echo STRING: skriv ut STRING

cat FILE: skriv innehållet av FILE

grep STRING FILE: skriv ut de rader i FILE som innehåller STRING

wc FILE: skriv ut antalet rader, ord och tecken i FILE

man COMMAND: visa manualsidan av COMMAND

runghc FILE: kör main-funktionen i Haskell-filen FILE

COMMAND1 | COMMAND2: skicka output från COMMAND1 till input av COMMAND2

COMMAND > FILE: skicka output från COMMAND till FILE

Prelude-funktioner

    main :: IO ()
    -- måste definieras i varje Main-module
  
    interact :: (String -> String) -> IO ()