ID1218 Johan Montelius Tentamen: Tillämpad Programmering (ID1218) 2011-12-16 09:00-12:00 Förnamn: Efternamn: Regler Du får inte ha något materiel med dig förutom skrivmateriel. Mobiler etc, skall lämnas till tentamensvakten. Instruktioner Tentamen har totalt 200 poäng och skall skrivas på 3 timmar. Antalet poäng på en uppgift skall ge dig en vägledning till hur mycket tid uppgiften kan ta att lösa, planera din tid väl. Läs igenom hela tentamen innan du börjar. Svaren skall lämnas på dessa sidor, använd det utrymme som finns under varje uppgift för att skriva ner ditt svar. Om du inte får plats med sitt svar så kan du använda ytterligare sidor. Dessa sidor skall då vara märkta med namn och personnumer. Det skall även tydligt framgå i svaret under uppgiften att ytterligare sidor har använts. 1
Du skall lämna in hela denna tentamen. Svar skall skrivas på svenska. Betyg Erhållna poäng Skriv inte här, detta är för rättningen. n < 100 F 100 n < 120 E 120 n < 140 D 140 n < 160 C 160 n < 180 B 180 n A Uppgift 1 2 3 4 5 6 7 Σ Max 30 30 30 30 30 20 30 200 Poäng Totalt antal poäng: Betyg: 2
1 Funktionell Programmering[totalt 30 poäng] Antag att vi representera en nod i ett binärt träd med tupeln {node, Key, Value, Left, Right} och ett löv med atomen nil (ett tomt träd representeras av ett löv). Paret Key, Value kommer nedan att kallas för ett element. Trädet är ordnat så att alla element i en nods vänstra gren är mindre och alla element i dess högra gren är större. Ordningen bestäms av ett elements nyckel (Key) och vi antar att vi kan jämföra två nycklar med de vanliga operatorerna dvs:<,>,=<,=>,==,=/=. 1.1 Lägg till ett element[5 poäng] Skriv en funktion insert(key, Value, Tree), som tar en nyckel, ett värde och ett träd och returnerar ett nytt träd där elementet Key - Value har lagts till. Trädet skall fortfarande vara ordnat. Det element som läggs till kommer att ha en unik nyckel. insert(key, Value, nil) -> {node, Key, Value, nil, nil insert(key, Value, {node, K, V, L, R}) -> if end. Key < K -> {node, K, V, insert(key, Value, L), R true -> {node, K, V, L, insert(key, Value, R)} 1.2 Traversera trädet[5 poäng] Skriv en funktion traverse(tree), som traverserar ett träd och returnerar en lista av element {Key, Value} som är alla element i trädet ordnade efter storleken på nyckeln. Du får använda dig av append/2 eller operatorn++ utan att definiera dessa. traverse(nil) -> []; traverse({node, _, V, L, R}) -> traverse(l) ++ [V] ++ traverse(r); 1.3 Plocka bort ett element[20 poäng] Skriv en funktion remove(key, Tree) som returnerar ett träd där elementet med nyckel Key har plockats bort. Trädet skall fortfarande vara ordnat. Om trädet inte har något sådant element så returneras ett identiskt träd. 3
%% one alternative remove(_, nil) -> nil; remove(key, {node, Key, _, nil, R}) -> R; remove(key, {node, Key, _, L, R}) -> %% select the higest value in left branch {K, V, Rest} = rightmost(l), {node, K, V, Rest, R remove(key, {node, K, V, L, R}) -> if Key < K -> {node, K, V, remove(key, L), R true -> {node, K, V, L, remove(key, R)} end. %% The tree is not empty rightmost({node, Key, Value, L, nil}) -> {Key, Value, L rightmost({node, K, V, L, R}) -> {Key, Value, Rest} = rightmost(r), {Key, Value, {node, K, V, L, Rest}}. 2 Komplexitet[totalt 30 poäng] Nedan finns funktionen filter/1, en funktion som tar bort dubbletter från en lista. Listan antas bestå av literaler och att jämföra två literaler kan göras i konstant tid. filter([]) -> []; filter([h T]) -> [H filter(remove(h, T))]. remove(_, []) -> []; remove(x, [X T]) -> remove(x, T); remove(x, [H T]) -> [H remove(x, T]. 2.1 remove[10 poäng] Vilken är storleksfunktionen (size function), upprepningsfunktionen (recurrence function) T remove (n) och den asymptotiska tidskomplexiteten för funk- 4
tionen remove/2? T remove (n)= c+t remove (n 1), storleksfunktionen är längden på listan och komplexiteten är O(n) 2.2 filter[20 poäng] Anta att upprepningsfunktionen för remove/2 är T remove (x), vilka är storleksfunktionen (size function), upprepningsfunktionen (recurrence function) och tidskomplexiteten för filter/1. T f ilter (n)=c+t remove (n 1)+ T f ilter (n 1), storleksfunktionen är längden på listan och komplexiteten är O(n 2 ). Om vi inte hade haft anropet till remove/2 så hade komplexiteten varit O(n) 3 Operationell semantik[totalt 30 poäng] Den operationella semantiken för Erlang kan beskrivas med hjälp av en abstrakt maskin, MiniErlang, vars omskrivningsregler ges i ett appendix. Visa med hjälp av MiniErlang 3.1 att skapa en lista[10 poäng] Visa hur, utan att ta några genvägar, hur uttrycket [1,2] exekveras med hjälp av MiniErlang. [1,2] ; 1 [2] CONS ; [2] CONS ; 1 2 [] CONS CONS ; 1 [] CONS CONS ; 2 1 CONS CONS ; [] 2 1 CONS ; [2] 1 ; [1,2] 3.2 ett program[10 poäng] Antag att vi utökar vår maskin med en regel som kan flytta ett helt uttryck E direkt till värdestacken om detta uttryck är fritt från funktionsanrop och representerar ett värde V. Vi kan alltså gör saker så som: E Es ; Vs Es ; V Vs [1,2] Es ; Vs Es ; [1,2] Vs 5
Givet nedanstånde MiniErlang-program, visa exekveringen av uttrycket r([1,2],[]). r([], A) -> A; r([h T], A) -> r(t,[h A]). r([1,2],[]) ; [1,2] [] CALL(r/2) ; [] CALL(r/2) ; [1,2] CALL(r/2) ; [] [1,2] r([2],[1]) ; [2] [1] CALL(r/2) ; [1] CALL(r/2) ; [2] CALL(r/2) ; [1] [2] r([],[2,1]) ; [] [2,1] CALL(r/2) ; [2,1] CALL(r/2) ; [] CALL(r/2) ; [2,1] [] [2,1] ; ; [2,1] 3.3 Utöka maskinen[10 poäng] I den beskrivning av MiniErlang som har givits kan man inte ha mer än ett uttryck i kroppen av en definition. Man kan till exempel inte skriva. foo(x) -> Y = bar(x), zot(x,y). Antag att vi utökar våra definitioner till att inte bara ha en kropp som är ett enkelt uttryck utan även sekvenser som i definitionen ovan. Vi skulle kunna skriva om definitionen av uttryck till följande: E := int [] [E 1 E 2 ] X F(E 1,..., E n ) E := E P=E, E Där P i sista regeln är ett mönster (pattern). Semantiken skall vara att uttrycket E evalueras och matchas med P och, att den substitution som skapas appliceras på E innan E evalueras. Antag att vi har följande regel för att skriva om en LET-operation (detta är alltså en ny regel som vi inte sett förut): 6
LET P in E Es ; V Vs S(E) Es ; Vs Där S är den substitution som fås genom att matcha mönstret P med värdet V. Hur skall den regel skrivas som tar hand om ett uttryck på formen P=E, E när den ligger överst på exekveringstacken? P = E, E Es ; Vs E LET P in E Es ; Vs 4 Högre-ordningens funktioner[30 poäng] 4.1 summan av kvadrater[10 poäng] Skriv en funktion som tar en lista med heltal och producerar: summan av, kvadraten av de tal som är jämnt delbara med tre. Du skall använda dig av följande högre ordningens funktioner: filter/2, map/3 och foldl/3 eller foldr/3. Operatorn rem ger resten vid heltalsdivision. Du behöver inte kunna exakt hur dessa funktioner tar sina argument. Om du är osäker så skriv ner dina antagande. sumkdt(list) -> Delbara = filter(fun(x) -> X rem 3 == 0 end, List), Kvadrater = map(fun(x) -> X*X end, Delbara), foldl(fun(x,a) -> X+A end, 0, Kvadrater). 4.2 räkna heltal[10 poäng] Skriv en funktion, total/2, som givet en lista, L, och ett heltal, N, returnerar antingen: {total, Total}, där Total är antalet element innan första förekomsten av 0 plus N eller {more, F}, där F är en funktion som kan tillämpas på ytterligare en lista, Next, om den första listan inte innehåller en 0:a. Funktionen F skall då returnera total(next, N+T) där T är antalet element i listan L. Exempel på hur man kan använda funktionen: > {more, F0} = total([], 0). {more, #Fun<...>} >{more, F1} = F0([1,2,3]). {more, #Fun<..>} 7
>F1([4,0,5]). {total, 4} total([],t) -> {more, fun(next) -> total(next,t) end total([0 _], T) -> {total, T total([_ R], T) -> total(r, T+1). 4.3 höger eller vänster[10 poäng] I Erlang, liksom i de flesta funktionella språk, finns de två inbyggda funktionerna foldl och foldr som gör fold på elementen i en lista antingen från vänster (foldl från början) eller från höger (foldr från slutet). Det finns för- och nackdelar med dessa två strategier och det är inte alltid klart vilken som blir mest effektiv. I nedanstående exempel vill vi lägga ihop alla elementen i en lista av listor. Funktionen flatten([[1,2],[3,4,5],[6,7]]) skall returnera[1,2,3,4,5,6,7]. Är det mest effektivt att använda foldl eller foldr i definitionen nedan (vi får vända på argumenten till append/2 för att ordningen skall bli densamma). Argumentera varför den ena skulle vara bättre än den andra. flatten(lists) -> foldl(fun(x,acc) -> append(acc,x) end, [], Lists). eller flatten(lists) -> foldr(fun(x,acc) -> append(x,acc) end, [], Lists) Funktionen foldr har fördelen att den hela tiden gör append av ett element och den ackumulerade listan. Vi har då en funktion som arbetar i O(n). Om man använder foldl så kommer vi att göra append på en ständigt ökande ackumulerad lista och ett element. Eftersom den ackumulerade listans längd är proportionell till n så har vi en komplexitet på O(n 2 ). Att foldl inte använder stackutrymme är en bisak i sammanhanget. 5 Concurrency[totalt 30 poäng] 5.1 en liten cell[10 poäng] Definiera en process i Erlang som innehåller två värden: ett saldo och antalet gånger som man övertrassera sitt konto. Processen skall ha följande beteende för de meddelanden den tar emot: 8
{deposit, Amount}: addera Amount till saldot. {withdraw, Amount, From}: dekrementera saldot och skicka ok till processen From {check, From}: skicka{saldo, Saldo} till processen From. account(saldo, Over) -> receive {deposit, N} -> account(saldo+n, Over); {withdraw, N, From} -> From! ok, account(saldo-n, Over); {check, From} -> From! {saldo, Saldo}, account(saldo, Over) end. 5.2 undvik att övertrassera[10 poäng] Antag att vi vill undvika att övertrassera kontot och därför implementerar följande funktion: safe_withdrawal(acount, Amount) -> Account! {check, self()}, receive end. {saldo, Saldo} -> if end Saldo >= Amount -> Account! {withdraw, Amount, self()}, receive end; true -> no ok -> ok Antag att all processer som använder konto måste använda sig av denna funktionen när de gör uttag. Hur säkra är vi på att vi inte övertrasserar kontot, vad kan hända? Om det finns flera som har access till kontot kan två processer kontrollera om det finns pengar på kontot och sedan göra uttag varav det andra uttaget kan vara en övertrassering. 9
5.3 ett låst konto[10 poäng] Antag att vi vill ha ensamrätt till ett konto för att göra flera transaktioner mellan flera konto. Vi vill ha ett sätt att låsa ett konto så att enbart den som har låset får göra göra några transaktioner eller kontrollera saldot på kontot. Implementera följande metoder: {lock, From} : skicka{locked, Ref}, där Ref en unik referens (använd make_ref()), till processen From, accepterar sedan enbart förfrågningar med denna referens tills låset släpps. {unlock, Ref} : om kontot är låst och detta är den referens som skapades vid låsningen så återgå till upplåst tillstånd. deposit, withdraw, check: versioner av dessa som hanteras i det låsta tillståndet om de har rätt referens Processen skall alltså ha två tillstånd, ett upplåst där den fungerar som vanligt och ett låst där de enbart hanterar meddelanden med rätt referens. account(saldo, Over) -> receive end. : som ovan : {lock, From} -> Ref = make_ref(), From! {locked, Ref}, locked(saldo, Over, Ref) locked(saldo, Over, Ref) -> receive {deposit, Ref, N} -> locked(saldo+n, Over, Ref); {withdraw, Ref, N, From} -> From! ok, locked(saldo-n, Over, Ref); {check, Ref, From} -> From! {saldo, Saldo}, locked(saldo, Over, Ref); {unlock, Ref} -> account(saldo, Over) end. 10
6 Värden, pekare och arrayer[totalt 20 poäng] 6.1 Legala uttryck[10 poäng] Vilka uttryck är legala uttryck i C ++ (som följer standarden C ++ 03 )? Svara enbart ja eller nej. 1. int a[3*7-1]; ja: eftersom 3*7-1 är en konstant i.e. 20 2. int n=4; int a[n]; nej: n är inte en konstant 3. int x=7; int* p = &x; ja: inget problem att ta adressen av x och det är en int* 4. int x=32; int** y = &(&x)-5; nej: vi kan inte ta adressen av en adress (inte ett l-värde) 5. int x=2; int y = *(&(*(&x+1))); ja: men farligt om du inte vet vad du gör 6.2 värdet y:[5 poäng] Givet nedanstående uttryck, vad är värdet på variabeln y? int a[5] = {3,2,-1,5,4 int y = *(&a[3] - 2); Variabeln y får värdet 2 eftersom (&a[3] - 2) är &a[1] och *(&a[1]) är a[1] dvs 2. 6.3 värdet y:[5 poäng] Givet nedanstående uttryck, vad är värdet på arrayen b? int b[5] = {3,2,-1,5,4 int* p = &b[1]; *(p + 2) = *p -4; Arrayen b[] har värdet 3,2, 1, 2,4 eftersom vi ändrar på b[3] och sätter den till b[1] 4 dvs 2. 11
7 Klasser och objekt[totalt 30 poäng] I följande frågor antar vi att vi includerar biblioteket iostream och använder dess namespace std. Givet är också nedanstående definition av klassen M yint. class MyInt { private: int n; public: MyInt(int x = 0): n(x) { cout << "konstruera " << x << endl; } MyInt(const MyInt& c) : n(c.n) { cout << "kopiera " << n << endl; } ~MyInt() { cout << "avallokera " << n << endl; } int get() const { return n;} void set(int x) { n = x; } 7.1 Hej, hej![5 poäng] Vad kommer att skrivas ut på skärmen om vi kör nedanstående program? MyInt take(myint x, MyInt y) { MyInt z; if (x.get() < y.get()) { z.set(x.get()); } else { z.set(y.get()); return z; int main() { MyInt x = 2; MyInt y = 3; MyInt z = take(x,y); cout << z.get() << endl; return 0; } konstruera 2 konstruera 3 kopiera 3 kopiera 2 12
konstruera 0 kopiera 12 * avallokera 2 avallokera 3 avallokera 12 * 12 avallokera 12 avallokera 3 avallokera 2 Alla kompilatorer gör dock några optimeringar och inser att man inte skapar en lokal kopia som sedan kopieras när den returneras; detta gör att de två *-märkta raderna kan undvikas. 7.2 kanske vad vi vill[5 poäng] Om vi definierar om funktionen take enligt nedan, vad kommer då att skrivas ut på cout. MyInt take(myint& x, MyInt& y) { MyInt z; if (x.get() < y.get()) { z.set(x.get()); } else { z.set(y.get()); return z; konstruera 2 konstruera 3 konstruera 0 kopiera 12 * avallokera 12 * 12 avallokera 12 avallokera 3 avallokera 2 Som i fallet ovan kommer en kompilator att plocka bort kopierandet och avallokeringen av det lokala objektet z vilket resulterar i att de två *-märkta raderna kan undvikas. 13
7.3 på hal is[5 poäng] I nedanstående program gör vi någonting farligt, programmet kommer att kompilera (kanske med en varning) men resultatet är inte helt bestämt, varför? MyInt &take(myint &x, MyInt &y) { MyInt z; if (x.get() < y.get()) { z.set(x.get()); } else { z.set(y.get()); return z; int main() { MyInt x = 2; MyInt y = 3; MyInt &z = take(x,y); cout << z.get() << endl; return 0; } Vi returnerar en referens till ett lokalt objekt som inte lever efter anropet till funktionen take. Anropet z.get() kommer returnera ett odefinierat värde (som i detta fall troligtvis fungerar). 7.4 inte här men där[5 poäng] Givet programmet nedan, vad kommer att skrivas på cout och varför? class IntCell { private: public: int *storedvalue; explicit IntCell(int initialvalue = 0 ) { storedvalue = new int(initialvalue); } int getvalue() const { return *storedvalue; } void setvalue(int val) { *storedvalue = val;} int main() 14
{ IntCell a(2); IntCell b = a; IntCell c; c = b; a.setvalue(4); } cout << a.getvalue() << endl; cout << b.getvalue() << endl; cout << c.getvalue() << endl; Tre fyror kommer att skrivas ut eftersom all tre objekt delar samma int som är allokerad utanför objekten. Vid kopiering kopieras pekaren till cellen, inte cellen själv. 7.5 inte så bra[10 poäng] I nedanstående definition av IntCell har vi lagt till en metod för operatorn= (dvs tilldelning). För att undvika en minnesläcka så avallokerar vi den allokerade strukturen innan vi kopierar värdet. Vi har kanske inte tänkt på allt och lösningen är kanske inte önskvärd i alla lägen, vad kan gå fel? class IntCell { private: int *storedvalue; public: IntCell(int initialvalue = 0 ) { storedvalue = new int(initialvalue); } IntCell& operator=(const IntCell& c) { delete storedvalue; storedvalue = c.storedvalue; return *this; } int getvalue() const { return *storedvalue; } void setvalue(int val) { *storedvalue = val;} Om vi har ett uttryck x=x, så kommer vi att avallokera den enda strukturen som vi har och sedan kopierar en void-pekare. Objektet x kommer efter 15
tilldelningen att ha en pekare storedvalue som pekar på en redan avallokerad struktur. Att avallokera ett objekt är alltid farligt; även om vi gör om destruktorn så att den tar hänsyn till x=xså kan vi tappa bort objekt när vi gör: x= y (y:s objekt avallokers), x=z(x:s objekt, som delas med y blir avallokerat). Appendix Här följer de transformeringsregler som definierar MiniErlang. En literal (integer) på uttrycksstacken flyttas över till värdestacken. i Es ; Vs Es ; i Vs Ett cons-uttryck delas upp, läggs på uttrycksstacken och åtföljs av en CONSinstruktion. [E 1 E 2 ] Es ; Vs E 1 E2 CONS Es ; Vs Ett funktionsanrop delas upp, läggs på uttrycksstacken och åtföljs av en CALLinstruktion. F(E 1,...,E n ) Es ; Vs E 1...... En CALL(F/n) Es ; Vs En CONS-instruktion tar de två översta värdena på värdestacken och bildar en cons-cell. Observera att värdena ligger i omvänd ordning på stacken. CONS Es ; V 2 V1 Vs Es ; [V 1 V 2 ] Vs En CALL-instruktion tar sina argument från värdestacken och lägger en kopia av matchande definitions kropp, E på uttrycksstacken där substitutionen från matchningen, S, har applicerats. Observera att värdena ligger på omvänd ordning på värdestacken, det översta värdet är alltså funktionens sista argument. CALL(F/n) Es ; V n... V1 Vs S(E) Es ; Vs 16