Exercise 5 - Maybe
What is Maybe<T>
Let's take the counter as an example.
Counter c = bank.findCounter();The findCounter() function is supposed to map from the bank (domain) to an available counter (codomain should be Counter!). However, if there is not available counter, it will return a null, which is not a Counter! This means that findCounter() is not a pure function!
To solve that, we can create a new Type called Maybe<Counter>, which basically extends the codomain to include the null. Thus, the findCounter() is now a pure function!
For more about what on earth is a maybe, read the Lab 05!
So, Maybe<T> is a wrapper for an item that may or may not exist. Its key purpose is to handle the possibility of missing values in a more elegant way than using null references.
And inside the Maybe<T>, it has two cases
case
None: do nothiing, which represents the absence of a valuecase
Some: actually do the thing, which represents the presence of a value (call benull)
These two cases are the class internals and are invisible to the users!
Main methods explanation
get()
// Return the wrapped value
// Or Maybe.NONE if
// 1. It is invoked from Maybe.NONE
protected abstract T get();Implementation in each case
@Override
protected Object get() {
throw new NoSuchElementException();
}@Override
protected T get() {
return this.content;
}filter(BooleanCondition<? super T> c)
// Returns Maybe.none() if
// 1. it is invoked from Maybe.NONE
// 2. the content is null
// 3. the content didn't pass the test
// Otherwise, return the Maybe(Wrapper) itself
public abstract Maybe<T> filter(BooleanCondition<? super T> c);Implementation in each case
@Override
public Maybe<Object> filter(BooleanCondition<? super Object> c) {
return Maybe.none();
} @Override
public Maybe<T> filter(BooleanCondition<? super T> c) {
if (this.content != null && !c.test(this.content)) {
return Maybe.none();
}
return this;
}map(Transformer<? super T, ? extends U> t)
// Return Maybe.none() if
// 1. It is invoked from Maybe.NONE
// Otherwise, return the wrapper of the value after transformation
public abstract <U> Maybe<U> map(Transformer<? super T, ? extends U> t);Implementation in each case
@Override
public <U> Maybe<U> map(Transformer<? super Object, ? extends U> t) {
return Maybe.none();
}@Override
public <U> Maybe<U> map(Transformer<? super T, ? extends U> t) {
return Maybe.some(t.transform(this.content));
}Application
You can just think of
map(),orElse()/orElseGet()as an if-else statement. It works like if the target isMaybe.some()then do the function defined in the map, else, do the function insideorElseGet(). Finally, it will return the value inside the Maybe.This is very important! You can chain this and organize just like if-else blocks!!! See more from the
takeWhile()in Exercise 7!
flatMap(Transformer<? super T, ? extends Maybe<? extends U>> t)
// Return Maybe.none() if
// 1. it is invoked from Maybe.NONE
// Otherwise, return value after transformation (in this case we explicitly specify it as a Wrapper)
public abstract <U> Maybe<U> flatMap(Transformer<? super T, ? extends Maybe<? extends U>> t);Implementation in each case
@Override
public <U> Maybe<U> flatMap(Transformer<? super Object, ? extends Maybe<? extends U>> t) {
return Maybe.none();
} @Override
public <U> Maybe<U> flatMap(Transformer<? super T, ? extends Maybe<? extends U>> t) {
@SuppressWarnings("unchecked")
Maybe<U> m = (Maybe<U>) t.transform(this.content);
return m;
}Application
flatMap()can technically achieve everything thatmap()can achieve. But the reverse is not true because map always add Wrapper after transforming.In
flatMap(), the transformer will always output aMaybeorLazyor whatever class it is under. (See Optional::flatMap in Java)
orElse(T t)
// Return the alternative value t provided as parameter, if
// 1. it is invoked from Maybe.NONE
// Otherwise, return the content that is in the Wrapper
public abstract T orElse(T t);Implementation in each case
@Override
public Object orElse(Object o) {
return o;
}@Override
public T orElse(T t) {
return this.content;
}Application
Remember that this method will always give you the either content of the wrapper (See Lab Sheet Q3) or the parameter. No wrapper is returned!
This is usually used at the last of your function chain because its return type may not be a Maybe, so we cannot chain anymore!
orElseGet(Producer<? extends T> p)
// Return the value produced by calling produce() on the provided Producer, if
// 1. It is invoked from Maybe.NONE
// Otherwise, return the content that is in the Wrapper
public abstract T orElseGet(Producer<? extends T> p);Implementation in each case
@Override
public Object orElseGet(Producer<? extends Object> p) {
return p.produce();
}@Override
public T orElseGet(Producer<? extends T> p) {
return this.content;
}ifPresent(Consumer<? super T> c)
// Do nothing and return, if
// 1. it is invoked from Maybe.NONE
// Otherwise, do something by executing the consumer's consume() method with the content as argument
public abstract void ifPresent(Consumer<? super T> c);Implementation in each case
@Override
public void ifPresent(Consumer<? super Object> c) {
return;
}@Override
public void ifPresent(Consumer<? super T> c) {
c.consume(this.content);
}Application
This is usually used to replace the
(... ≠null)check. See Lab Sheet Q2.It is used to consume the value in the Maybe wrapper, in Infinite List, it can be used to implement the
forEach()method.
of()
// Factory method used to create a Maybe(Wrapper)
public static <T> Maybe<T> of(T t) {
if (t == null) {
return Maybe.none();
}
return Maybe.some(t);
}Implementation in each case
This method will call the factory method of None and Some<T>
public static <T> Maybe<T> none() {
@SuppressWarnings("unchecked")
Maybe<T> none = (Maybe<T>) NONE;
return none;
}public static <T> Maybe<T> some(T t) {
return new Some<T>(t);
}Single return statement
The major purpose of using functional programming in this course is to let you rewrite methods into one single statment. So, how we can use the basic idea of FP to understand / read a single return statement?
The fundamental idea of FP: In FP, functions are treated as the first-class citizen.
Line 1,
Maybe.ofis actually creating the argument that will be passed all the way down.Line 2,
.flatMap()takes in a lambda as an expression and itself (.flatMap()) is a function that we are going to apply on the target. This function will return another "mutated" instance for further operation. The return type of the function is defined in the declaration.Inside the lambda expression, it explicitly sets the
Transformer t.How the transformer or a.k.a the parameter works is that the input is the L.H.S, which in CS2030S's Maybe should be the value in the previous wrapper. The output of the lambda is the R.H.S
And how the output is being processed is dependent on the outter method, e.g.
flatMap()will just return the output,map()will add a wrapper around the output.filter()will use the output to implement the checking, etc.
Line 3 and 4 is similar.
You can think the Line 1 as creating a naked man and Line 2,3,4 are actually adding layers to the man (From my tutor 😂)
Tips
In 2030s's Maybe, for every lambda expression passed into the API of maybe, the L.H.S is always the content of the previous Maybe
General advice in writing one-line code
Start by considering the condition to use (In the Exercise 7 — Infinite List, we always start with
this.head.get().End by using
orElse()/orElseGet().Anything between the start and the end is your if branch.
The "placeholder" in your end is the else branch.
Last updated