Akciók

Bevezetés

Tiszta funkcionális nyelv

Hivatkozási helyfüggetlenség: kifejezés helyettesíthető az értékével

áttekinthető kód, optimalizáció, programhelyesség bizonyítás

Kihívások

Megoldás: akciók

A kifejezések egy része akció. Ez a típus alapján eldönthető.

Két szint a program futásában:

Igaz marad a hivatkozási helyfüggetlenség:
Ha y = exp akkor y és exp bárhol felcserélhető és tetszőleges kifejezés kiemelhető külön definícióba.


Tehát a Haskellben végezhetők mellékhatásos számítások, ráadásul a nyelv szintaxisa támogatja az imperatív programozási stílust, ezért a Haskell egy multiparadigmás programozási nyelvnek tekinthető. A Haskell mégis inkább funkcionális, mert az imperatív nyelvi elemek vannak beágyazva a funkcionális nyelvi elemek közé, és nem fordítva.

A Haskell élesen elválasztja az akciókat a kifejezésektől (statikus típusozás szinten) és a kiértékelést a végrehajtástól, leginkább ez különbözteti meg az ML családtól (a LISP család még messzebb áll).

Az akciók típusa

Az akciók típusa IO a ahol a az akció végrehajtásakor kapott érték típusa.

Szöveg beolvasása Enter-ig:

Test>

Akciókat visszaadó függvények

Test>
Test>
Test>

Ha az akciónak csak a mellékhatása a lényeges, akkor nullást ad vissza.

Legyen

w = putStrLn "x.txt"

w az az akció, amit bármikor ha végrehajtunk, az "x.txt" szöveg kikerül a képernyőre.


putStrLn "x.txt" kiértékelése megáll ott amikor a putStrLn függvény visszaadja az adott akciót.

Az akciók típusában minden esetben fel kell tüntetni hogy az akció mit eredményez a végrehajtás során. Ha egy akció eredménye nem lényeges, akkor ezt úgy szokás jelezni, hogy az akció nullást ad vissza.

A nullás definíciója (a speciális szintaxis miatt ez csak elvi definíció):

Akciók az értelmezőben

  1. Az értelmező kiértékeli a kifejezést
  2. Ha a kifejezés akció, akkor végrehajtja, egyébként szövegként kiírja
  3. Ha az akció nem nullást ad vissza, akkor a végeredményt szövegként kiírja.

A do kifejezés

A do kifejezés lehetővé teszi az akciók komponálását.

A do kifejezés tördelésérzékeny.

Példa: Szimpla kompozíció

a1 = do
      putStrLn "hello"
      putStrLn "hello"

vagy

Test>

Példa: Értékátadó kompozíció

a2 = do
      x <- getLine
      putStrLn x

vagy

Test>

Példa: Visszatérési érték módosítás

a3 = do
      x <- getLine
      return ("hello " ++ x)

vagy

Test>

do kifejezés összefoglaló


A do utolsó sorában nem definiálhatunk változót.

A return függvény

A return egy függvény:

Test>

Akciók tesztelése a jegyzetben

x ~~ return a igaz ha x nem csinál mást csak visszaadja a-t.
x ~~ error s igaz ha x megáll s hibaüzenettel.
x ~~ s? t igaz ha x kiírja s-et és utána t igaz.
x ~~ s! t igaz ha x inputot vár és s beírása után t igaz.

(?) és (!) jobbra köt.

Feladat: Sor kétszer visszaírása

Definiáljunk egy akciót ami beolvas egy sort, majd kétszer visszaírja a képernyőre:

echoTwice :: IO ()
echoTwice = do
    line <- getLine
    putStrLn line
    putStrLn line

Feladat: Két karakterből az első visszaírása

Definiáljunk egy akciót ami beolvas két karaktert, és visszaadja az elsőt.

firstChar :: IO Char
firstChar = do
    c <- getChar
    _ <- getChar
    return c

Feladat: Két sor párban visszaadása

Definiáljunk egy akciót beolvas két sort majd visszaadja azokat egy párban.

read2Lines :: IO (String, String)
read2Lines = do
    l1 <- getLine
    l2 <- getLine
    return (l1, l2)

Feladat: Control.Monad.join

Definiáljuk a következő függvényt!

join :: IO (IO a) -> IO a
join x = do
   y <- x
   y

Feladat: Prelude.print

Definiáljuk újra a Prelude-beli print függvényt!

print :: Show a => a -> IO ()
print = putStrLn . show

Akciók az értelmezőben (másképp)

Az értelmező parancssorába beírjuk az x kifejezést.

Az akciók mint kifejezések

Az akciók teljes jogú kifejezések.

Tárolhatók adatként:

Test>

Átadhatók függvényeknek:

Test>
Test>
x= -1

Az if kifejezés egy speciális szintaxisú függvénynek tekinthető:

Feladat: Sor visszaírása

Definiáljunk egy akciót ami beolvas egy sort. Ha a sor üres volt, írja ki hogy “Üres sor.”, egyébként pedig írja ki a beolvasott sort.

echo :: IO ()
echo = do
    line <- getLine
    if null line
        then putStrLn "Üres sor."
        else putStrLn line

Feladat: Sor visszaírása amíg nem üres

Definiáljunk egy akciót ami addig írja vissza a beolvasott sorokat, amíg üres sort nem kap, egyébként kiírja hogy “Üres sor.”.

echoUntilEmptyLine :: IO ()
echoUntilEmptyLine = do
    line <- getLine
    if null line
        then putStrLn "Üres sor."
        else do
            putStrLn line
            echoUntilEmptyLine
Test>

A do kifejezések egymásba ágyazhatók (mivel teljes jogú kifejezések).

Vezérlési szerkezetek

A sequence függvény, ami akciókat hajt végre az eredmények összegyűjtésével:

Test>

Variáns:

Test>

A vezérlési szerkezetek a Haskellben függvények.

A sequence_ nem halmozza fel az összegyűlő () értékeket, ami hosszú lista esetén space leak-hez vezetne.

Feladat: N sor beolvasása

Definiáljunk egy akciót ami beolvas egy n egész számot, majd n darab sort, és visszaadja a sorokat egy listában.

readLines :: IO [String]
readLines = do
    n <- readLn
    sequence (replicate n getLine)

Feladat: Prelude.mapM

Definiáljuk újra a mapM vezérlési szerkezetet!

mapM :: (a -> IO b) -> [a] -> IO [b]
mapM f = sequence . map f

Feladat: Prelude.mapM_

Definiáljuk újra a mapM_ vezérlési szerkezetet!

mapM_ :: (a -> IO b) -> [a] -> IO ()
mapM_ f = sequence_ . map f

Feladat: Prelude.putStr

Definiáljuk újra a putStr függvényt a putChar segítségével!

putStr :: String -> IO ()
putStr = mapM_ putChar

Feladat: Prelude.sequence

Definiáljuk újra a Prelude-ben definiált sequence függvényt!

sequence :: [IO a] -> IO [a]
sequence [] = return []
sequence (h:t) = do
    a <- h
    as <- sequence t
    return (a:as)

Segítség: Használjunk mintaillesztést a kapott listára, majd a nem triviális esetben használjunk rekurziót.

Fájlkezelés

Fájl beolvasása:

Test>
-- type FilePath = String

Egyéb műveletek:

Test>
Test>

Tiszta funkcionális stílus

Egy Haskell program íródhat imperatív stílusban:

upperCase :: IO ()
upperCase = do
  b <- isEOF      -- System.IO modulban
  if b then return ()
       else do
          c <- getChar
          putChar (toUpper c)
          upperCase

De minél kevesebb az akció, annál inkább előtérbe kerülnek a tiszta funkcionális programozás előnyei.

Folytatás

Megoldás lusta I/O-val:

upperCase' :: IO ()
upperCase' = do
  cs <- getContents
  putStr (map toUpper cs)
Test>

A lusta I/O-nak megvannak az előnyei és a hátrányai. Ha több kontrollt szeretnénk (például fájlcserélőhöz), de magas szintű eszközzel, iterátorok javasoltak.

Folytatás

Legszebb:

upperCase'' :: IO ()
upperCase'' = interact (map toUpper)

interact a Prelude-ben definiált:

Test>

Így az egész program tiszta lesz és az összes I/O részletet az interact környezet tartalmazza.

Feladat: Definiáljuk újra az interact függvényt!

Feladat: Sorok beszámozása

Definiáljunk egy egysoros akciót, ami az input sorait beszámozza! Használjuk az interact, lines, unlines, zipWith függvényeket.

numberLines :: IO ()
numberLines = interact (unlines . zipWith (++) (map ((++ ". ") . show) [1..]) . lines)