DEV Community

Sergiy Yevtushenko
Sergiy Yevtushenko

Posted on • Updated on

Introduction to Pragmatic Functional Java

UPDATE: added important note about default initialization.

The Pragmatic Functional Java (PFJ) is an attempt to define a new idiomatic Java coding style. Coding style, which will completely utilize all features of current and upcoming Java versions. Coding style, which will involve compiler to help writing concise yet reliable and readable code.

While this style can be used even with Java 8, with Java 11 it looks much cleaner and concise. It gets even more expressive with Java 17 and benefits from every new Java language feature.

But PFJ is not a free lunch, it requires significant changes in developers' habits and approaches. Changing habits is not easy, traditional imperative ones are especially hard to tackle.

Is it worth it? Definitely! PFJ code is concise, expressive and reliable, easy to read and maintain. In most cases, if code compiles - it works!

(This text is an integral part of the Pragmatica library).

Elements Of Pragmatic Functional Java

PFJ is derived from wonderful Effective Java book with
some additional concepts and conventions, in particular, derived from Functional Programming.

Note that despite use of FP concepts, PFJ does not try to enforce FP-specific terminology. (Although references are
provided for those who is interested to explore those concepts further).

PFJ focuses on:

  • reducing mental overhead
  • improving code reliability
  • improving long-term maintainability
  • involving compiler to help write correct code
  • making writing correct code easy and natural; writing incorrect code, while still possible, should require efforts

Despite ambitious goals, there are only two key PFJ rules:

  • Avoid null as much as possible
  • No business exceptions

Below, each key rule is explored in more details:

Avoid null As Much As Possible (ANAMAP rule)

Nullability of variables is one of the Special States.
They are a well-known source of run-time errors and boilerplate code. To eliminate these issues and represent values which can be missing, PFJ uses Option container. This covers all cases when such a value may appear - return values, input parameters or fields.

In some cases, for example for performance or compatibility with existing frameworks reasons, classes may use null internally. These cases must be clearly documented and invisible to class users, i.e., all class APIs should use Option<T>.

This approach has several advantages:

  • Nullable variables are immediately visible in code. No need to read documentation/check source code/rely on annotations.
  • Compiler distinguishes nullable and non-nullable variables and prevents incorrect assignments between them.
  • All boilerplate necessary for null checks is eliminated.

Important component of the ANAMAP rule:

  • No default initialization. Every single variable should be explicitly initialized. There are two reasons for this: preserving context and elimination of null values.

No Business Exceptions (NBE rule)

PFJ uses exceptions only to represent cases of fatal, unrecoverable (technical) failures. Such an exception might be intercepted only for purposes of logging and/or graceful shutdown of the application. All other exceptions and their interception are discouraged and avoided as much as possible.

Business exceptions are another case of Special States.
For propagation and handling of business level errors, PFJ uses Result container.

Again, this covers all cases when error may appear - return values, input parameters or fields. Practice shows that fields rarely (if ever) need to use this container.

There are no justified cases when business level exceptions can be used. Interfacing with existing Java libraries and legacy code performed via dedicated wrapping methods. The Result container contains an implementation of these wrapping methods.

The No Business Exceptions rule provides the following advantages:

  • Methods which can return error are immediately visible in code. No need to read documentation/check source code/analyze call tree to check which exceptions can be thrown and under which conditions.
  • Compiler enforces proper error handling and propagation.
  • Virtually zero boilerplate for error handling and propagation.
  • Code can be written for happy day scenario and errors handled at the point where this is most convenient - original intent of exceptions, which was never actually achieved.
  • Code remains composable, easy to read and reason about, no hidden breaks or unexpected transitions in the execution flow - what you read is what will be executed.

Transforming Legacy Code Into PFJ Style Code

OK, key rules seems looking good and useful, but how real code will look like?

Let's start from quite typical backend code:

public interface UserRepository {
    User findById(User.Id userId);
}

public interface UserProfileRepository {
    UserProfile findById(User.Id userId);
}

public class UserService {
    private final UserRepository userRepository;
    private final UserProfileRepository userProfileRepository;

    public UserWithProfile getUserWithProfile(User.Id userId) {
        User user = userRepository.findById(userId);

        if (user == null) {
            throw UserNotFoundException("User with ID " + userId + " not found");
        }

        UserProfile details = userProfileRepository.findById(userId);

        return UserWithProfile.of(user, details == null 
            ? UserProfile.defaultDetails()
            : details);
    }
}
Enter fullscreen mode Exit fullscreen mode

Interfaces at the beginning of the example are provided for context clarity.

The main point of interest is the getUserWithProfile method. Let's analyze it step by step.

  • First statement retrieves the user variable from the user repository.
  • Since user may not be present in the repository, user variable might be null. The following null check verifies if this is the case and throws a business exception if yes.
  • Next step is the retrieval of the user profile details. Lack of details is not considered an error. Instead, when details are missing, then defaults are used for the profile.

The code above has several issues in it. First, returning null in case if value is not present in repository is not obvious from the interface. We need to check documentation, look into implementation or make a guess how these repositories work.
Sometimes annotations are used to provide a hint, but this still does not guarantee API behavior.

To address this issue, let's apply ANAMAP rule to the repositories:

public interface UserRepository {
    Option<User> findById(User.Id userId);
}

public interface UserProfileRepository {
    Option<UserProfile> findById(User.Id userId);
}
Enter fullscreen mode Exit fullscreen mode

Now there is no need to make any guesses - API explicitly tells that returned value may not be present.

Now let's take a look into getUserWithProfile method again. The second thing to note is that the method may return a value or may throw an exception. This is a business exception, so we can apply NBE rule.
Main goal of the change - make the fact that a method may return value OR error explicit:

    public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
Enter fullscreen mode Exit fullscreen mode

OK, now we have API's cleaned up and can start changing the code. The first change will be caused by fact, that userRepository now returns Option<User>:

    public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
        Option<User> user = userRepository.findById(userId);
    }
Enter fullscreen mode Exit fullscreen mode

Now we need to check if the user is present and if not, return an error. With traditional imperative approach, code should be looking like this:

    public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
        Option<User> user = userRepository.findById(userId);

        if (user.isEmpty()) {
            return Result.failure(Causes.cause("User with ID " + userId + " not found"));
        }
    }
Enter fullscreen mode Exit fullscreen mode

The code does not look very appealing, but it is not worse than original either, so let's keep it for now as is.

The next step is to try to convert remaining parts of code:

    public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
        Option<User> user = userRepository.findById(userId);

        if (user.isEmpty()) {
            return Result.failure(Causes.cause("User with ID " + userId + " not found"));
        }

        Option<UserProfile> details = userProfileRepository.findById(userId);

    }
Enter fullscreen mode Exit fullscreen mode

Here comes the catch: details and user are stored inside Option<T> containers, so to assemble UserWithProfile we
need to somehow extract values. Here could be different approaches, for example, use Option.fold() method.
Resulting code will definitely not be pretty, and most likely will violate ANAMAP rule.

There is another approach - use the fact that Option<T> is a container with special properties.
In particular, it is possible to transform value inside Option<T> using Option.map() and Option.flatMap() methods.
Also, we know, that details value will be either, provided by repository or replaced with default. For this, we can use
Option.or() method to extract details from container.
Let's try these approaches:

    public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
        Option<User> user = userRepository.findById(userId);

        if (user.isEmpty()) {
            return Result.failure(Causes.cause("User with ID " + userId + " not found"));
        }

        UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails());

        Option<UserWithProfile> userWithProfile =  user.map(userValue -> UserWithProfile.of(userValue, details));

    }
Enter fullscreen mode Exit fullscreen mode

Now we need to write a final step - transform userWithProfile container from Option<T> to Result<T>:

    public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
        Option<User> user = userRepository.findById(userId);

        if (user.isEmpty()) {
            return Result.failure(Causes.cause("User with ID " + userId + " not found"));
        }

        UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails());

        Option<UserWithProfile> userWithProfile =  user.map(userValue -> UserWithProfile.of(userValue, details));

        return userWithProfile.toResult(Cause.cause(""));
    }
Enter fullscreen mode Exit fullscreen mode

Let's keep error cause in return statement empty for a moment and look again at the code.
We can easily spot an issue: we definitely know that userWithProfile is always present - case, when user is not present, is already handled above. How can we fix this?

Note, that we can invoke user.map() without checking if user is present or not. The transformation will be applied only if user is present, and ignored if not. This way, we can eliminate if(user.isEmpty()) check. Let's move the retrieving of details and transformation of User into UserWithProfile inside the lambda passed to user.map():

    public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
        Option<UserWithProfile> userWithProfile = userRepository.findById(userId).map(userValue -> {
            UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails());
            return UserWithProfile.of(userValue, details);
        });

        return userWithProfile.toResult(Cause.cause(""));
    }
Enter fullscreen mode Exit fullscreen mode

Last line need to be changed now, since userWithProfile can be missing. The error will be the same as in previous version, since userWithProfile might be missing only if the value returned by userRepository.findById(userId) is missing:

    public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
        Option<UserWithProfile> userWithProfile = userRepository.findById(userId).map(userValue -> {
            UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails());
            return UserWithProfile.of(userValue, details);
        });

        return userWithProfile.toResult(Causes.cause("User with ID " + userId + " not found"));
    }
Enter fullscreen mode Exit fullscreen mode

Finally, we can inline details and userWithProfile as they are used only once and immediately after creation:

    public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
        return userRepository.findById(userId)
            .map(userValue -> UserWithProfile.of(userValue, userProfileRepository.findById(userId)
                                                                                 .or(UserProfile.defaultDetails())))
            .toResult(Causes.cause("User with ID " + userId + " not found"));
    }
Enter fullscreen mode Exit fullscreen mode

Note how indentation helps to group code into logically linked parts.

Let's analyze the resulting code.

  • Code is more concise and written for happy day scenario, no explicit error or null checks, no distraction from business logic
  • There is no simple way to skip or avoid error or null checks, writing correct and reliable code is straightforward and natural.

Less obvious observations:

  • All types are automatically derived. This simplifies refactoring and removes unnecessary clutter. If necessary, types still can be added.
  • If at some point repositories will start returning Result<T> instead of Option<T>, the code will remain unchanged, except the last transformation (toResult) will be removed.
  • Aside the replacing of ternary operator with Option.or() method, resulting code looks a lot like if we would move code from original return statement inside lambda passed to map() method.

The last observation is very useful to start conveniently writing (reading usually is not an issue) PFJ-style code.
It can be rewritten into the following empirical rule: look for value on the right side. Just compare:

 User user = userRepository.findById(userId);    // <-- value is on the left side of the expression
Enter fullscreen mode Exit fullscreen mode

and

 return userRepository.findById(userId)
                      .map(user -> ...); // <-- value is on the right side of the expression
Enter fullscreen mode Exit fullscreen mode

This useful observation helps with transition from legacy imperative code style to PFJ.

Interfacing With Legacy Code

Needless to say, that existing code does not follow PFJ approaches. It throws exceptions, returns null and so on and so forth.
Sometimes it is possible to rework this code to make it PFJ-compatible, but quite often this not the case. Especially this is true for external libraries and frameworks.

Calling Legacy Code

There are two major issues with legacy code invocation. Each of them is related to violation of corresponding PFJ rule:

Handling Business Exceptions

The Result<T> contains a helper method called lift() which covers most use cases. Method signature looks so:

static <R> Result<R> lift(FN1<? extends Cause, ? super Throwable> exceptionMapper, ThrowingSupplier<R> supplier)
Enter fullscreen mode Exit fullscreen mode

The first parameter is the function which transforms an exception into the instance of Cause (which, in turn, is
used to create Result<T> instances in failure cases).

The second parameter is the lambda, which wraps the call to actual code which need to be made PFJ-compatible.

The simplest possible function, which transforms the exception into an instance of Cause is provided in Causes
utility class: fromThrowable(). Together with Result.lift() they can be used as follows:

public static Result<URI> createURI(String uri) {
    return Result.lift(Causes::fromThrowable, () -> URI.create(uri));
}
Enter fullscreen mode Exit fullscreen mode

Handling null Value Returns

This case is rather straightforward - if the API can return null, just wrap it into Option<T> using Option.option() method.

Providing Legacy API

Sometimes it is necessary to allow legacy code call code written in PFJ style. In particular, this often happens when
some smaller subsystem is converted to PFJ style, but rest of the system remains written in old style and API need to be
preserved.

The most convenient way to do this is to split implementation into two parts - PFJ style API and adapter, which only adapts new API to old API. Here could be very useful simple helper method like one shown below:

public static <T> T unwrap(Result<T> value) {
    return value.fold(
        cause -> { throw new IllegalStateException(cause.message()); },
        content -> content
    );
}
Enter fullscreen mode Exit fullscreen mode

There is no ready to use helper method provided in Result<T> for the following reasons:

  • there could be different use cases and different types of exceptions (checked and unchecked) can be thrown.
  • transformation of the Cause into different specific exceptions heavily depends on the particular use case.

Managing Variable Scopes

This section will be dedicated to various practical cases which appear while writing PFJ-style code.

Examples below assume use of Result<T>, but this is largely irrelevant, as all considerations are applicable to Option<T> as well. Also, examples assume that functions invoked in the examples, are converted to return Result<T> instead of throwing exceptions.

Nested Scopes

The functional style code intensively uses lambdas to perform computations and transformations of the values
inside Option<T> and Result<T> containers. Each lambda implicitly creates scope for their parameters - they are accessible inside the lambda body, but not accessible outside it.
This is a useful property in general, but for traditional imperative code it is rather unusual and might feel inconvenient at first. Fortunately, there is a simple technique to overcome perceived inconvenience.

Let's take a look at the following imperative code:

var value1 = function1(...);                    // function1() may throw exception
var value2 = function2(value1, ...);            // function2() may throw exception
var value3 = function3(value1, value2, ...);    // function3() may throw exception
Enter fullscreen mode Exit fullscreen mode

Variable value1 should be accessible for invocation of function2() and function3(). This does mean that following straightforward transformation to PFJ style will not work:

   function1(...)
       .flatMap(value1 -> function2(value1, ...))
       .flatMap(value2 -> function3(value1, value2, ...)); // <-- ERROR, value1 is not accessible!  
Enter fullscreen mode Exit fullscreen mode

To keep value accessible we need to use nested scope, i.e., nest calls as follows:

   function1(...)
       .flatMap(value1 -> function2(value1, ...)
           .flatMap(value2 -> function3(value1, value2, ...)));   
Enter fullscreen mode Exit fullscreen mode

Second call to flatMap() is done for value returned by function2 rather to value returned by first flatMap(). This way we keep value1 within the scope and make it accessible for function3.

Although it is possible to make arbitrarily deep nested scopes, usually more than a couple of nested scopes are harder to read and follow. In this case, it is highly recommended to extract deeper scopes into dedicated function.

Parallel Scopes

Another frequently observed case is the need to calculate/retrieve several independent values and then make a call or build an object. Let's take a look at the example below:

var value1 = function1(...);    // function1() may throw exception
var value2 = function2(...);    // function2() may throw exception
var value3 = function3(...);    // function3() may throw exception

return new MyObject(value1, value2, value3);
Enter fullscreen mode Exit fullscreen mode

At first look, transformation to PFJ style can be done exactly as for nested scopes. The visibility of each value will be the same as for imperative code. Unfortunately, this will make scopes deeply nested, especially if many values need to be obtained.

For such cases, Option<T> and Result<T> provide a set of all() methods. These methods perform "parallel" computation
of all values and return dedicated version of MapperX<...> interface. This interface has only three methods - id(), map() and flatMap(). The map() and flatMap() methods works exactly like corresponding methods in Option<T> and Result<T>, except they accept lambdas with different number of parameters. Let's take a look how it works in practice and convert imperative code above into PFJ style:

return Result.all(
          function1(...), 
          function2(...), 
          function3(...)
        ).map(MyObject::new);
Enter fullscreen mode Exit fullscreen mode

Besides being compact and flat, this approach has few more advantages. First, it explicitly expresses intent - calculate all values before use. Imperative code does this sequentially, hiding original intent.
Second advantage - calculation of each value is isolated and does not bring unnecessary values into scope. This reduces
context necessary to understand and reason about each function invocation.

Alternative Scopes

A less frequent, but still, important case is when we need to retrieve value, but if it is not available, then we use an alternative source of the value. Cases when more than one alternative is available are even less frequent, but even more painful when error handling is involved.

Let's take a look at following imperative code:


MyType value;

try {
    value = function1(...);
} catch (MyException e1) {
    try {
        value = function2(...);    
    } catch(MyException e2) {
        try {
            value = function3(...);
        } catch(MyException e3) {
            ... // repeat as many times as there are alternatives 
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The code is somewhat contrived because nested cases usually hidden inside other methods. Nevertheless, overall logic is far from simple, mostly because beside choosing the value, we also need to handle errors. Error handling clutters the code and makes initial intent - choose first available alternative - buried inside error handling.

Transformation into the PFJ style makes intent crystal clear:

var value = Result.any(
        function1(...),
        function2(...),
        function3(...)
    );
Enter fullscreen mode Exit fullscreen mode

Unfortunately, here is one important difference: original imperative code calculates second and subsequent alternatives only when necessary. In some cases, this is not an issue, but in many cases this is highly undesirable. Fortunately, there is a lazy version of the Result.any(). Using it, we can rewrite code as follows:

var value = Result.any(
        function1(...),
        () -> function2(...),
        () -> function3(...)
    );
Enter fullscreen mode Exit fullscreen mode

Now, converted code behaves exactly like its imperative counterpart.

Brief Technical Overview of Option<T> and Result<T>

These two containers are monads in the Functional Programming terms.

Option<T> is rather straightforward implementation of Option/Optional/Maybe monad.

Result<T> is an intentionally simplified and specialized version of the Either<L,R>: left type is fixed and should implement Cause interface. Specialization makes API very similar to Option<T> and eliminates a lot of unnecessary typing by the price of loss of universality and generality.

This particular implementation is focused on two things:

  • Interoperability between each other and existing JDK classes like Optional<T> and Stream<T>
  • API designed to make expression of intent clear

Last statement worth more in-depth explanation.

Each container has few core methods:

  • factory method(s)
  • map() transformation method, which transforms value but does not change special state: present Option<T> remains present, success Result<T> remains success.
  • flatMap() transformation method, which, beside transformation, may also change special state: convert present Option<T> into empty or success Result<T> into failure.
  • fold() method, which handles both cases (present/empty for Option<T> and success/failure for Result<T>) at once.

Besides core methods, there are a bunch of helper methods, which are useful in frequently observed use cases.
Among these methods, there is a group of methods which are explicitly designed to produce side effects.

Option<T> has the following methods for side effects:

Option<T> whenPresent(Consumer<? super T> consumer);
Option<T> whenEmpty(Runnable action);
Option<T> apply(Runnable emptyValConsumer, Consumer<? super T> nonEmptyValConsumer);
Enter fullscreen mode Exit fullscreen mode

Result<T> has the following methods for side effects:

Result<T> onSuccess(Consumer<T> consumer);
Result<T> onSuccessDo(Runnable action);
Result<T> onFailure(Consumer<? super Cause> consumer);
Result<T> onFailureDo(Runnable action);
Result<T> apply(Consumer<? super Cause> failureConsumer, Consumer<? super T> successConsumer);
Enter fullscreen mode Exit fullscreen mode

These methods provide hints to the reader that code deals with side effects rather than transformations.

Other Useful Tools

Besides Option<T> and Result<T>, PFJ employs some other general purpose classes. Below, each of them is described in more details.

Functions

JDK provided many useful functional interfaces. Unfortunately, functional interfaces for general purpose functions is limited only to two versions: single parameter Function<T, R> and two parameters BiFunction<T, U, R>.

Obviously, this is not enough in many practical cases. Also, for some reason, type parameters for these functions are reverse to how functions in Java are declared: result type is listed last, while in function declaration it is defined first.

PFJ uses a consistent set of functional interfaces for functions with 1 to 9 parameters. For brevity, they are called FN1...FN9. So far, there were no use cases for functions with more parameters (and usually this is a code smell). But if this will be necessary, the list could be extended further.

Tuples

Tuples is a special container which can be used to store several values of different types in a single variable. Unlike classes or records, values stored inside have no names. This makes them an indispensable tool for capturing an arbitrary set of values while preserving types. A great example of this use case is the implementation of Result.all() and Option.all() sets of methods.

In some sense, tuples could be considered a frozen set of parameters prepared for function invocation. From this perspective, the decision to make tuple internal values accessible only via map() method sounds reasonable. Nevertheless, tuple with 2 parameters has additional accessors which make possible use of Tuple2<T1,T2> as a replacement for various Pair<T1,T2> implementations.

PFJ uses a consistent set of tuple implementations with 0 to 9 values. Tuples with 0 and 1 value are provided for consistency.

Conclusion

Pragmatic Functional Java is a modern, very concise yet readable Java coding style based on Functional Programming concepts. It provides a number of benefits comparing to traditional idiomatic Java coding style:

  • PFJ involves Java compiler to help write reliable code:
    • Code which compiles usually works
    • Many errors shifted from run-time to compile time
    • Some classes of errors, like NullPointerException or unhandled exceptions, are virtually eliminated
  • PFJ significantly reduces the amount of boilerplate code related to error propagation and handling, as well as null checks
  • PFJ focuses on clear expression of intent and reducing mental overhead

Top comments (8)

Collapse
 
yannick555 profile image
Yannick Loth

Hello Sergiy,

Thanks for this post! I've been applying these recommendations for the whole last year. In many cases I like the approach, though I can't say for sure that the resulting code is easier to read for Java developers who are not used to FP and lambdas/function references all over the place.

I have to say that it's amazing how much this has changed my point of view about code and how much it helped the shift towards FP - in a way that actually makes me also comfortable with other FP languages, not just Java.

Please find infra some thoughts about FP in Java.

Best regards

--

Now I'm at the point where I try to figure out how to configure my code with FP instead of with magic (like the Spring framework does)... I don't like it when the behavior of systems that I maintain is magical.

  • Automation, yes. Magic, no.
  • Conventions, yes. Too many conventions, no. (Usually, too many is the amount of conventions where it is not possible anymore for the documentation to be complete, or when finding the right documentation is not straightforward, or when the conventions bring so many different new concepts that it's impossible to really understand what's going on without delving deep into the implementation of the framework.)

For example, what would be the signature of a generic withTransaction() method that starts a transaction around any service method I can pass to it? Is it even possible in Java without reflection?
Another example, what would be the signature of a generic withAuthorization() method that first checks if the user is authorized to access some method?

(Usually, when I try to change the way I organize/write/design code, thinking about transversal aspects like transaction management and security is enough to make sure that it will work - these are often the pain and blocking points).

--

On thing I like (at least conceptually, as an idea) is the ability to define once-used functions inside a code block, very locally, just where you use them.

{
var myLittleConsumer= (T)->...; //here you define the local function (in this case some consumer)
...
var t=T.of(...); //here you build/get your instance of T
...
myLittleConsumer.accept(t); //here you consume your instance of T
...
}
Enter fullscreen mode Exit fullscreen mode

But practically, putting a function in a variable and calling it using the reference to the variable is not very natural, at least in Java. Such functions also don't appear in the outline of a class and make it more difficult to navigate through the code.

Without local function definitions, the pollution of otherwise well designed classes with many private (and usually static) small functions that make no sense except at the only place you use them is real, if you want to avoid lambda expressions and favor function references. I also tried defining new inner classes just for this purpose, but it's also kind of an awful hack instead of an elegant pattern. This just doesn't feel right. Whatever I do to try to dominate and order many small used-once functions, I end up with some code structure that feels wrong, overly complex or not legible.
Locality + navigation of functions/function references is a problem in current IDEs or with the current state of the Java language. I'm not sure though that it's better with other FP languages...

--

One addition I've made to what you describe is implementing a Bool class that is basically much like a Boolean, has only two instances (TRUE and FALSE), and has some additional methods like many static constructor methods (of()), methods to convert to/from Java's boolean and Boolean and the fold method: fold( ()-> falseAction, ()->trueAction) (fold always returns the instance to allow for chaining). This fold() method allows to program if-else statements using expressions (and a more functional style). Once you have this Bool class, it's incredible how often you write code like the following:

Bool.of(getSomeBooleanValue()).fold(/*sameBooleanValue==false*/()->, /*someBooleanValue==true*/ ()-> )
Enter fullscreen mode Exit fullscreen mode

There are so many places in the code where if-else statements are replaced by expressions and functions. But then again, sometimes I have the feeling that a simple if-else statement is just more legible.

Once you have this class, anything of type boolean or Boolean may easily be actually replaced by the type Bool, in order to benefit from Bool's features.
Static constructor methods like for example Bool.of(Boolean), Bool.of(()->Boolean), Bool.of(boolean), Bool.of(Predicate<>)... and instance conversion methods like Boolean myBool.toBoolean() provide an easy way to convert from and to the standard types if necessary.

Note:
I've been thinking about the best name for this. Wouldn't Bit (with values ZERO and ONE) be better? Isn't Bit too tied to the actual, underlying (let's say "electronic") representation?
If we use Bit, how do we represent it? There is not bit in Java, there is just a byte with the value 0 and another byte with the value 1... Of course we can manipulate bits, but isn't it actually always byte/char manipulation?

Both Bool and Bit represent something with two states. But Bool somehow seems to be more general: it has two values/states TRUE and FALSE, independent of the underlying representation (a bit like enums in Java).
In maths, there are even multi-valued booleans, where there may be more than two states. And complete theories have been built on top of those, depending on the semantics of their states.
Thus, for now, I'd rather recommend staying with Bool instead of Bit, for the sake of generality.

--

As for if-else with Bool, wouldn't it be interesting to think about all the statements used in the Java language and how to replace them with expressions and functions. That's also on my list.

--

My 2 cents about terminology : This has probably already been long discussed in other forums, but I strongly favor the term Maybe instead of any variation of Option or Optional, because it is true for both the consumer and the provider of an API: as a consumer of a function, I never have the choice (read: option) to receive some value or nothing. While the provider a function has the possibility (read: option) to return some value or not. What is true: as a consumer, maybe I receive something, and maybe not. What is also true: as a provider, maybe I return some value, and maybe I return nothing. The term Maybe just fits the points of view of both the consumer and the provider, while Option and Optional lie to the consumer of a function (though they say the truth to the provider of a function).
A large part of this post is about writing code that does not lie, and even stronger, that does explicitly tell the truth (cf. your Result and Option classes, the NBE rule, the ANAMAP rule...).

--

I've got some questions about the NBE rule:

PFJ uses exceptions only to represent cases of fatal, unrecoverable (technical) failures. Such an exception might be intercepted only for purposes of logging and/or graceful shutdown of the application.

What exactly should be the scope of "fatal, unrecoverable (technical) failures"? With the example of a web app (servlet, nothing reactive or fancy), with multiple concurrent requests from multiple users, should one consider "fatal" as in "fatal for the request" (one request fails) or "fatal for the app" (complete shutdown of the app) ?

Also:

Code can be written for happy day scenario and errors handled at the point where this is most convenient - original intent of exceptions, which was never actually achieved.

So, if some anomaly occurs in method E (which is called from D on a call stack A->B->C->D->E), one gets a Failure from the method instead of a Success. Then, in D, one has to handle this Failure and let it go up the stack to D (by returning a Failure instance) and eventually to B, where some error handling occurs (I don't let it go up to A just to reason about the general case where errors may be handled at any level of the call stack). What I don't see is how we don't have to handle the error in each single one of the methods D, then C, then B: don't we have to handle (even if it's just if(result.isFailure()) return result; immediately after receiving the result from the failing function) both success and error cases in every one of these methods?
Somehow this has held me back from fully embracing using a Result class - though I know it's heavily used in FP languages. Is there anything I miss?

--

Usually, if not stated differently, a value may be null, or not. That is the default in Java, hence we must check for null to avoid NPE.

Using Maybe to avoid null in our code base means that we create a convention according to which, if it's not a Maybe, it's never null, it's always expected to have some actual value (actually, nothing is ever null, even the reference to a Maybe must not ever be null). Because if it is possible to have no value, then we use Maybe to explicitly represent this special state.

And doing so, we compartmentalize the code: most stuff we use may return null, but (part of) our own code base never returns null because we always use Maybe.
That, in my opinion, is not ideal either.

Another way to avoid this is to explicitly state what must never be either null or without value, by wrapping the value inside a NN instance (NN stands for "not null" or "never null") and have the convention that anything of type NNmust never be null and must always contain some value. And then, consider that anything that is not wrapped inside a NN may be null, exactly like we have been used to since the inception of Java.

Now, does NN have some advantages beyond this? I can't tell, I never actually used this in a larger code base. It's just a thought I have at this time.
I implemented it like this (I have not tested it yet):

import java.util.Objects;

/**
 * This class explicitly indicates that any variable, method argument or returned value of this type must not be {@code null}. The value it wraps is never {@code null}.
 * <p>"NN" stands for "not null", or "never null".</p>
 *
 * <p>In Java, by default, any reference may be {@code null}. Using this container class, the intent of the developer to never manipulate a reference that may be {@code null}.</p>
 *
 * @param <T> the type of the contained value
 */
public final class NN<T> {
    private final T value;

    private NN(final T value) {
        this.value = Objects.requireNonNull(value, "The specified value must not be null.");
    }

    public static <T> Result<NN<T>> of(final T value) {
        try {
            return Result.success(new NN<>(value));
        } catch (final NullPointerException npe) {
            return Result.failure();
        }
    }

    public T get() {
        return value;
    }

    @Override
    public String toString() {
        return value.toString();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final NN<?> nn = (NN<?>) o;
        if (value == nn.value) return true;
        if (value.getClass() != nn.value.getClass()) return false;
        return value.equals(nn.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
siy profile image
Sergiy Yevtushenko

Sorry for being silent. Your comment requires a detailed answer, but I just have too little time for it. I'll definitely answer, just a little bit later.

Collapse
 
ant_kuranov profile image
Anton Kuranov

To be written in a pure-functional style, your code should look like this:

return userRepository.findById(userId)
  .map(userValue  -> UserWithProfile.of(userValue,
      userProfileRepository.findById(userId).or(userProfile.defaultDetails()))
    .toResult(Cause.cause("")))
  .or(Result.failure(Causes.cause("User with ID " + userId + " not found")));
Enter fullscreen mode Exit fullscreen mode

To be honest, comparing with the "legacy" code I found it less readable and maintainable. We introduced here a non-standard third-party dependency on PFJ (why not Vavr, FunctionalJ or any other?) and two new entities: Option and Result to express absolutely the same functionality.

Collapse
 
siy profile image
Sergiy Yevtushenko
  • I don't care about "pure functional"
  • Yes, your version is less readable and, at least, must be refactored.
  • It's not about expressing functionality, it's about preserving context and get support from compiler.

There is no such thing as "non-stadard" third-party dependency. And PFJ is not a library. I have a feeling that you didn't really read the article and missed the whole thing.

Collapse
 
yendenikhil profile image
Nik

I am using functional programming in java for last few year and I use overall java since version 1.4. I loved the way you explained the concepts and reasoning (the context ) behind the decisions.

There is a learning from other programming languages which is subtly used with your own thought process. This is amazing. The monads of Haskell and Result class similar to what you have in rust fits very well. Great job!

Collapse
 
skypy profile image
Kinjal

This is very good explanation on functional approach. Thank you for sharing.

Collapse
 
friendbear profile image
T Kumagai

Good job

Collapse
 
jimstockwell profile image
Jim Stockwell

Interesting. Thank you!