A feladat összefoglaló leírása

Ebben a feladatban a Logo nyelvből ismert teknőcgrafika egy egyszerűsített változatát kell megvalósítani. Ezt úgy érjük el, hogy a nyelvben leírható programokat egy Haskell adattípusként definiáljuk, majd a programozási szerkezeteket, utasításokat részben Haskellbeli függvényekkel, részben pedig saját kombinátorainkkal adjuk meg. Ezeket a programokat aztán a szerkezetekhez társított szemantikán keresztül képesek leszünk futtatni, vonalsorozatokként értelmezni és ezeket SVG-ábrákká alakítani.

A teknőc állapotának ábrázolása (1 pont)

A teknőcgrafikából lényegében egy teknőccel mozgunk, amelyik a beállításainak megfelelően adott vastagságú vonalat húz. A mozgáshoz nyilván kell tartanunk, hogy a vásznon pontosan melyik koordinátában tartozkodunk, illetve a teknőc milyen irányba néz. Továbbá a vonalhúzást ki is tudjuk kapcsolni, ezért annak állapotát is meg kell jegyeznünk.

A teknőc állapotának leírásához definiáljuk következő típusokat:

Point2D :: *  -- kétdimenziós pont, egész értékű x és y koordinátákkal
Angle   :: *  -- szög, fokban, lebegőpontos szám
Width   :: *  -- vonalvastagság, egész szám
Turtle  :: *  -- a teknőc állapota, az előbbiek felhasználásával alkotott
              -- rendezett n-es

Ezen típusok felhasználásával képesnek kell lennünk definiálni a teknőc kezdőállapotát, amely legyen most a következő:

initState :: Turtle
initState = ((0,0), 0, 1, True)

Ez azt jelenti, hogy a teknőcünk kezdetben az origóban áll, felfele néz, vonalat húz, egyes vastagsággal.

Egy teknőcprogram ábrázolása (2 pont)

Adjuk meg a teknőcprogramokat leíró típust, amely legyen a következő:

Program :: *

A típushoz pedig ezek a adatkonstruktorok tartozzanak:

Empty   :: Program                        -- üres program
Advance :: Int -> Program                 -- n pontnyi haladás előre
Turn    :: Double -> Program              -- n foknyi fordulás balra
Pen     :: Bool -> Program                -- húzunk-e vonalat
Width   :: Int -> Program                 -- vonalvastagság beállítása
Seq     :: Program -> Program -> Program  -- szekvenciális kompozíció

A teknőcprogramok kombinátorai (3 pont)

A teknőcprogramok típusának megadásával most vezessük be a programok építéséhez használható kombinátorokat. Ezek a következők:

forward  :: Int -> Program                -- n pontnyi lépés előre (irány nem változik)
backward :: Int -> Program                -- n pontnyi lépés hátra (irány nem változik)
left     :: Double -> Program             -- n foknyi fordulás balra (pozíció nem változik)
right    :: Double -> Program             -- n foknyi fordulás jobbra (pozíció nem változik)
width    :: Int -> Program                -- vonalvastagság beállítása
penup    :: Program                       -- vonalhúzás kikapcsolása
pendown  :: Program                       -- vonalhúzás bekapcsolása
skip     :: Program                       -- üres program
times    :: Int -> Program -> Program     -- program ismétlése n-szer

infixr 4 >->
(>->) :: Program -> Program -> Program    -- szekvenciális kompozíció

Például a következő programokat le kell tudnunk írni a kombinátorok segítségével:

triangle :: Int -> Program
triangle s =
  forward s  >->
  right 120  >->
  forward s  >->
  left 60    >->
  backward s

dashedLine :: Int -> Int -> Int -> Program
dashedLine n s w = times n $
  width w   >->
  pendown   >->
  forward s >->
  penup     >->
  forward s

spiral :: Int -> Angle -> Program
spiral n d =
  if (n > 100)
    then skip
    else
      forward n         >->
      right d           >->
      spiral (n + 2) d

Ügyeljünk arra, hogy a kombinátorok nem minden esetben képezhetőek le közvetlenül az egyes adatkonstruktorokra! Ekkor igyekezzünk ezeket úgy megadni, hogy azokkal kifejezhetőek legyenek.

Vonalsorozatok ábrázolása (1 pont)

A teknőcprogramokat vonalsorozatokra képezzük le, ezzel adjuk meg a jelentésüket, illetve ezáltal válik láthatóvá az eredményük. Ehhez most elsőként meg kell a vonalsorozatokat, valamint azokon belül a vonalakat ábrázoló típust:

Line    :: *
Drawing :: *

Egy vonalat a kezdő- és végpontjával (Point2D) és a vastagságával (Width) adunk meg. A vonalsorozatokat leíró Drawing típus pedig nem más, mint vonalak listája.

A teknőcprogramok szemantikája (4 pont)

A vonalsorozatok előállításához először le kell írnunk, hogy a programokat ábrázoló típus, amelyet tulajdonképpen egy absztrakt szintaxisfaként viselkedik, elemeit miként értelmezzük. Ezt a szintaxisfa (rekurzív) bejárásával tudjuk megtenni egy State és egy Writer monád kompozíciójában. Ezt a következő típussal írjuk le:

unProgram :: (MonadState Turtle m, MonadWriter Drawing m) => Program -> m ()

Itt a State szerepe az lesz, hogy a teknőc állapotát kezelje (le tudjuk kérdezni, illetve megváltoztatni a feldolgozás során), a Writer monádot pedig arra használjuk, hogy összegyűjtsük benne az eredményként keletkező sorozatot.

A koordináták kezelése során feltételezzük, hogy a képernyő koordinátarendszerét használjuk, ahol az origó tulajdonképpen a képernyő bal felső sarka, és az x tengely a képernyő jobb felső, az y tengely pedig a képernyő bal alsó sarka felé mutat.

(A fordításhoz engedélyeznünk kell a FlexibleContexts kiterjesztést.)

Teknőcprogramok futtatása (1 pont)

Az előbbi, unProgram függvényünket úgy tudjuk használni, ha kiegészítjük még egy másikkal, amelyik az implementációban alkalmazott State és Writer monádok hatását képes lefuttatni. Ha a State futtatásához alkalmazzuk a teknőc kezdőállapotát, akkor lényegében azt a (látszólag mellékhatásoktól mentes) függvényt kapjuk, amelyik egy teknőcprogramot egy vonalsorozat képez.

Így ennek a típusa az alábbi:

runProgram :: Program -> Drawing

Például a következőképpen működik:

runProgram skip == []
runProgram (triangle 10) == [((0,0),(10,0),1),((10,0),(5,9),1),((5,9),(0,0),1)]
runProgram (dashedLine 3 5 5) == [((0,0),(5,0),5),((10,0),(15,0),5),((20,0),(25,0),5)]
runProgram (spiral 95 95) == [((0,0),(95,0),1),((95,0),(87,97),1),((87,97),(-10,80),1)]

SVG-ábrák ábrázolása (1 pont)

A programokból kiszámított vonalsorozatokat szeretnénk még SVG-ábrák formájában megjeleníthetővé tenni. Ezért készítsünk egy olyan típust, amelyik egy String értékként (annak forráskódjaként) ábrázolt SVG-objektumot csomagol be:

SVG :: *

Vigyázzunk arra, hogy csomagolásról van szó: vagyis kívülről ne legyen látszódjon, hogy ez valójában nem több, mint egy String érték! Ezért mind a csomagoláshoz, mind pedig a benne levő String érték kinyeréséhez készítsünk külön függvényeket:

SVG         :: String -> SVG  -- adatkonstruktor
svgToString :: SVG -> String

Ennek megfelelően a következőnek teljesülnie kell:

svgToString . SVG :: String -> String

A vonalsorozatok megjelenítésének absztrakt leírása (1 pont)

Készítsünk egy absztrakt szignatúrát, amely a vonalsorozatok leképezését írja le minden olyan típusra, amely a RenderTarget osztály tagja! Mivel ilyen osztályunk még nem létezik, ezért értelemszerűen az absztrakt szignatúrát ennek az osztálynak a megadásával tudjuk létrehozni.

render :: RenderTarget a => Drawing -> a

Vonalsorozatok megjelenítése SVG-ábraként (2 pont)

Tegyük lehetővé, hogy a korábban definiált SVG típus esetében működjön a render függvény! Ekkor tulajdonképpen azt kell megadnunk, hogy az SVG típus miként lesz a korábban szintén említett RenderTarget osztály tagja. Ez magával vonja azt is, hogy leírjuk, a vonalsorozatokból miként lesz SVG-ábra.

Ehhez fel tudjuk használni azt, hogy az SVG-objektumok általános alakja a következő:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
  <!-- a rajz vektoros leírása -->
</svg>

vonalakat pedig a következő SVG-elemmel tudunk rajzoltatni (feltételezzük, hogy a vonalak színe most mindig azonos, vagyis mindig fekete):

<line x1="&p1_x;" y1="&p1_y;" x2="&p2_x;" y2="&p2_y;" stroke="black" stroke-width="&width;"/>

ahol pN_x és pN_y rendre a kezdő- és végpontok megfelelő koordinátáit jelentik, illetve a width érték a vonal vastagságát. Fontos, hogy a line címke minden attribútumát idézőjelek közé kell tenni.

Az alkalmazására néhány példa:

(svgToString $ render []) ==
  "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\"></svg>"
(svgToString $ render [((0,0),(0,1),1), ((0,1),(3,4),2)]) == concat
  [ "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">"
  , "<line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\" stroke=\"black\" stroke-width=\"1\"/>"
  , "<line x1=\"0\" y1=\"1\" x2=\"3\" y2=\"4\" stroke=\"black\" stroke-width=\"2\"/>"
  , "</svg>"
  ]
(svgToString $ render $ runProgram skip) ==
  "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\"></svg>"
(svgToString $ render $ runProgram $ triangle 10) == concat
  [ "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">"
  , "<line x1=\"0\" y1=\"0\" x2=\"10\" y2=\"0\" stroke=\"black\" stroke-width=\"1\"/>"
  , "<line x1=\"10\" y1=\"0\" x2=\"5\" y2=\"9\" stroke=\"black\" stroke-width=\"1\"/>"
  , "<line x1=\"5\" y1=\"9\" x2=\"0\" y2=\"0\" stroke=\"black\" stroke-width=\"1\"/>"
  , "</svg>"
  ]
(svgToString $ render $ runProgram $ dashedLine 3 5 5) == concat
  [ "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">"
  , "<line x1=\"0\" y1=\"0\" x2=\"5\" y2=\"0\" stroke=\"black\" stroke-width=\"5\"/>"
  , "<line x1=\"10\" y1=\"0\" x2=\"15\" y2=\"0\" stroke=\"black\" stroke-width=\"5\"/>"
  , "<line x1=\"20\" y1=\"0\" x2=\"25\" y2=\"0\" stroke=\"black\" stroke-width=\"5\"/>"
  , "</svg>"
  ]

Magukat az eredményeket, ha a writeFile IO-akció segítségével egy állományba íratjuk, akkor közvetlenül egy böngészővel, például a Firefoxszal is megnézhetjük. Például:

writeFile "test.svg" $ svgToString $ render $ runProgram $ penup >-> right 45 >-> forward 100 >-> pendown >-> triangle 50

Pontozás

elégtelen: 0 — 4
elégséges: 5 — 7
közepes: 8 — 10
: 11 — 13
jeles: 14 — 16