-
Notifications
You must be signed in to change notification settings - Fork 4
Functional Tropes
Javimmutable is not a functional programming library but its collections fit naturally into that style of programming. To further support that style the library provides some monadic classes:
- Maybe: A replacement for Java's
Optional
class that also supports null as a value. - NotNull: Another replacement for Java's
Optional
class and ensures it never contains null. - Result: Similar to
Maybe
but can contain either a value or an exception. - Computation: A deferred computation class that allows a computation to be composed in steps and evaluated later.
This page outlines what these classes are and what they do. For motivation on why these classes can be useful read a good book on functional programming.
Collections have a find()
method that returns a Maybe
rather than a value. The Maybe
either contains a value or is empty. Methods are provided to map the value to make new Maybe
s as well as to safely extract the value or an alternative.
While Maybe
is similar to Optional
it has a number of advantages:
- It's
Serializable
so you can use it as a field inSerializable
objects. - It doesn't mind storing null so it won't turn itself into an empty if you map its value to null.
- It has more options for mapping the value.
Though Maybe
is fine with storing null it does provide two ways to eliminate null values.
-
notNull()
method returns an equivalentNotNull
object. -
nullToEmpty()
method returns an emptyMaybe
if its value is null.
Maybe
has three static factory methods:
-
Maybe.empty()
returns an emptyMaybe
. All emptyMaybe
values share a single common instance to reduce memory footprint. -
Maybe.of(value)
returns a fullMaybe
. The resultingMaybe
is always full even ifvalue
isnull
. -
Maybe.cast(class, value)
returns aMaybe
representing an attempt to cast thevalue
to an instance of the given class. An emptyMaybe
is returned if thevalue
cannot be cast into the given class safely. Anull
value will result in a fullMaybe
.
Suppose you have an IMap
containing all environemnt variables. You want to take the value of one of them (if it exists), parse it as an integer, and use it, if it exists, or a default, if it does not. This example shows how all of this could be done:
IMap<String,String> envVars = IMaps.hashed(System.getenv());
int port = envVars.find("PORT").map(Integer::parseInt).get(80);
Sometimes you really don't want null
as a value. In that case NotNull
has you covered. It always ensures that null
values are mapped to empty NotNull
s. If you construct an instance using null
as its value you'll get an empty NotNull
. Likewise if any map
or flatMap
function produces a null
value the result will be an empty NotNull
.
You can convert a NotNull
into an equivalent Maybe
by calling the maybe()
method.
NotNull
has three static factory methods:
-
NotNull.empty()
returns an emptyNotNull
. All emptyNotNull
values share a single common instance to reduce memory footprint. -
NotNull.of(value)
returns a fullNotNull
ifvalue
is not null. Otherwise it returns an emptyNotNull
. -
NotNull.cast(class, value)
returns aNotNull
representing an attempt to cast thevalue
to an instance of the given class. An emptyNotNull
is returned if thevalue
cannot be cast into the given class safely. Anull
value will also result in an emptyNotNull
.
Frequently we need to make multiple method calls to accomplish a task. If any of them can trigger an exception that should halt the computation it can be cumbersome to express that. Also there are advantages to capturing an exception into the return value of a method call rather than throw an exception that must be caught higher in the stack.
The Result
class makes doing this a simple process. Result
is an object that holds either a computed value or an exception. Return values have some advantages in that they can be passed around like any other value. Also Result
offers map
and flatMap
methods that make it easy to compose operations into a final result.
For example suppose you have three methods to call to compute a result.
public String callWebService(String host, int port) throws IOException;
public IList<Address> extractHouseAddresses(String webServiceResult) throws ParseException;
public IList<BigDecimal> lookupHouseValues(IList<Address> houseAddresses) throws DatabaseException;
Result<BigDecimal> totalValue = Result.attempt(() -> callWebService("some-host", 443))
.map(resultJson -> extractHouseAddresses(resultJson))
.map(addresses -> lookupHouseValues(addresses))
.map(values -> values.reduce(BigDecimal.ZERO, BigDecimal::add));
If any of the calls throws an exception the calculation will stop and the final Result
will contain the exception. If all of them succeed the final Result
will contain the total value of all of the homes. While this is a contrived example it still serves to demonstrate how the methods of Result
can be applied.
A Computation
holds a sequence of method calls that can be executed by calling its call()
or compute()
methods. Computation
implements Callable
so it can be submitted to an ExecutorService
or run on a Thread
if desired. Like Result
they provide map
and flatMap
methods that make them easy to compose.
The call()
method performs the computation and then either returns its computed value or throws any exception thrown during the process. The compute()
method performs the computation and returns a Result
object containing the computed value or an exception if the computation failed.