Declaratively / Imperative

Imperative: You tell the computer how do it step by step and change state along the way. Functional: You tell the computer what you want, without modifying existing objects. Instead of changing something in place, you return a new thing.

// Imperative — mutates the original
public void set(T t) { this.t = t; }

// Functional — returns a new MCell, original untouched
public MCell<T> withValue(T t) { return new MCell<>(t); }

In this MCell class, set, swap, copy_to, copy_from are all imperative — they mutate state. The recitation is going to show you how to replace them with functional alternatives that return new values instead of modifying existing ones.

The current state is not prepared for empty cells.


Making an empty box, to make it like Optional

  1. Create a brand new empty box using a public method
// static factory method
// factory: a static method that creates and returns an object
public static <T> MCell<T> empty() {
	return new MCell<>(null);
}
  1. Take an existing box and empty it
// side-effecting instance method (not recommended)
public void nullify() {
	this.t = null;
}


Pure functions always return the same output for the same input. This makes them easier to test, compose, and reason about formally. The default mindset is shifting toward immutability.


Existing same-value Here, you could compare MCell<Integer> with an MCell<String> and the compiler won’t complain. It’ll just return a false.

New same_value This version stops you from comparing unrelated types in the first place. <? extends T> ensures that they’re always compatible for comparison.


“equals retains the element type T in its signature” → MCell<? extends T> keeps type info, so compiler can check compatibility. “same_value erases the element type entirely” → MCell<?> throws away type info, so compiler can’t catch mismatches.

So even though they are already compatible, by right equals is stronger.


(a)

public MCell<T> copy() {
	return new MCell<T>(T);
}

Use the make new function .of and obtain the value using .get()

(b)

public static MCell<Integer> inc(MCell<Integer> m) {
	MCell<Integer> inc = m + 1;
	return new MCell<Integer> inc;
}

Do empty check.
m is a box. You can't add 1.
You need to unwrap it using m.get()

(c)

public static MCell<Integer> add(MCell<Integer> m1, MCell<Integer> m2) {
	MCell<Integer> sum = m1 + m2;
	return new MCell<Integer> sum
}

Do empty check.
Likewise, both m1 and m2 are boxes. You need to unwrap them.

Note

  1. If MCell is immutable (no side-effecting methods), copy() can just return this instead of creating a new object. Since nothing can change the original, sharing the same reference is safe. That’s a nice optimization insight — immutability gives you shortcuts.
  • It should not create a new object, and just return this (the same object with 2 references now)
  1. Empty propagates, not defaults to zero. add(empty, of(5)) returns empty, not of(5). This is important — treating empty as zero would silently give wrong answers. Empty means “I don’t have a value,” which is fundamentally different from “I have the value 0.”
  • That second point is the same logic behind Optional and null safety in general. Absence ≠ zero.
  • Think about it practically. If I say “add your bank balance to my bank balance” but I don’t know your balance — the answer isn’t my balance. The answer is “I don’t know.” The absence infects the whole computation.

For context, apply is the method inside Function. Similar to how compare is inside Comparator.

map’s Design.

public <R> MCell<R> map(Function<? super T, ? extends R> fn) {
// this is defined for MCell, not lists

	if (this.get() == null) return MCell.empty();
	// check if this is null
	
	return MCell.of(fn.apply(this.get())); 
	// wrap to a new box
}

// Because map is called on cell, so "this" becomes cell.
// this always refers to the object before the dot.

// If i wanted to map over a whole list

List<MCell<Integer>> results = cells.stream()
    .map(cell -> cell.map(x -> x + 1))
    .collect(Collectors.toList());
    
Two different maps here: the stream's map iterates over the list, and each cell's map transforms the value inside that cell.

flatMap was designed for “what if my function already returns a box?”

public <R> MCell<R> flatMap(Function<? super T, ? extends MCell<R>> fn) {
	if (this.get() == null) return MCell.empty();
	return fn.apply(this.get());
}

Function<T, R> became Function<? super T, ? extends R>?

Just for more flexibility.

? super T: the function accepts T or anything above T
If it can handle Object, it can definitely handle a String

? extends R: the function returns R or something below R
If it returns Orange and you expect Fruit, that's okay

Reimplementing

(a)

public MCell<T> copy() {
	return new MCell<T>(T);
}

Use the make new function .of and obtain the value using .get()

// right method
public MCell<T> copy() {
	return MCell.of(this.get());
}

// right method with map
public MCell<T> copy() { 
	return this.map(x -> x); 
}

(b)

public static MCell<Integer> inc(MCell<Integer> m) {
	MCell<Integer> inc = m + 1;
	return new MCell<Integer> inc;
}

Do empty check.
m is a box. You can't add 1.
You need to unwrap it using m.get()

// right method
public static MCell<Integer> inc(MCell<Integer> m) {
	if (m.get() == null) return MCell.empty();
	return MCell.of(m.get() + 1);
}

// right method with map
public static MCell<Integer> inc(MCell<Integer> m) {
	return m.map(x -> x + 1);
}

(c)

public static MCell<Integer> add(MCell<Integer> m1, MCell<Integer> m2) {
	MCell<Integer> sum = m1 + m2;
	return new MCell<Integer> sum
}

Do empty check.
Likewise, both m1 and m2 are boxes. You need to unwrap them.

// right method
public static MCell<Integer> add(MCell<Integer> m1, MCell<Integer> m2) {
	if (m1.get() == null || m2.get() == null) return 
	MCell.empty();
	return MCell.of(m1.get() + m2.get());
}

// right method with map
public static MCell<Integer> add(MCell<Integer> m1, MCell<Integer> m2) {
	return m1.flatMap(x -> m2.map(y -> x + y));
}