0% found this document useful (0 votes)
33 views26 pages

Final 2223 s2 W Solution

Uploaded by

arshincollege
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
33 views26 pages

Final 2223 s2 W Solution

Uploaded by

arshincollege
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 26

Final Assessment CS2030S AY22/23 Sem 2

NATIONAL UNIVERSITY OF SINGAPORE

SCHOOL OF COMPUTING
MIDTERM ASSESSMENT FOR
Semester 2 AY2022/2023

CS2030S Programming Methodology II

April 2023 Time Allowed 120 minutes

INSTRUCTIONS TO CANDIDATES
1. This assessment paper contains 19 questions and comprises 14 printed pages, including this page.
2. Write all your answers in the answer sheet provided.
3. The total marks for this assessment is 100. Answer ALL questions.
4. This is an OPEN BOOK assessment. You are only allowed to refer to hard-copy materials.

5. All programs in this assessment paper use Java 17 and are compiled with the flags -Xlint:unchecked
and -Xlint:rawtypes .

6. Import statements for all programs are omitted. You may assume that the necessary classes are imported.
7. An abridged API for Java Stream can be found on Page 14.

Question Points
1 - 11 33
12 4
13 7
14 8
15 12
16 9
17 6
18 15
19 6
TOTAL 100

Page 1
Final Assessment CS2030S AY22/23 Sem 2

Section I: Multiple Choice Questions


Pick the most appropriate answer and shade the corresponding letter on the answer sheet. If none of the an-
swers is appropriate, shade X in the corresponding answer box on the answer sheet. If more than one answer is
equally appropriate, shade ONLY one of the answers in the answer box on the answer sheet.

Each question is worth 3 marks.


1. The exception ClassCastException is thrown during run time when the code tries to cast an object to an
incompatible type. Which of the following concepts in CS2030S, when used properly, helps to reduce the
chances of ClassCastException being thrown?

A. The Liskov Substitution Principle


B. Generics
C. Raw types
D. Pure functions
E. Immutability
Shade X in the answer box if none of the above is correct.

Solution: B. Generics

2. The NullPointerException is thrown when the code tries to use a member of a null object. Which of
the following concepts in CS2030S helps to reduce the chances of NullPointerException being thrown
during run time?
A. Dynamic binding
B. Type inference
C. The Maybe Monad
D. Lazy evaluation
E. Pokémon exceptions
Shade X in the answer box if none of the above is correct.

Solution: C. The Maybe Monad

3. Writing code with control flow and logic that is easy to reason about leads to code that is easier to debug
and maintain. Which of the following concepts in CS2030S does NOT help to make the code easier to reason
about?
A. The Liskov Substitution Principle
B. Referential transparency
C. Immutable classes
D. Pure functions
E. Aliasing
Shade X in the answer box if none of the above is correct.

Page 2
Final Assessment CS2030S AY22/23 Sem 2

Solution: E. Aliasing

4. Software evolves over time. Changes in one part of the software can impact other parts. Which of the
following concepts in CS2030S does NOT help in limiting the impact of code changes as software evolves?
A. The Liskov Substitution Principle
B. Information hiding
C. Tell, don’t ask
D. Method overloading
E. Pure function
Shade X in the answer box if none of the above is correct.

Solution: D. Method overloading

5. Which of the following is a trait of Java as a strongly-typed programming language?


A. It allows type casting with strict rules.
B. It allows arrays to be covariant.
C. It performs type inferencing.
D. It allows type erasure.
E. It allows raw types.
Shade X in the answer box if none of the above is correct.

Solution: A. It allows type casting with strict rules.

6. Consider the following program:


class Z {
void foo() {
}

static void bar(Z z, int i) {


z.foo(); // Line A
}

public static void main(String[] args) {


Z.bar(new Z(), 3); // Line B
}
}

Which of the following can be determined by the Java compiler when the program above is compiled?
A. The value of parameter i at Line A.
B. The run-time type of parameter z at Line A
C. Whether Line A will ever be reached during execution.
D. Which implementation of foo is invoked at Line A.
E. Which implementation of bar is invoked at Line B.
Shade X in the answer box if none of the above is correct.

Page 3
Final Assessment CS2030S AY22/23 Sem 2

Solution: E. Which implementation of bar is invoked at Line B.

7. Consider the following classes:


class Point {
:
}

class Rectangle {
private Point topLeft;
private Point lowerRight;
:
}

Details of the two classes Point and Rectangle are omitted. The relationship between the classes Point
and Rectangle is called:

A. inheritance
B. covariance
C. composition
D. subtyping
E. nesting class
Shade X in the answer box if none of the above is correct.

Solution: C. composition.

Page 4
Final Assessment CS2030S AY22/23 Sem 2

8. Consider the Box class below:


class Box<T> {
:
public static <T> Box<T> of(T value) {
:
}

public static <T> void foo(Box<? super T> box) {


:
}
}

Additionally, we have the following types:


interface I { }
class A { }
class AI extends A implements I { }
class AI1 extends AI { }

and variables
Object o;
I i;
A a;
AI ai;
AI1 ai1;

Which of the following method invocations would lead to a type mismatch and thus a compilation error?
A. Box.foo(Box.of(o));
B. Box.foo(Box.of(i));
C. Box.foo(Box.of(a));
D. Box.foo(Box.of(ai));
E. Box.foo(Box.of(ai1));

Shade X in the answer box if none of the above is correct.

Solution: X. The T in Box<T> , of , and foo are three different versions of T . So they can be
inferred to be three different types.

9. Consider the following program:


class Main {
public static void main(String[] args) {
Producer<Box<Integer>> p = () -> {
Box<Integer> b = Box.of(5);
// Line C
return b;

p.produce();
}
}

Which of the following statements is correct about the state of the stack when the program executes and
reaches Line C (following CS2030S convention)? Here, the stack refers to the stack in the stack and heap
diagram.
A. The stack is empty.

Page 5
Final Assessment CS2030S AY22/23 Sem 2

B. Only the stack frame for main is on the stack.


C. Only the stack frames for main and produce are on the stack.
D. The stack frames for main , produce , and of are on the stack.
E. This is a trick question as Line C is never called.
Shade X in the answer box if none of the above is correct.

Solution: C. Only the stack frame for main and produce are on the stack.
Line C is inside the lambda and this lambda is (as per our convention) used in an anonymous class
extending Producer that has a single abstract method called produce . So, produce is in the stack
as we are still inside the method produce .
of is not in the stack because Box.of(5) has finished evaluation.

Page 6
Final Assessment CS2030S AY22/23 Sem 2

10. Consider the two interfaces and one class below.


interface Func1<T1, R> {
R call(T1 t1);
}

interface Func2<T1, T2, R> {


R call(T1 t1, T2 t2);
}

class A {
public A foo1(A x) {
return null;
}

public A foo2(A x, A y) {
return null;
}
}

Additionally, you are given the following variables.


A a = new A();
Func1<A, A> f1;
Func2<A, A, A> f2;

Which of the following method references CANNOT be assigned into either f1 or f2 ?


A. A::foo1
B. A::foo2
C. a::foo1
D. a::foo2
Shade X in the answer box if none of the above is correct.

Solution: B. A::foo2 .
The assignments are as follows:

• f2 = A::foo1;

• f1 = A::foo1;

• f2 = A::foo2;

But none of the following can compile:

• f1 = A::foo2;

• f2 = A::foo2;

11. Consider the following program:


class CF {
:
private static void run(int x) {
doSomething();
System.out.print(x);
}

Page 7
Final Assessment CS2030S AY22/23 Sem 2

public static void main(String[] args) {


CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> run(4));
cf.thenRunAsync(() -> run(3));
cf.thenRunAsync(() -> run(2)).join();
cf.thenRunAsync(() -> run(1)).join();
}
}

The method doSomething takes a non-deterministic amount of time each time it is called. Which of the
following statement is NOT correct about the possible printed values?
A. If 1 is printed, it is always printed after 4.
B. If 2 is printed, it is always printed after 4.
C. If 3 is printed, it is always printed after 4.
D. If 3 is printed, it is always printed before 2 and 1.
E. 1, 2, and 4 are always printed (but not necessary in this order).

Solution: D. If 3 is printed, it is always printed before 2 and 1.


Consider the dependency graph. cf must be completed before run(3) , run(2) , or run(1) can be
evaluated. So, A, B, and C are correct (but the question asked for the one that is incorrect).
E is also correct, but we may get “421” or “412” with “3” can be inserted anywhere after “4” or not
printed at all (because there is no .join() on it). This also shows that “3” need not be printed before
“2” and “1”.

Shade X in the answer box if none of the choices above is the answer.

Page 8
Final Assessment CS2030S AY22/23 Sem 2

Section II: Short Questions


12. (4 points) Consider the following classes:
class E1 extends Exception { }
class E2 extends E1 { }

class W {
static void m1() throws E1 {
m2();
// Line D
m2();
// Line E
}

static void m2() throws E1 {


m3(0);
// Line F
m3(1);
// Line G
}

static void m3(int i) throws E1 {


if (i == 0) {
throw new E2();
}
// Line H
}

public static void main(String[] args) {


try {
m1();
} catch (E2 e) {
// Line J
} catch (E1 e) {
// Line K
} finally {
// Line L
}
}
}

Trace through the execution flow of the program when the program is executed, and write down the lines
that would be reached during the execution in order.
For example, if the execution reaches Lines D, E, F, and G in that order, write DEFG on the answer sheet.

Solution: JL
Note that the mark // Line X is exactly at that particular line and not the line above it. So // Line D
is only reached if and only if the first call of m2(); terminates normally without exception.
Further, note that although m1 is declared with throws E1 , it is actually throwing new E2(); . So it
will go to // Line J . This skips all other lines including // Line K and goes straight to // Line L
because finally is always executed.

• 4 marks for JL

• 2 marks for J , for catching the correct exception


• 0 marks for any other answer

Page 9
Final Assessment CS2030S AY22/23 Sem 2

13. (7 points) What does each of the following stream pipelines print?
(a) (3 points) dummy
Stream.of(1, 2, 3, 4)
.map(n -> Stream.iterate(1, i -> i + 1)
.limit(n)
.map(x -> x.toString())
.reduce("", (x, y) -> x + y))
.forEach(x -> System.out.print(x + " "));

Solution: 1 12 123 1234


The last white space is optional. If we use a table to represent the computation where each iteration
is a row on the table (since Stream is an abstraction of a loop), we get the following table:
Operation Elements
of 1 2 3 4
map + iterate + limit [1] [1, 2] [1, 2, 3] [1, 2, 3, 4]
above + toString() [“1”] [“1”, “2”] [“1”, “2”, “3”] [“1”, “2”, “3”, “4”]
above + reduce() “1” “12”] “123” [“1234”]

• -1 mark for missing/incorrect whitespace and/or extra separator (e.g., , )

• -2 marks for incorrect reduce operation

• -1 mark for extra/missing values in the stream


• Quotation marks are ignored

(b) (4 points) dummy


Stream.of(1, 2, 3)
.flatMap(x -> Stream.of(2, 3, 4)
.map(y -> x * y)
.filter(z -> z % 2 == 0))
.forEach(x -> System.out.print(x + " "));

Solution: 2 4 4 6 8 6 12
The table:
Operation Elements
of 1 2 3
flatMap + map 2 3 4 4 6 8 3 6 9
filter 2 - 4 4 6 8 - 6 -

• -1 mark for missing/incorrect whitespace and/or extra separator (e.g., , )

• -2 marks for incorrect flatMap operation

• -1 mark for extra/missing values in the stream


• Quotation marks are ignored

Page 10
Final Assessment CS2030S AY22/23 Sem 2

14. (8 points) A store has a program for calculating the discount prices on all products. For their big sale, there
is a requirement that for products above $100, there is a discount of 10%.
Consider the class SaleDiscount :
class SaleDiscount {
private double price;
private int productID;

public SaleDiscount(double price, int productID) {


this.price = price;
this.productID = productID;
}

public double getPrice() {


return this.price;
}

public void setPrice(double price) {


this.price = price;
}

public void discount() {


if (price > 100) {
this.price = 0.90 * this.price;
} // Line M
}
}

Sales are not going well, so the store owners decide to also discount 50% off of all products but only for
products costing $100 or lower. Consider the class FireSaleDiscount :
class FireSaleDiscount extends SaleDiscount {
public FireSaleDiscount(double price, int productName) {
super(price, productName);
}

@Override
public void discount() {
double price = this.getPrice();
double discount = (price > 100) ? 0.90 : 0.5;
this.setPrice(discount * price);
}
}
(a) (3 points) Does the class FireSaleDiscount violate the Liskov Substitution Principle? If so, why? If
not, why not?

Solution: Yes, it is substitutable, the discount method does not change the desirable property
of the parent method. It will still discount those above $100 correctly (10% off).

• 1 mark for stating “not violating LSP”


• 2 marks for correct reasoning. Overall 1 mark may be given if the reasoning is roughly cor-
rect but stated that it does violate LSP.

Some common issues:

• In LSP, we substitute superclass with subclass and not the other way around.
• The desirable property is conditioned on products above $100. Products with a price smaller
than or equal to $100 are not part of the desired property.

Page 11
Final Assessment CS2030S AY22/23 Sem 2

(b) (5 points) Consider the following class Store


class Store {
public static void main(String[] args) {
int productID = 2030;

SaleDiscount jPhone = new SaleDiscount(1200.0, productID);


SaleDiscount jPhonePlus = new SaleDiscount(1700.0, productID);

jPhone.discount();
}
}

Draw the stack and heap diagram, when the program reaches Line M. Line M is described in the SaleDiscount
class. Label your stack frames, objects, and variables clearly. You may omit args in your diagram.

Solution:

Stack Heap

SaleDiscount

price 1080
y: 1
productID 2030
discount
this

SaleDiscount

main productID 2030


price 1700
y: 1
jPhone productID 2030

jPhonePlus

1 mark deduction per mistake, which includes (but is not limited to):

• Wrong productID

• Additional arrow or arrow in the opposite direction

• Redundant and/or incorrect Information


• Missing frames
• Incorrect order of stack frame

Page 12
Final Assessment CS2030S AY22/23 Sem 2

15. (12 points) Consider the following Java classes.


class A {
public void f(B param) { }
public void f(C param) { }
}

class B extends A {
public void f(A param) { }
public void f(B param) { }
}

class C extends B {
public void f(C param) { }
public void f(B param) { }
}

Additionally, you are given the following variables to work with.


A a = new C();
B b = new B();
C c = new C();

For each of the questions below, if there is no answer, simply write “no answer”. If there are multiple possible
answers, you may choose any answer.
(a) (3 points) Which of the methods above cannot be invoked using only the given variables? You are not
allowed to use the new keyword to create a new instance. Write your answer in the following format
ClassName::method(ParameterType) .

(b) (3 points) You are allowed to add the statement super.f(param); to one and only one of the meth-
ods, in only one of the classes. Which method should the statement super.f(param); be added to,
such that there is no answer for Part (a) above? Write your answer in the following format:
ClassName::method(ParameterType) .

(c) (3 points) Write a method invocation that will invoke the method A::f(C) . Your method invocation
can only use the given variables as the target of invocation and argument.

(d) (3 points) Write a method invocation that will cause a compilation error because there is no suitable
method that can be found. Your method invocation can only use the given variables as the target of
invocation and argument.

Solution: Consider all possibilities below:

• a.f(a) leads to a compilation error since there is no method in class A that accepts A or its
super types as the argument.
• a.f(b) invokes C::f(B)

• a.f(c) invokes C::f(C)

• b.f(a) invokes B::f(A)

• b.f(b) invokes B::f(B)

• b.f(c) invokes A::f(C)

• c.f(a) invokes B::f(A)

Page 13
Final Assessment CS2030S AY22/23 Sem 2

• c.f(b) invokes C::f(B)

• c.f(c) invokes C::f(C)

So, (a) the method A::f(B) did not get invoked. (b) To invoke A::f(B) , we can call super.f(param)
in B::f(B) , (c) b.f(c) would invoke A::f(C) , and (d) a.f(a) would cause a compilation error.
For (a) and (b), the grading is slightly more lenient.

• 3 marks for somwehat incorrect format (e.g., A::f(B param) , a::f(B param) , A::f(B param) ,
a.f(b) ) as long as the intention is clear.

• 1 mark for wrong method name as, technically, such a method does not exist (e.g., A::foo(B) ).

• 0 mark otherwise.

For (c) and (d), the grading is stricter because we require a method invocation.

• 3 marks for correct method invocation and allow a simple error (e.g., missing closing parenthesis)

• 1 mark for wrong formatting


– Wrong format but correct function (e.g., B::f(c) )
– Wrong method name (e.g., b.foo(c) )
– Wrong variable (e.g., a.f(A) (argument is a class>)) or A.f(a) (calling static method?).
– Creating a new variable for arguments (e.g., b.f(new C()) .

* 0 mark for using a new variable for invocation (e.g., new B().f(c) ).

• 0 mark otherwise.

We allow the answer for (b) and (c) to be swapped. This assumes that students see the ordering in the
answer sheet as

+---+---+
| a | b |
+---+---+
| c | d |
+---+---+

instead of

+---+---+
| a | c |
+---+---+
| b | d |
+---+---+

Page 14
Final Assessment CS2030S AY22/23 Sem 2

16. (9 points) For each of the following code snippets, we will compile using the -Xlint:unchecked and -Xlint:rawtypes
flags.
(a) (3 points) Consider the following class and interface:
class A {
}

@FunctionalInterface
interface Producer {
int produce();
}

and the following snippet:


A a = new A();
Producer p = (Producer) a;

Explain why the Java compiler allows a to be cast into Producer without a compilation error and
without a compilation warning, even though it would cause a run-time error during execution.

Solution: Java allows type casting here because there is a possibility that during run-time, a is
an instance of a class that extends A and implements the Producer interface.

• 3 marks for the correct explanation.


• 0 mark otherwise.

About 35% got this correct. Common mistakes include:

• Explainin why (Producer) a can be assigned to Producer instead of the typecast.

• Stating that A <: Producer or Producer <: A even though the code snippet in the ques-
tion already tells you otherwise.
• Vague statements like “(In the future), A can implement Producer ” to explain why it can
compile. Yes, you can make A implement Producer but that is not answering the question.
• Same explanation as the solution but using superclass of A instead of its subclass. This will
compile, but will also not cause a run-time error because you will then have A <: B <:
Producer .

• Talking about generics and raw types when there are no generics in the code snippet at all.
• Explicit type casting in the program does not mean that the code will always compile and/or
type casting is successful.

• Adding -Xlint does not mean warnings will magically disappear.


• Treating Producer as a nested class of A , treating A as a anonymous class / lambda
expression.
• Talking about the @ annotation or that Producer is a functional interface which is not
very relevant.
• Treating A as an abstract class.
• Talking about mutability/immutability, which is not relevant.

(b) (3 points) Now consider the code below. The difference from Part (a) is that Producer is now a generic
interface.
class A {
}

Page 15
Final Assessment CS2030S AY22/23 Sem 2

@FunctionalInterface
interface Producer<T> {
T produce();
}

and the following snippet:


A a = new A();
Producer<Integer> p = (Producer<Integer>) a;

Explain why the Java compiler produces a warning when casting a into Producer<Integer> .

Solution: During run-time, there is no way for the type checker to check that a is an instance of
Producer<Integer> since the type argument Integer has been erased.

• 3 marks for mentioning (i) checking that a is an instance of Producer<Integer> and (ii)
this check is not possible due to type erasure.
• 0 mark otherwise.

About 11% got this correct. Common mistakes include

• Implementing produce method does not imply implementing the Produce interface. (con-
verse error, also common in 16a)
• Answers describe the process of type erasure, type casting etc. but make no reference to
what is happening in the question’s example.

• Talking about type erasure without explaining why type-checking cannot be done.
• Talking about type checking without type erasure. e.g., Student talks about invariance of
generics, such as Producer<String> not a subtype of Producer<Integer> . This does not
work without type erasure because it will just not compile.
• Many mistakes in 16a are also repeated here.
• Answering why the code compiles instead of why it gives a warning.

• Confusing what happens at compile time vs runtime. If it is just stating it incorrectly, marks
may still be given for some cases if the rest of the explanation is correct.

(c) (3 points) Now consider the code below. The difference from Part (a) is that class A is declared as
final .
final class A {
}

@FunctionalInterface
interface Producer {
int produce();
}

and the following snippet:


A a = new A();
Producer p = (Producer) a;

Explain why the Java compiler produces a compilation error. You may contrast this to your answer in
Part (a) of this question.

Page 16
Final Assessment CS2030S AY22/23 Sem 2

Solution: It is not possible that during run-time, a is an instance of a class that extends from
A , as A is declared as final. So the run-time type of a must be A and A does not implement
Producer .

• 3 marks for correct explanation that (i) A cannot be extended due to final , (ii) therefore
a must be have a run-time type of A , and since (iii) A ̸<: Producer<T> , (iv) it not possible
for run-time type of a to be a subtype of Producer<T> .
• 0 mark otherwise.

Note that (i) follows trivially from A being final and (iii) A ̸<: Producer follows trivially from
the code. Mentioning (i) and (iii) only is not sufficient.
About 9% got this correct. Common mistakes include

• Stopping only at the description of final and/or type relationship between A , Producer
and potential subtypes of A without talking about RTT of a .

• Confusing final keyword usage for classes with that of variables, talking about “immutabil-
ity” and “no reassignment/instantiation”.
• Assuming that explicit type cast implies a narrowing type conversion (talking about this is
not relevant anyway). This often results in students talking about how Producer ̸<: A
which does not make sense.

• Some repeat of common mistakes in 16a and 16b.

Page 17
Final Assessment CS2030S AY22/23 Sem 2

17. (6 points) Recall that the cs2030s.fp package includes the following Transformer and Combiner func-
tional interfaces.
@FunctionalInterface
interface Transformer<T, R> {
R transform(T t);
}

@FunctionalInterface
interface Combiner<S, T, R> {
R combine(S s, T t);
}

We want to create the class called Curry with two static methods. In the sample runs below, we have the
following code fragments.
Combiner<Integer, Integer, Integer> f1 = (x, y) -> x - y;
Transformer<Integer, Transformer<Integer, Integer>> f2 = x -> y -> x - y;

Answer the question by filling in the corresponding blanks on the answer sheet.
class Curry {
public static <S, T, R> __BLANK1__ curry(__BLANK2__ f) {
return __BLANK3__;
}
public static <S, T, R> __BLANK4__ uncurry(__BLANK5__ f) {
return __BLANK6__;
}
}
(a) (3 points) Implement a static method curry that takes in a Combiner and returns a Transformer
satisfying the following sample run.
jshell> f1.combine(2,1)
$.. ==> 1
jshell> Curry.curry(f1).transform(2).transform(1)
$.. ==> 1
(b) (3 points) Implement a static method uncurry that takes in a Transformer and returns a Combiner
satisfying the following sample run.
jshell> f2.transform(2).transform(1)
$.. ==> 1
jshell> Curry.uncurry(f2).combine(2,1)
$.. ==> 1

Solution:

class Curry {
public static <S,T,R> Transformer<S, Transformer<T, R>> curry(Combiner<S, T, R> f) {
return x -> y -> f.combine(x, y);
}
public static <S,T,R> Combiner<S,T,R> uncurry(Transformer<S, Transformer<T, R>> f) {
return (x, y) -> f.transform(x).transform(y);
}
}

Note that PECS is not needed if we let only type inference. We allow three alternatives for BLANK1 / BLANK5
and two alternatives for BLANK2 / BLANK4 , with PECS considered (left as an exercise to the reader).

• -1 mark for incorrect return type


• -1 mark for incorrect method parameter

Page 18
Final Assessment CS2030S AY22/23 Sem 2

• -1 mark for incorrect lambda

Some mistakes that give immediate 0:

• Did not use type parameter S , T , and R .


• Did not curry/uncurry the function.

Roughly 55% of students received full marks for this question.

Page 19
Final Assessment CS2030S AY22/23 Sem 2

18. (15 points) Consider the following implementation of InfiniteList , taken from the notes:
class InfiniteList<T> {
public final Producer<T> head;
public final Producer<InfiniteList<T>> tail;

private InfiniteList(Producer<T> head, Producer<InfiniteList<T>> tail) {


this.head = head;
this.tail = tail;
}

public static <T> InfiniteList<T> iterate(T seed, Transformer<T, T> next) {


return new InfiniteList<T>(
() -> seed,
() -> InfiniteList.iterate(next.transform(seed), next)

}

public T head() {
T h = this.head.produce();
return h == null ? this.tail.produce().head() : h;
}

public InfiniteList<T> tail() {


T h = this.head.produce();
return h == null ? this.tail.produce().tail() : this.tail.produce();
}

public InfiniteList<T> filter(BooleanCondition<? super T> cond) {


return new InfiniteList<T>(
() -> cond.test(this.head()) ? this.head() : null,
() -> this.tail().filter(cond)

}
}

The following functional interfaces are used:


@FunctionalInterface
interface Producer<T> {
T produce();
}

@FunctionalInterface
interface BooleanCondition<T> {
boolean test(T t);
}

@FunctionalInterface
interface Transformer<T, R> {
R transform(T t);
}

Page 20
Final Assessment CS2030S AY22/23 Sem 2

(a) (4 points) Implement a method called lazyGet inside the InfiniteList class that takes in an int
index i and lazily returns the i-th element from the list as a producer of the element.
The method is declared as:
public Producer<T> lazyGet(int i) {

}
and can be used as follows:
jshell> Transformer<Integer, Integer> f = x -> {
...> System.out.println(" > " + (x + 2));
...> return x + 2;
...> }
jshell> Producer<Integer> p = InfiniteList.iterate(0, f).lazyGet(0)
jshell> p.produce()
$.. ==> 0
jshell> Producer<Integer> p = InfiniteList.iterate(0, f).lazyGet(2)
jshell> p.produce()
> 2
> 4
$.. ==> 4
Fill in the body of the method lazyGet .

Solution:

public Producer<T> lazyGet(int i) {


return () -> i == 0 ? this.head() : this.tail().lazyGet(i - 1).produce();
}

The corresponding iterative solution is accepted as well.

• When i == 0
– 2 marks for () -> head()
– 1 mark is given if the code returns either head or () -> head.produce() instead,
as this might return a producer that produces null .
– 0 mark for () -> head , as this returns Producer<Producler<T>> .

• When i > 0 ,
– 2 marks for () -> tail().lazyGet(i-1).produce())
– 1 mark for () -> tail.produce().lazyGet(i-1).produce() as this includes filtered
elements when counting down from i .
– 0 mark for () -> tail().lazyGet(i-1) , as this returns Producer<Producer<T>> .
– Other common wrong combinations (0 marks) include () -> tail.lazyGet(i-1) ,
() -> tail.produce().lazyGet(i-1) , etc.

Any eager-evaluation solution receives 0.


Only 101 (13%) students received 4 marks for this question.

(b) (6 points) Write a method called skip inside the InfiniteList class with a parameter x of type
T . The method skips all beginning elements that are equal to x and returns the remaining elements
as an infinite list. Checking for equality should be done using the equals method.
For instance, calling skip(0) on the infinite list 0, 0, 0, 1, 1, 1, 2, 2, 2, . . . returns the infinite list 1, 1, 1,
2, 2, 2 . . .. Calling skip(1) on the infinite list 0, 0, 0, 1, 1, 1, 2, 2, 2, . . . returns 0, 0, 0, 1, 1, 1, 2, 2, 2, . . ..
The method skip has the following declaration and should be executed eagerly.

Page 21
Final Assessment CS2030S AY22/23 Sem 2

private InfiniteList<T> skip(T x) {

}
Fill in the body of the method skip .

Solution:

private InfiniteList<T> skip(T x) {


return this.head().equals(x) ? this.tail().skip(x) : this;
}

• When 1st element is x .

– 3 marks for tail().skip() , or


– 3 marks for tail.produce().skip(x) , or
– 1 mark for tail.skip(x) .
– Other solution that removes all x from the list, and leave x intact at the head of the
list, etc., are not given any marks.

• When 1st element is not x .


– 3 marks for this , or
– 3 marks for new InfiniteList<T>(head, tail) , or
– 3 marks for new InfiniteList<T>(() -> head(), () -> tail()) .
– -1 mark if a raw type is used
– A common mistake is to return new InfiniteList<T>(head, () -> tail().skip(x)) ,
which would skip x in the rest of the list.
• -1 mark for an answer that compares using == .
• -1 mark for an answer that compares the producer head with x

266 (35%) students received full marks for this part.

(c) (5 points) Using the method skip above, write a method called unique inside the InfiniteList
class that lazily returns a new infinite list where consecutive elements that are the same are removed.
For instance, calling unique() on the infinite list 0, 0, 1, 1, 1, 0, 0, 1, 1, 2, 2, . . ., returns 0, 1, 0, 1, 2, . . ..
The method unique has the following declaration and should be executed lazily.
public InfiniteList<T> unique() {

}
Fill in the body of the method unique .

Solution:

public InfiniteList<T> unique() {


return new InfiniteList<>(
() -> this.head(),
() -> this.skip(this.head()).unique());
}

• 1 marks for the first argument. Either head or () -> head() receive 1 marks. () -> head
receives 0.
• 4 marks for the second argument. 4 marks are given for

Page 22
Final Assessment CS2030S AY22/23 Sem 2

– () -> skip(head()).unique()
– () -> tail().skip(head()).unique()
– () -> tail.produce().skip(head()).unique()
– () -> tail().unique().skip(head())
– () -> tail.produce().unique().skip(head())

Calling tail.skip(..) leads to a 1-mark deduction. Skipping with skip(head) or skip(head.produce())


leads to a 1-mark deduction.

• -1 for use of raw types

Any eagerly evaluated solution receives 0.


Note that new InfiniteList<T>(head, ()->tail().skip(head()).unique()) is not correct,
as it might lead to infinite recursion, but is accepted as correct anyway.
Only 144 (19%) students received 5 marks for this question.

Page 23
Final Assessment CS2030S AY22/23 Sem 2

19. (6 points) Consider a monad called Monad<X> that adds a context to the computation on a value of type
X . We have the following methods em , fm , gm , and hm that take in a value of type X and return a
Monad<X> . We wish to apply the four methods, in order, to a value x , like so:

Monad<X> result = Monad.of(x)


.flatMap(i -> em(i))
.flatMap(i -> fm(i))
.flatMap(i -> gm(i))
.flatMap(i -> hm(i));

When you asked an AI to write a program snippet to chain the methods together, the following program is
generated:
Monad<X> efm(X x) {
return Monad.of(x)
.flatMap(i -> em(i));
.flatMap(i -> fm(i));
}

Monad<X> ghm(X x) {
return Monad.of(x)
.flatMap(i -> gm(i))
.flatMap(i -> hm(i))
}

Monad<X> result = Monad.of(x)


.flatMap(i -> efm(i))
.flatMap(i -> ghm(i));

The AI insists that the two programs are equivalent but refuses to prove it to you. So you set out to prove it
yourselves. Fill in the blanks below to show that the two expressions are equivalent:
We start with:
Monad.of(x)
.flatMap(i -> efm(i))
.flatMap(i -> ghm(i))

Following the AI’s implementation, we have


Monad.of(x)
.flatMap(i -> Monad.of(i).flatMap(i -> em(i)).flatMap(i -> fm(i)))
.flatMap(i -> Monad.of(i).flatMap(i -> gm(i)).flatMap(i -> hm(i)))

Applying Monad’s __BLANK7__ law, we have


Monad.of(x)
.flatMap(i -> ____BLANK8____)
.flatMap(i -> ____BLANK9____)

Applying Monad’s __BLANK10__ law, we have


Monad.of(x)
.flatMap(i -> em(i)).flatMap(i -> fm(i))
.flatMap(i -> gm(i)).flatMap(i -> hm(i))

And so, the AI is correct and the two programs are equivalent.

Solution:

Monad.of(x)
.flatMap(i -> Monad.of(i).flatMap(i -> em(i)).flatMap(i -> fm(i)))
.flatMap(i -> Monad.of(i).flatMap(i -> gm(i)).flatMap(i -> hm(i)))

Page 24
Final Assessment CS2030S AY22/23 Sem 2

=> by left identity


Monad.of(x)
.flatMap(i -> em(i).flatMap(i -> fm(i)))
.flatMap(i -> gm(i).flatMap(i -> hm(i)))
=> by associativity
Monad.of(x)
.flatMap(i -> em(i)).flatMap(i -> fm(i))
.flatMap(i -> gm(i)).flatMap(i -> hm(i))

We allow working downwards/upwards.

• 2 marks for BLANK7 assuming correct application (see below).

• 2 marks for BLANK10 assuming correct application (see below).


• 1 mark each for BLANK8 and BLANK9 .

Each blank must be a single expression with some minor syntactic error allowed:

• We do not accept expressions where misplaced parenthesis makes it ambiguous in case of asso-
ciativity or other parentheses’ precedence.
• We do not accept non-expression such as below:
– Monad.of(i).flatMap(i -> em(i))).flatMap(i -> fm(i)
– It does not into the blank as it closes that already given parentheses prematurely and ex-
tends it with additional flatMap .

352 (47%) students received 6 marks for this question.

Page 25
Final Assessment CS2030S AY22/23 Sem 2

Stream API (Abridged)


filter

Stream<T> filter(Predicate<? super T> predicate)

Returns a stream consisting of the elements of this stream that match the given predicate.

map

<R> Stream<R> map(Function<? super T,? extends R> mapper)

Returns a stream consisting of the results of applying the given function to the elements of this stream.

flatMap

<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

Returns a stream consisting of the results of replacing each element of this stream with the contents of a mapped stream
produced by applying the provided mapping function to each element. Each mapped stream is closed after its contents
have been placed into this stream. (If a mapped stream is null an empty stream is used, instead.)

limit

Stream<T> limit(long maxSize)

Returns a stream consisting of the elements of this stream, truncated to be no longer than maxSize in length.

forEach

void forEach(Consumer<? super T> action)

Performs an action for each element of this stream.

reduce

T reduce(T identity, BinaryOperator<T> accumulator)

Performs a reduction on the elements of this stream, using the provided identity value and an associative accumulation
function, and returns the reduced value.

of

static <T> Stream<T> of(T... values)

Returns a sequential ordered stream whose elements are the specified values.

iterate

static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)

Returns an infinite sequential ordered Stream produced by iterative application of a function f to an initial element
seed, producing a Stream consisting of seed, f(seed), f(f(seed)), etc.

END OF PAPER

Page 26

You might also like