Umeå universitet Datavetenskap Lennart Edblom Tentamen 1997-04-30 Datavetenskap A, mom.2 Tentamen- lösningsförslag Datavetenskap A, moment 2 (7 poäng) 16 maj 1998 Skrivtid : 9-15 Hjälpmedel: Appendix C och D ur Wikström:Functional Programming Using Standard ML Maxpoäng 58 (för godkänt krävs ca 29 poäng). Tentanden får behålla detta frågeformulär. Läs igenom alla uppgifter,så att du upptäcker eventuella oklarheter. Jag besöker er i skrivsalen ungefär kl 12.30 , då kan ni be om hjälp. För övrigt är jag anträffbar på tel 010-652 5394 för det mesta. Uppgifterna är slumpmässigt oordnade.Se alltså till att du hinner försöka på alla uppgifter! En del är kanske lättare än du tror. Me se till att du inte fastnar på ML-delen! Disponera din tid så att du hinner jobba med båda delarna! Lämna in lösningarna i nummerordning. Endast ett problem på varje blad! Skriv namn på varje blad. Fyll också i försättsbladet, och markera med X de uppgift du lämnar in ett lösningsförslag på. Kommentera källkoden, så att jag kan se om du tänkt rätt men skrivit fel. Om problembeskrivningen (trots att ni frågat mig) är oklar, bestäm dig då för en rimlig tolkning och anteckna den vid din lösning. Även en ofullständig lösning kan ge några poäng. Om en uppgift innehåller en deluppgift som du inte kan lösa, får du ändå i följande deluppgifter om så är lämpligt använda en funktion som antas lösa den olösta deluppgiften. Skriv tydligt! Försök att göra lösningarna så lättlästa som möjligt De fördefinierade funktionerna i appendix D får användas om ej annat anges. Uppgift 1 (6p) a) Vilka typer har följande uttryck/funktioner: • ([5],[8]) int list * int list • fn f => fn x => f x div x; (int -> int) -> int -> int • [[(”a”,1), (”b”,2)], [(”c”,3), (”d”,4)]] • fun f [ ] = [ ] | f ((x,y)::t) = x::f t (string * int) list list (‘a * ‘b) list -> ‘a list b) Förklara kortfattat men uttömmande följande begrepp. 1) Polymorf funktion 2) Högre ordningens funktion a) Polymorf funktion - en funktion vars typ beror av de argument den appliceras på. Det typuttryck som beskriver funktionens typ innehåller typvariabler. Den fungerar på en hel ”familj” av typer eftersom funktionen opererar på argumentets struktur, inte själva värdena. b) Högre ordningens funktion - en funktion som tar en annan funktion som argument och /eller returnerar en funktion som resultat. Uppgift 2 (3p) a) Visa steg för steg hur följande uttryck beräknas (reduceras) i ML. Alla inblandade funktioner (genlist, times) finns definierade i Appendix D genlist 2 (times 3) 3; genlist 2 (times 3) 3 -> 2:: genlist (times 3 2) (times 3) 2 -> 2:: genlist 6 (times 3) 2 -> 2:: 6:: genlist (times 3 6) (times 3) 1 -> 2:: 6:: genlist 18 (times 3) 1 -> 2:: 6:: 18:: genlist (times 3 18) (times 3) 0 -> 2:: 6:: 18:: genlist 54 (times 3) 0 -> 2:: 6:: 18:: nil -> [2,6,18] Eftersom uttryck i ML alltid evalueras från vänster till höger blir det genlist som först ”försöker hitta” sina argument. Det blir inte (times 3) som tar 3 som argument. b) Vad gör ML-funktionen volvo? (Vilket är det vanliga namnet på funktionen?) fun volvo saab fiat nil = nil | volvo saab fiat (opel::audi) = saab opel (volvo saab fiat audi) Tyvärr blev det ett fel i ovanstående funktion. Resultatet i basfallet skulle ha varit fiat, inte nil! Då hade det möjligen varit lättare att se att detta är reduce-funktionen. (Inte map som många hade satsat på). Uppgift 3 (4p) Romerska talsystemet är inte ett positionssystem som vårt talsystem, utan talen beskrivs som en sekvens av bokstäver där bokstäver med störst värde kommer först. Bokstäverna har följande värden: I = 1, V = 5, X = 10, L = 50, C = 100, D = 500, M = 1000 Om en bokstav används flera gånger adderas dess värde för varje gång, utom i de fall då bokstaven är före en bokstav med större värde; då subtraheras bokstavens värde i stället. Ex: II =2 IV =4 XIX =19 LXXXIX =89 Du ska skriva en ML-funktion som omvandlar en sträng som representerar en romersk siffra till ett heltal. Du får anta att strängen är ett korrekt format romerskt tal. Uppgift 4 (7p) a) Definiera en datatyp FRUIT med två konstruerarkonstanter Apple och Pear. b) Antag att tack vare framsteg inom botaniken skulle man kunna odla fruktträd med både äpplen och päron på samma träd. Definiera en datatyp FRUITTREE! Den nya typen ska vara en binär struktur med ett värde av typen FRUIT i "löven". Exempel: c) Skapa och bind trädet i figuren ovan till namnet mytree med hjälp av en deklaration! 2 d) Skriv en funktion count av typ FRUITTREE -> (int *int) som räknar antalet äpplen och antalet päron på ett träd! e) Skriv ett uttryck som beräknar antal äpplen och antalet päron på mytree ! a) datatype FRUIT = Apple | Pear; b) datatype FRUITTREE = Leaf of FRUIT | Node of FRUITTREE*FRUITTREE; Det träd jag deklarerat kan inte vara tomt. Vill man kunna ha såna kan man lägga till en konstruerare Empty. Observera att man måste ha en konstruerare även för FRUIT. Flera skriver datatype FRUITTREE = FRUIT | Node ...... men då blir FRUIT en ny konstruerare, och Apple och Pear är oåtkomliga. Andra skriver datatype FRUITTREE = Empty | Node of (FRUITTREE * FRUITTREE), men då får vi ett träd med bara struktur, inga frukter! En annan variant är datatype FRUITTREE = Empty | Node of ( FRUIT * FRUITTREE * FRUITTREE), men då får vi frukter i alla noder, även de interna. Ytterligare andra vill gärna ha med en typvariabel och deklarera ett ‘a FRUITTREE, vilket visserligen inte är fel, men helt poänglöst i denna uppgift, eftersom vi sagt att det ska vara FRUITs och inget annat i trädet. Många använder dessutom en typvariabel på högersidan utan att ha deklarerat någon på vänstersidan, dvs man har bara skrivit datatype FRUITTREE. Ännu en typ av fel är att använda typen FRUIT (i stället för FRUITTREE) på högersidan. men då får vi ingen rekursiv typ, och kan ej bygga några träd! c) val mytree = Node(Node(Leaf Pear,Leaf Apple), Node(Leaf Apple,Node(Leaf Pear,Leaf Apple))); d) fun count (Leaf Apple) = (1,0) | count (Leaf Pear) = (0,1) | count (Node (l,r)) = let val (al,pl) = count l and (ar,pr) = count r in (al+ar,pl+pr) end; Man kan förstås också ha en underfunktion som räknar äpplen, och en som räknar päron, som en del varit inne på. e) count mytree; Uppgift 5 (6p) Innehållet i ett lager representeras av en lista av föremål. Varje föremål representeras av en trippel av typ (string*int*int) , där betydelsen av delarna är (namn, antal i lager, gräns för att beställa mer). a) Skriv en funktion som returnerar en lista av de föremål där antal i lager är mindre än beställningsgränsen. Ex: -getmore [("RAM",9,10),("ROM",12,10),("PROM",20,21)]; > val it = [("RAM",9,10),("PROM",20,21)]:(string*int*int)list; b) Skriv en funktion som tar en lagerlista och en uppdateringslista av typ (string*int)list där varje par har betydelsen (namn, antal nyinkomna), och skapar en ny lagerlista där antal i lager = gammalt antal i lager + antal nyinkomna. Posterna i uppdateringslistan kan vara i godtycklig ordning. Det behöver inte finnas uppdateringsposter för alla föremål i lagerlistan. Det kan finnas flera uppdateringsposter för samma föremål. Exempel: - update [("RAM",9,10),("ROM",12,10),("PROM",20,21)] [("PROM",15),("RAM",12),("PROM",15)]; >[("RAM",21,10),("ROM",12,10),("PROM",50,21)]:(string*int*int)list; 3 a) Jag visar en lösning här där jag definierar s.k. projektionsfunktioner för att plocka fram de olika komponenterna i tuppeln. Naturligtvis kan man göra det direkt med mönstermatchning också. fun item ((s:string),n,r)=s; fun numb (s,(n:int),r) = n; fun reord (s,n,r) = r; fun getmore [] = [] | getmore (x::xs) = if numb x < reord x then x::getmore xs else getmore xs; Som vanligt kan man också använda en ackumulerande parameter där man samlar upp listan på de varor som ska beställas i denna parameter. Detaljerna kan ni säkert lista ut själva. Ingen hade löst detta med en HOF, vilket ju annars kan vara ganska elegant. men det är kanske inte den lösning man först tänker på. local fun check (x,(y:int),(z:int)) = y<z in fun getmore l = filter check l end; b) Det som är lite knepigt med denna uppgift är att det krävs dubbel rekursion, som i mitt lösningsförslag nedan där man för varje post i uppdateringslistan måste gå igenom (med rekursion) lagerlistan tills man hittar den post som ska uppdateras. Några har försökt smälta samman detta i en enda rekursiv funktion, men det är svårt... Ett gångbart alternativ är att för varje post i lagerlistan gå igenom uppdateringslistan för att hitta uppdateringar. Men då måste man komma ihåg att varje vara kan ha flera uppdateringsposter, så man kan inte avsluta rekursionen när man hittar en uppdateringspost, utan man måste gå igenom hela uppdateringslistan. Det andra lösningsförslaget visar hur detta kan gå till. fun uitem ((s:string),n) = s; fun unumb (s,(n:int)) = n; fun upd [] u = [] | upd (x::xs) u = if item x = uitem u then (item x,numb x + unumb u,reord x)::xs else x::upd xs u; fun update xlist [] = xlist | update xlist (u::us) = update (upd xlist u) us; Alt: fun upd v [] = v | upd (name,ant,lim) (u::us) = if name = uitem u then upd (name, ant+unumb u,lim) us else upd (name,ant,lim) us; fun update [] ulist = [] | update (x::xs) ulist = (upd x ulist)::update xs ulist; Uppgift 6(4p) Givet strukturen structure MY_struct = struct abstype ELEM = Pair of (int * string) with fun mkElem (i,s) = Pair(i,s) end fun inc (x:int,s) = (x+1,s) val x= Pair(1,”one”) end 4 Vilka av följande signaturer matchar strukturen? Om en signatur inte matchar, förklara varför! signature ONE = sig end signature TWO = sig type ELEM val inc : ELEM -> ELEM end signature THREE = sig datatype ELEM val x : ELEM end signature FOUR = sig type ELEM val mkElem : (int *string) -> ELEM val inc : (int *string) -> (int *string) val x : ELEM end ONE är OK, matchar vilken struktur som helst TWO matchar inte, ty funktionen inc i strukturen är av typ (int * 'a) -> (int * 'a) THREE är en felaktig signatur, en datatype i en signatur måste ha minst en konstruktor. Kan inte heller motsvaras av en abstype i strukturen FOUR är OK om vi antar att det står val x=mkElem(1,”one”) i strukturen Följande uppgift ska endast lösas av de C-studenter som enbart tenterar ML (ej programspråksdelen). Uppgift 7(6p) Vi har läst in ett dokument till en enda lång sträng. Nu vill vi leta reda på alla ord i detta dokument som innehåller en annan, given sträng. De tecken som skiljer ord åt i dokumentet är "\n", " ", "," och ".", alltså newline, blank, komma och punkt. Det kan finnas flera av dessa ordskiljande tecken efter varann. Din uppgift är att skriva en ML-funktion lookup som givet två strängar, dokumentsträngen och söksträngen, returnerar en lista med alla ord som innehåller söksträngen. Tänk igenom uppgiften och skriv ner en algoritm innan du börjar koda. Dela upp den i deluppgifter / hjälpfunktioner på lämpligt sätt. Även om du inte kan lösa uppgiften som helhet så kan en korrekt algoritm och/eller korrekta delfunktioner ge poäng. Ex: lookup "tom,peng. pung" "om" ska returnera ["tom"] Denna lösning är ej testkörd. Det kan finnas nåt fel i den, det kan också finnas smartare lösningar. Jag har inte heller skrivit ner nån algoritm fastän ni bör ha gjort det i era lösningar. prefix tar två listor av strängar (där varje sträng lämpligen består av bara ett tecken) och kollar om den första listan är ett prefix till den andra fun prefix [] ys = true | prefix xs [] = false | prefix (x::xs) (y::ys) = x=y andalso prefix xs ys; substring tar två listor av strängar och kollar om den första listan finns som en dellista av den andra. (Om man tänker sig att man sedan applicerar implode på de två argumenten så har man alltså kollat om den första strängen är en delsträng av den andra.) 5 fun | | ys; fun | | | | substring [] ys = true substring xs [] = false substring xs (yys as y::ys) = prefix xs yys orelse substring xs separator separator separator separator separator " " = true "." = true "," = true "\n" = true _ = false; words är av typen string -> string list list . Den tar en sträng, "exploderar" den till en lista av strängar med ett tecken, delar upp denna i ord (ett ord = en lista av enteckensträngar) och returnerar en lista av ord. Ordskiljande tecken tas bort fun words ss = let fun getword w [] = rev w | getword w (x::xs) = if separator x then (rev w)::skip xs else getword (x::w) xs and fun skip [] = [] | skip (x::xs) = if separator x then skip xs else getword [x] xs in skip (explode ss) end; fun search key dok = map implode (filter (substring (explode key)) (words dok)); Uppgift 8 (2p) Para ihop rätt språk med rätt kodsnutt! Prolog Ada MEAN <– (+/A) ÷ A APL sum = ++count C Fortran IV (COND ((NULL L) 0) (T ADD1 (CDR L))) LISP LISP DO 100 I=1,10 Fortran APL C Uppgift 9 (6p) a) Förklara tydligt hur var och en av följande parameteröverföringsmetoder fungerar. Pass-by-value Pass-by-reference Pass-by-value-result Pass-by-value Den aktuella parameterns värde beräknas, och tilldelas den formella parametern. F.P. fungerar sedan som en lokal variabel i underprogrammet, den aktuella parameterns värde kan ej ändras. Pass-by-reference Den aktuella parameters adress beräknas. Om a.p. är ett uttryck allokeras en ny adress. Den formella parametern sätts att referera till den aktuella parameterns adress. En ändring av f.p. kommer alltså att påverka a.p. Pass-by-value-result Fungerar som c-b-value, med det tillägget att när återhoppet sker kopieras det värde som den formella parametern då har tillbaka till den aktuella parametern. b) Konstruera ett exempel som ger olika resultat för var och en av ovanstående tre metoder 6 Uppgift 10 (4p) I boken delas arrayer upp i fyra kategorier, baserat på olika alternativ för bindning av index och minnesallokering. Vilka är dessa fyra kategorier? Beskriv hur (när och var) bindning av index och minnesallokering görs för var och en av dem. • Statiska arrayer - bindning av index och minnesallokering görs statiskt, innan exekveringen. Inga ändringar kan göras. • ”Fixed stack-dynamic” - indexgränser bestäms statiskt och kan ej ändras, men minne allokeras (på exekveringsstacken) när deklarationen ”elaboreras”, dvs t ex när det underprogram där arrayen deklareras exekveras. • Stack-dynamic” - indexgränserna bestäms först när minne allokeras (alltså när deklarationen ”elaboreras”), men kan sedan inte ändras, dvs arrayens storlek kan inte ändras. Minne allokeras på stacken p.s.s. som för ”fixed stack-dynamic”. • ”Heap-dynamic” - indexgränser och arraystorlek kan ändras dynamiskt, som en följd måste även minnesallokering göras dynamiskt och kunna ändras. Görs då på ”högen”. Många har blandat ihop detta med de fyra typer av variabler man kan urskilja (och som efterfrågades i första tentan). Dessa kategorier sammanfaller bara delvis med de arraykategorier som efterfrågas! Uppgift 11 (5p) a) Vad menas med referensomgivningen för en sats/ ett uttryck? Referensomgivningen till en sats / ett uttryck är alla namn (identifierare, variabler) som är ”synliga” för den satsen, dvs som man kan referera till / använda i satsen. b) Beskriv hur referensomgivningen är konstruerad / bestäms i ett språk med • statisk räckviddsbindning • dynamisk räckviddsbindning Statisk rvb - referensomgivningen avgörs redan vid kompileringen, och beror på programmets textmässiga struktur. I ett Algol/Pascal-linkande språk med blockstruktur består referensomgivningen av alla variabler i det aktuella blocket, samt alla variabler i textmässigt omgivande block som inte är ”dolda” av lokala (eller mer ”närliggande”) variabler med samma namn Dynamisk rvb - referensomgivningen avgörs dynamiskt, under exekveringen. Den består (vanligen) av alla lokala variabler (dvs variabler i det aktuella blocket/underprogrammet) plus alla variabler i anropande enhet som inte är ”dolda” plus alla variabler i den enhet som anropade den anropande enheten o.s.v., dvs alla variabler som kan nås genom att följa anropskedjan bakåt. c) I ett språk med statisk räckviddsbindning, kan en variabels räckvidd skilja sig från dess livslängd? Förklara / ge exempel! En variabels räckvidd kan mycket väl skilja sig från dess livslängd? I språk med helt statisk minnesallokering är livslängden även för underprogram-variabler hela programexekveringen, men räckvidden är bara det underprogram där variabeln deklarerats. Ett annat exempel är dynamiska variabler som allokeras på heapen, som fortfarande kan vara ”levande” fast dom inte är åtkomliga. (Men flera av er skiljer dåligt på pekaren som pekar på denna variabel, och variabeln själv!) d) Det finns flera andra saker/kontroller i ett programspråk som kan avgöras / göras antingen statiskt eller dynamiskt. Nämn en sådan sak, och beskriv kort vad det är. (Du behöver inte beskriva skillnaderna mellan statiskt och dynamiskt utförande, utan bara det övergripande syftet) 7 Statisk / dynamisk • typkontroll • minnesallokering Uppgift 12 (4p) Förklara följande termer, dvs definiera dem och beskriv deras innebörd / betydelse (om den inte framgår tydligt av själva definitionen): • ”Short-circuit evaluation” • Ett starkt typat (strongly typed) språk • Exceptions (”undantag”) • Garbage • Short-circuit evaluation Ett utrycks värde beräknas utan att alla operander evalueras. (En operand evalueras alltså bara om dess värde behövs. Detta är alltså ett specialfall av lat exvaluering) • Ett starkt typat (strongly typed) språk Bokens definition är att ett språk är starkt typat om alla typfel upptäcks, antingen under kompilering eller exekvering. En annan vanligt förekommande definition är att ett språk är starkt typat om alla nödvändiga typkontroller sker under kompileringen. Inga typfel ska då kunna uppkomma under exekveringen. • Exceptions (”undantag”) Ett undantag är en oväntad händelse (t ex ett fel) som uppkommer/händer under exekveringen av ett program. (Bör lämpligen hanteras på ett sådant sätt att man slipper exekveringsavbrott.) • Dangling pointers En pekare som pekar på minnesceller som inte innehåller något dataobjekt. Det objekt som tidigare fanns där har avallokerats. Dessa celler kan alltså allokeras för något annat ändamål, men kan ändå åtkommas vi den ”dinglande pekaren”. Uppgift 13 (4p) Här kommer ett elakt program. (Bli inte rädda, det är inte så svårt som det kan se ut). program P; var a,b,c : integer; proc Q; begin a:=a+2; c:=c+2; end {Q} proc R; var c:integer; begin c:=2; call Q; b:=a+b; write (a,b,c); end {R} proc S; var b,c:integer; proc Q; begin a:=a+1; c:=c+1; end {Q} begin 8 b:=3; c:=1; call Q; call R; end {S} begin a:=1; b:=1; c:=1; call S; end. a) Visa vilka aktiveringsposter som finns på stacken när satsen a:=a+2 i Q exekveras Visa för varje post vars dess statiska och dynamiska länk pekar. Visa också vilka variabler som hör till posten, och vilka deras värden är. b) Vad menas med sidoeffekter? Förekommer det några sådana i ovanstående program? Ge exempel i så fall. Varför har alla ritat in aktiveringsposten för anropet till Q i S på stacken?? Den aktiveringen av Q är ju sen länge avslutad, och a-posten poppad från stacken när vi kommer till a:=a+2 i Q i P! Att visa vilka variabler som finns i varje a-psot, och vilka deras värden blir var ni också dåliga på. Länkarna kunde ni bättre. St at i sk l änk Dynami sk l änk Var i abel vär den Q c: 2 c:2 , 4 ( q ) R b: 3 c: 1 , 2 ( q) b:3 , 7 ( r )c:1 , 2 ( q ) S a: 1 , 2 ( q) , 4 ( q) b: 1,1 5 ,( 3r () q) c: a) a: 1 , 21 ( q) , 4 ( q) b: c: 1 Pv id b) En sidoeffekt är när ett underprogram förändrar värdet hos en icke-lokal variabel (eller, i en mindre skala, när evalueringen av ett uttryck påverkar "tillståndet"). Sidoeffekter är oönskade eftersom de gör det omöjligt att förstå ett underprogramanrop utan att studera underprogrammet självt (förstör abstraktionen). Det är gott om sidoeffekter i programmet i upppgiften; värdena på a och c ändras i Q, b ändras i R, a och c ändras i den andra Q (inuti S). 9 Uppgift 14 (3p) Är pekare nödvändiga i ett programmeringsspråk? Ange och diskutera skäl för och emot! (Obs att inget specifikt svar är ”rätt” på denna uppgift. Det som bedöms är allsidigheten och ”tyngden” i din argumentation). Enligt boken är de två huvudsakliga användningsområdena för pekare • indirekt adressering (inklusive pekararitmetik) • möjlighet att skapa dynamiska datastrukturer, och att överhuvudtaget allokera och hantera minne dynamiskt. Man skulle också kunna lägga till • effektivitet, t ex att kunna skicka en pekare/adress som parameter i st f en hel datastruktur. Att skapa dynamiska datastrukturer måste definitivt vara möjligt i ett modernt språk. Frågan är dock om det ska ske genom explicit pekarhantering, eller om det ska skötas av runtime-systemet som i t ex ML? Ju lägre nivå (ju ”närmare maskinen”) man programmerar på, dess mer nödvändigt är det att kunna använda indirekt adressering. I ett språk på en hög abstraktionsnivå är det emellertid inte nödvändigt, vilket vi t ex ser av Pascal. Det finns visserligen en del ”maskinnära programmering ” man inte kan göra i ett sådant språk, men såna uppgifter kan man ha speciella språk för. Om man har ett språk där alla datatyper är ”fullvärdiga medlemar”, dvs kan skickas som parametrar, retuneras som funktionsresultat etc, så behöver man inte pekare för parameteröverföring etc. Det kan fortfarande vara adresser som skickas (av effektivitetsskäl, men det hanteras av systemet, inte av programmeraren. Pekare har också nackdelar. Den största är attäven en van programmerare kan råka skapa ”dangling references”, pekare till avallokerade objekt, som om de ändå används kan komma att orsaka svårförståeliga fel. Pekare kan också vara svårförståeliga för programmeraren och minskar programmerareffektiviteten, och ökar chansen för fel. Olika språk försöker minimera dessa nackdelar på olika sätt, men de finns ändå där. Sammanfattningsvis, om man har ett språk som har inbyggd ”garbage collection” och dynamisk minneshantering, där alla typer är fullvärdiga, som innehåller tillräckligt kraftfulla datatyper och som överhuvudtaget har goda möjligheter till dataabstraktion, då behöver man inte pekare. Sen kan det visserligen vara så att många ändå önskar sig pekare, t ex för att • man vill ha kontroll över vad som görs. 10