Unit 31: Box and Maybe
Learning Objectives
Students should
- appreciate the generality of the class
Box<T>
andMaybe<T>
- appreciate how passing in functions as parameter can lead to highly general abstractions
- appreciate how
Maybe<T>
preserves the "maybe null" semantics over a reference type by internalizing checks fornull
Lambda as a Cross-Barrier State Manipulator
Recall that every class has an abstraction barrier between the client and the implementer. The internal states of the class are heavily protected and hidden. The implementer selectively provides a set of methods to access and manipulate the internal states of instances. This approach allows the implementer to control what the client can and cannot do to the internal states. This is good if we want to build abstractions over specific entities such as shapes or data structures such as a stack, but it is not flexible enough to build general abstraction.
Let's consider the following class:
1 2 3 |
|
It is a box containing a single item of type T
. Suppose that we want to keep the item
hidden and we want to have certain rules and maintain some semantics about the use of the item
. As such, we don't want to provide any setter or getter, so that the client may not break our rules. What are some ways we can still operate on this item
?
The only way we can do this is to provide methods that accept a lambda expression, apply the lambda expression on the item, and return the new box with the new value. For instance,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
The method map
takes in a lambda expression and allows us to arbitrarily apply a function to the item, while the method filter
allows us to perform an arbitrary check on the property of the item.
Methods such as these, which accept a function as a parameter, allows the client to manipulate the data behind the abstraction barrier without knowing the internals of the object. Here, we are treating lambda expressions as "manipulators" that we can pass in behind the abstraction barrier and modify the internals arbitrarily for us, while the container or the box tries to maintain the semantics for us.
Maybe
Let's now look at Box<T>
in a slightly different light. Let's rename it to Maybe<T>
. Maybe<T>
is an option type, a common abstraction in programming languages (java.util.Optional
in Java, option
in Scala, Maybe
in Haskell, Nullable<T>
in C#, etc) that is a wrapper around a value that is either there or is null
. The Maybe<T>
abstraction allows us to write code without mostly not worrying about the possibility that our value is missing. When we call map
on a value that is missing, nothing happens.
Recall that we wish to write a program that is as close to pure mathematical functions as possible, a mathematical function always has a well-defined domain and codomain.
Recap that in Lab 3, we had the following code in Network.java
:
1 2 3 4 5 6 7 8 |
|
This comes from the fact that our method Agent::act
may return a null
value. In other words, act
is no longer a mapping from Buffer
to Agent
as in this case, there is a possibility that it returns a null
which we do not consider to be an Agent
. Part of the reason why null
is not an Agent
is that we cannot perform an act
on a null
value. This is also why we do not insert null
into the queue.
This violation of the puriting of the function adds complication to our code. We have to add the check for null
value. One way to fix this is to for Agent::act
to be a mapping from Buffer
to Maybe<Agent>
. In such cases, we will no longer return a null
value but we may actually return an instance of run-time type None<Agent>
(the compile-time type will always be Maybe<Agent>
). If we make such changes, we can replace the code above with:
1 2 3 4 5 6 |
|
With this design, Agent::act
is now a function with the domain Buffer
mapped to the codomain Maybe<Agent>
, and it is pure.
Another way to view the Maybe<T>
class is that it internalizes all the checks for null
on the client's behalf. Maybe<T>
ensures that if null
represents a missing value, then the semantics of this missing value is preserved throughout the chain of map
and filter
operations. Within its implementation, Maybe<T>
do the right thing when the value is missing to prevent us from encountering NullPointerException
. There is a check for null
when needed, internally, within Maybe<T>
. This internalization removes the burden of checking for null
on the programmer and removes the possibility of run-time crashes due to missing null
checks.