Laboration 3 Grafiska figurer I den här laborationen skall du konstruera ett schemeprogram som kan rita rektanglar, punkter, cirklar, linjer och bilder som består utav en eller flera av nyss nämnda figurer. Den färdigskrivna delen av källkoden finns i filen /info/sudata10/laborationer/graph/graph.scm. OBS! Börja med att kopiera källkoden till din scheme-katalog (som du skapade i Laboration 1). En graf (egentligen en grafisk enhet) består av namnet på en figur och en form som talar om hur figuren skall se ut och var den skall placeras, t.ex grafen med namnet cirkel som skall ha origo i punkten (1,2) och radien 3. En möjlig schemedefinition av grafer (grafiska enheter) är ;; Make a new graph (define makegraph (lambda (name shape) (cons name shape))) ;; name of a graph (define name (lambda (graph) (car graph))) ;; shape of a graph (define shape (lambda (graph) (cdr graph))) För att konstruera ovannämnda cirkel kan man skriva, t.ex > (define my_circle (makegraph circle (list 1 2 3))) my_circle > my_circle (circle 1 2 3) Ett bättre sätt att konstruera en cirkel är att göra en procedur som tar koordinaterna för origo och radien som argument. Denna procedur kan i sin tur använda makegraph för att tillverka objektet. ;; make a circle (define makecircle (lambda (x0 y0 radius) (makegraph circle (list x0 y0 radius)))
2 Datalogi, grk 1 Laboration 3 En funktion som skapar ett objekt utgående från vissa indata kallas för en konstruktor (eftersom den konstruerar ett objekt). De flesta konstruktorer är lika enkla som makecircle ovan, men en del gör mer bearbetning med indata. Uppgift 1 Syfte: att förstå hur listor kan användas för att skapa nya datatyper att förstå hur man kan använda konstruktorer för att skapa nya datatyper Skriv en procedur (makepoint x0 y0) som använder makegraph. Resultatet av anropet (makepoint 1 2) skall bli (point 1 2). För att ta reda på t.ex. x-koordinaten i en punkt (point) så skulle man kunna ta ut den med hjälp av (car (cdr (makepoint 10 20))) som ger 10. Uttrycket (car (cdr...)()) brukar vanligtvis förkortas till (cadr...). Se SCHEME-häftet sid 26 för mer information om hur car och cdr kan kombineras. Det är dock mer flexibelt (och snyggare) att använda en funktion för att extrahera denna data från punkten. En sådan funktion kallas selektor. Uppgift 2 Syfte: att förstå hur en enkel selektor fungerar Skriv en funktion (X_coord point) som returnerar x-koordinaten för parametern point. Anropet (X_coord (makepoint 10 12)) ska ge 10 som resultat. Skriv en funktion Y_coord som ska returnera y-koordinaten för parametern point. Börja med att läsa in källkodsfilen du kopierade till din scheme-katalog i Emacs. Skriv sedan in makegraph, name och shape ovan i samma buffert. OBS! allt som sägs om utritning är frivilligt. Innan du kan rita ut något måste du först starta grafiken. Kolla på sidan 6 för att se hur man gör. När du sedan skrivit konstruktorer och selektorer för en punkt så kan du pröva att rita ut den (men du måste inte). I fortsättningen kan du, om du vill, prova att rita dina objekt med funktionen drawgraph på samma sätt som för punkten. Observera att för att drawgraph ska fungera så måste funktionerna ha exakt det namn som står i labbinstruktionen (kom ihåg att DrScheme skiljer på gemener och versaler), dina konstruktorer och selektorer måste vara evaluerade och de måste förstås fungera. Glöm inte att även evaluera drawgraph innan du försöker använda den. Eftersom en cirkel har ett origo och en radie är det naturligt att man använder sig av en punkt när man skapar en cirkel, så att cirkeln kommer att bestå av en punkt och en radie. Uppgift 3 Konsten att abstrahera datatyper ytterligare Syfte: Att förstå hur ett objekt är uppbyggt av delobjekt och därmed kan använda dessas konstruktorer. Skriv in och ändra (makecircle x0 y0 radius) ovan så att den använder makepoint. Då ska (makecircle 1 2 3) ge resultatet (circle (point 1 2) 3).
Laboration 3 Datalogi, grk 1 3 På liknande sätt kan man beskriva andra geometriska figurer. En linje kan t.ex. beskrivas med hjälp av två ändpunkter: ;; make a line (define makeline (lambda (x0 y0 x1 y1) (makegraph line (list (makepoint x0 y0) (makepoint x1 y1))))) > (makeline 0 1-4 7) (line (point 0 1) (point -4 7)) En rektangel kan beskrivas med hjälp av två punkter, där den ena punkten ligger i nedre vänstra hörnet och den andra i det övre högra hörnet (det skulle gå lika bra att använda nedre högra och övre vänstra). Detta är exempel på en mer avancerad konstruktor eftersom den beroende på indata måste skapa punkterna på olika sätt. I det grafiska paketet är origo längst upp till vänster och koordinataxlarna pekar åt höger respektive nedåt, enl följande figur: (0, 0) x y Uppgift 4 Syfte: att lära sig att skriva smartare konstruktorer. Skriv en procedur makerectangle som skapar en rektangel där det nedre vänstra hörnet ligger före det övre högra hörnet i listan. makerectangle ska ta 4 argument: x0, y0, x1 och y1 som är koordinater för de två punkter, (x0, y0) och (x1, y1), som spänner upp rektangeln. Observera att punkterna kan spänna upp endera diagonal i rektangeln. x0 och x1 skall alltså vara max- och minvärden i x-led för rektangeln (inte nödvändigtvis i den ordningen). y0 och y1 anger p.s.s. max- och minvärden i y-led. Resultatet av anropet (makerectangle 3 2 1 7) skall bli (rectangle (point 1 7) (point 3 2)). Man ska få samma resultat av anropen (makerectangle 1 2 3 7) och (makerectangle 1 7 3 2), dvs det skall inte spela någon roll vilken diagonal man ger. De fördefinierade funktionerna min och max är användbara i den här uppgiften. De finns beskrivna i SCHEME-häftet på s. 22. En bild kan bestå av en mängd figurer. Det är naturligt i scheme att placera alla dessa figurer i en lista som får representera en mängd av figurer. En bild kan då också bestå av andra bilder. För att skapa en bild som består av en cirkel, en kvadrat och en punkt kan man skriva: ;; make a picture. thegraphs is a list of graphs ;; or other pictures. (define makepict
4 Datalogi, grk 1 Laboration 3 (lambda (thegraphs) (makegraph pict thegraphs))) > (define my_circle (makecircle 1 2 3)) (circle (point 1 2) 3) > (define my_square (makerectangle 0 0 1 1)) (rectangle (point 0 1) (point 1 0)) > (define my_point (makepoint 3 2)) (point 3 2) > (makepict (list my_square my_circle my_point)) (pict (rectangle (point 0 1) (point 1 0)) (circle (point 1 2) 3) (point 3 2)) En selektor kan göra mer än att plocka ut och returnera relevanta data (tex radien i en cirkel). Den följande selektorn tar ut radien i en cirkel, men kontrollerar först att indatat (circle) verkligen är en cirkel. Den fungerar som innan om den anropas med en cirkel, annars skrivs ett felmeddelande ut (t.ex, This doesn t work with rectangle ). Du behöver inte bry dig om hur proceduren errormessage fungerar. ;; radius of a circle (define Radius (lambda (circle) (if (eq? (name circle) circle) (cadr (shape circle)) (errormessage "this does not work with" circle)))) ;; report an error. string is the error message and ;; object the object that caused the error. (define errormessage (lambda (string object) (error (string-append string " " (symbol->string (name object))))) För att få reda på horisontella koordinaten för en punkt kan man skriva: ;; x-coord of point. (define X_coord (lambda (point) (if (eq? (name point) point) (car (shape point)) (errormessage "this does not work with" point)))) Observera hur lika dessa två funktioner är. Vidare så kan (bör) varenda selektor kontrollera att den indata den har fått är av förväntad typ (cirkel, rektangel etc.) I stället för att skriva denna kod i alla selektorer kan man skriva en hjälpfunktion (retrieve) som tittar på indatas typ och jämför det med det förväntade indatat. Om indatas typ är det förväntade så kan man med hjälp av den funktion som utgör det tredje argumentet, selection, ta fram det sökta värdet, dvs. radien i cirkelradie-exemplet. Ett exempel: >(retrieve my_circle circle cadr) 3
Laboration 3 Datalogi, grk 1 5 Uppgift 5 Syfte: att lära sig hitta den gemensamma faktorn hos procedurer Skriv en procedur (retrieve graph expected-name selection) som ger möjlighet att definiera procedurerna X_coord och Radius som: ;; radius of circle (define Radius (lambda (circle) (retrieve circle circle cadr))) ;; x-coord of point (define X_coord (lambda (point) (retrieve point point car))) Var noga med att testa dina procedurer vartefter de blir klara. Det underlättar felsökning i det färdiga programmet. Uppgift 6 Syfte: att lära sig skriva och använda selektorer och predikat När du skriver selektorerna tänk då på att inte använda onödiga car och cdr, utan använd alltid färdiga selektorer då sådana finns och där det är relevant. retrieve som du skrev nyss ska användas där den är relevant. Skriv även procedurerna Y_coord Lower_left_corner Upper_right_corner Lower_right_corner Upper_left_corner Origo Startpoint Endpoint Graphs First_of Rest_of no_more? som returnerar Vertikal koordinat för en punkt. Nedre vänstra hörnet i en rektangel. övre högra hörnet i en rektangel. Nedre högra hörnet i en rektangel. övre vänstra hörnet i en rektangel. Cirkelns mittpunkt. Ena punkten på en linje. Andra punkten på en linje. Listan av figurer i en bild (pict). Den första figuren i listan av bilder. En ny bild bestående av alla utom den första figuren i listan av bilder Sant om listan av bilder i en bild är tom, annars falskt. Observera att Rest_of skall returnera en ny bild, inte bara en lista med figurer. Den sista funktionen slutar på? eftersom det är ett predikat och inte en selektor. Predikat kallas funktioner som, givet indata, returnerar sanningsvärden, d.v.s. sant eller falskt (#t eller #f i Scheme). Pröva gärna att rita ut dina figurer m.h.a. drawgraph, men OBS att det inte är ett krav att du kan göra det.
6 Datalogi, grk 1 Laboration 3 Uppgift 7 Syfte: att verkligen förstå nyttan av konstruktorer och selektorer Definiera om makegraph som nedan och redogör för vad du behöver ändra i resten av dina funktioner för att de fortfarande ska fungera. ;; make a new graph (define makegraph (lambda (name shape) (cons shape name))) Om du fick ändra på många ställen har du inte gjort rätt. Gör om. Att skriva om makegraph som ovan känns kanske onödigt men det illustrerar anledningen till varför man bör använda sig av konstruktorer och selektorer för att höja abstraktionsnivån. Ofta tvingas man att testa en mängd olika sätt att representera data i programmen och om man då använder konstruktorer och selektorer så isolerar man problemet med representation från programmets logik. Vid byte av datarepresentation slipper man ändra andra delar än just konstruktorer och selektorer om man gjort rätt. För att bli godkänd på den här labben ska du åtminstone ha gjort alla deluppgifter och de funktioner du skrivit ska fungera (om du kan rita ut alla olika typer av figurer m.h.a. drawgraph har du antagligen gjort rätt). kunna förklara hur retrieve fungerar, vad den gör och vad den är bra för. kunna förklara vad konstruktorer, selektorer och predikat är, vad de används till samt varför man ska använda dem. kommentera dina funktioner snyggt och vettigt. Dessutom ska du kunna svara på de kluriga frågor som handledarna kan komma att ställa vid redovisningen. Att rita grafiska figurer på skärmen med DrScheme Starta först DrScheme. Utför kommandot (require (lib simple-draw sudata )) om du vill ladda in ritmodulerna. Efter det kommer kommandot (start 400 400) göra att ett särskilt grafikfönster öppnas. Om du vill rita något av dina grafiska objekt så använder du funktionen drawgraph som finns i just den fil du kopierade till din scheme-katalog. T.ex. så ritar följande två rader en punkt på skärmen: (define mypoint (makepoint 10 20)) (drawgraph mypoint) Om du har laddat ned modulerna kan du med kommandot (start-graphics) starta ett fönster.