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ő:
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:
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:
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
jó: 11 — 13
jeles: 14 — 16
|