Skip to content

Functional Tropes

Brian Burton edited this page May 22, 2023 · 3 revisions

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.

Maybe

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 Maybes 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 in Serializable 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 equivalent NotNull object.
  • nullToEmpty() method returns an empty Maybe if its value is null.

Maybe has three static factory methods:

  • Maybe.empty() returns an empty Maybe. All empty Maybe values share a single common instance to reduce memory footprint.
  • Maybe.of(value) returns a full Maybe. The resulting Maybe is always full even if value is null.
  • Maybe.cast(class, value) returns a Maybe representing an attempt to cast the value to an instance of the given class. An empty Maybe is returned if the value cannot be cast into the given class safely. A null value will result in a full Maybe.

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);

NotNull

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 NotNulls. 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 empty NotNull. All empty NotNull values share a single common instance to reduce memory footprint.
  • NotNull.of(value) returns a full NotNull if value is not null. Otherwise it returns an empty NotNull.
  • NotNull.cast(class, value) returns a NotNull representing an attempt to cast the value to an instance of the given class. An empty NotNull is returned if the value cannot be cast into the given class safely. A null value will also result in an empty NotNull.

Result

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.

Computation

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.

Clone this wiki locally