Be more functional in java with vavr

Share This Post

With the release of java 8 a new paradigm was discovered in the development with java, but the question arises – is it enough? And what if we could have other functionalities of more purely functional languages in java? To meet these needs vavr was invented with the mission of reducing the code making it more readable and adding robustness with the immutability of the data. And in this article, we will see how to be more functional in java with vavr.

Be more functional in java with vavr

Vavr among other things includes immutability in lists and functions to work with them, it also includes some of the monads most used in other languages, more functional, currying and partial application in functions.

Functions

Composition
With the arrival of java 8 the concept of Function and BiFunction was included, with which we can define functions of one or two input parameters, for example:


Function<Integer, Integer> pow = (n) -> n * n;
assertThat(pow.apply(2)).isEqualTo(4);
BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
assertThat(multiply.apply(10, 5)).isEqualTo(50);

With vavr we can have functions up to 8 parameters with the FunctionN types


Function1<Integer, Integer> pow = (n) -> n * n;
assertThat(pow.apply(2)).isEqualTo(4);
Function3<Integer, Integer, Integer, Integer> multiply = (n1, n2, n3) -> n1 * n2 * n3;
assertThat(multiply.apply(5, 4, 3)).isEqualTo(60);

Besides being able to create functions of up to 8 input parameters, it also offers us the composition of functions with operations .andThen .apply and .compose


Function1<String, String> toUpper = String::toUpperCase;
Function1<String, String> trim = String::trim;
Function1<String, String> cheers = (s) -> String.format("Hello %s", s);
assertThat(trim
            .andThen(toUpper)
            .andThen(cheers)
            .apply("   john")).isEqualTo("Hello JOHN");
Function1<String, String> composedCheer =
cheers.compose(trim).compose(toUpper);
assertThat(composedCheer.apply(" steve ")).isEqualTo("Hello STEVE");

Lifting

With lifting what we get is to deal with the exceptions when composing the functions, with which the function will return an Option.none in case that an exception and Option.some, in case everything has gone correctly.
This is very useful when composing functions that use third-party libraries that can return exceptions.


Function1<String, String> toUpper = (s) -> {
    if (s.isEmpty()) throw new IllegalArgumentException("input can not be null");
    return s.toUpperCase();
};
Function1<String, String> trim = String::trim;
Function1<String, String> cheers = (s) -> String.format("Hello %s", s);
Function1<String, String> composedCheer = cheers.compose(trim).compose(toUpper);
​
Function1<String, Option> lifted = Function1.lift(composedCheer);
assertThat(lifted.apply("")).isEqualTo(Option.none());
assertThat(lifted.apply(" steve ")).isEqualTo(Option.some("Hello STEVE"));

Partial application

  Sacrificial architecture

With the partial application we can create a new function by setting n parameters to an existing one, where n will always be less than the arity of the original function and the return will be an original arity function – parameters set


Function2<String, String, String> cheers = (s1, s2) -> String.format("%s %s", s1, s2);
Function1<String, String> sayHello = cheers.apply("Hello");
Function1<String, String> sayHola = cheers.apply("Hola");
assertThat(sayHola.apply("Juan")).isEqualTo("Hola Juan");
assertThat(sayHello.apply("John")).isEqualTo("Hello John");

We have defined a generic cheers function that accepts two input parameters, we have derived this to two new sayHello and sayHola applying it partially, and we already have two more specific ones to say hello and we could derive more cases if we needed them.

Currying

Currying is the technique of decomposing a function of multiple arguments into a succession of functions of an argument.


Function3<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
Function1<Integer, Function1<Integer, Integer>> add2 = sum.curried().apply(2);
Function1<Integer, Integer> add2And3 = add2.curried().apply(3);
assertThat(add2And3.apply(4)).isEqualTo(9);

Memoization

One of the premises of the functional programming is to have pure functions, without side effects, this basically means that a function passing the same arguments always has to return the same result.
Therefore, if it always returns the same thing, why not cache it? because this is the mission of memoization, caching the inputs and outputs of the functions to only launch them once.


void memoization() {
        Function1<Integer, Integer> calculate =
            Function1.of(this::aVeryExpensiveMethod).memoized();
        StopWatch watch = new StopWatch();
        watch.start();
        calculate.apply(40);
        System.out.println(watch.getTime());
        calculate.apply(40);
        System.out.println(watch.getTime());
        calculate.apply(50);
        System.out.println(watch.getTime());
}
private Integer aVeryExpensiveMethod(Integer number) {
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return number * number;
}

Monads

Try

The monad Try includes an execution capturing of a possible exception, its two possible return values are the case of failure by exception or the value if it has gone well.

  Debugging The Perplexing: Don’t Panic!

Some useful methods of the Try are:

.isSuccess () -> as the name itself indicates, returns a boolean by checking if it is a success.

.isFailure () -> returns a boolean by checking if it is a failure.

get () -> get the value in case it has gone correctly, if a get is made and it is not checked if it done without checking if it is success, it will drop the exception.

map () -> map over the value in case it went well, if it is a failure it will not be executed.

getOrElse (T) -> which allows to return a default value in the case of error.

getOrElse (Supplier) -> which allows to pass another function in the case of error.

recover (throwable -> {}) -> Same as getOrElse but in this case we will have the exception that has been thrown to be able to achieve it or to be able to return different values depending on the type of exception.


Function2<Integer, Integer, Integer> divide = (n1, n2) -> n1 / n2;
assertThat(Try.of(() -> divide.apply(10, 0)).isFailure()).isTrue();
assertThat(Try.of(() -> divide.apply(10, 5)).isSuccess()).isTrue();
assertThat(Try.of(() -> divide.apply(10, 5)).get()).isEqualTo(2);
assertThat(Try.of(() -> divide.apply(10, 0)).getOrElse(0)).isEqualTo(0);

Lazy

Lazy is a monad about a Supplier to whom memoization is applied the first time it is evaluated.


Lazy<List> lazyOperation = Lazy.of(this::getAllActiveUsers);
assertThat(lazyOperation.isEvaluated()).isFalse();
assertThat(lazyOperation.get()).isNotEmpty();
assertThat(lazyOperation.isEvaluated()).isTrue();

Either

Either represents a value of two types, Left and Right being by convention, putting the value in the Right when it is correct and in the Left when it is not.
Always the result will be a left or a right, it can never be the case that they are both.

CTA Software

Data structures

Immutable lists
If one of the principles of functional programming is immutability, what happens when we define a list and add items? Well, we are mutating it.
Vavr provides a specialization of List, which once created cannot be modified, any operation of adding, deleting, replacing, will give us a new instance with the changes applied.

  Observability Best Practices in Software Development

 


import io.vavr.collection.List;
...
//Append
List original = List.of(1,2,3);
List newList = original.append(4);
assertThat(original.size()).isEqualTo(3);
assertThat(newList.size()).isEqualTo(4);
//Remove
List original = List.of(1, 2, 3);
List newList = original.remove(3);
assertThat(original.size()).isEqualTo(3);
assertThat(newList.size()).isEqualTo(2);
//Replace
List original = List.of(1, 2, 4);
List newList = original.replace(4,3);
assertThat(original).contains(1,2,4);
assertThat(newList).contains(1,2,3);

Besides the immutability, it also provides direct methods to operate with the list without going through the stream, get the minimum, maximum, average value .. for more information about what this list offers you, check javadoc.
And these are the main features that Vavr offers us, however, there are some more that help us to be more functional in a language like java with vavr.

If you would like to get more information about java with vavr, I highly recommend you to subscribe to our monthly newsletter by clicking here.

And if you found this article about java with vavr useful, you might like…

Scala generics I: Scala type bounds

Scala generics II: covariance and contravariance

Scala generics III: Generalized type constraints

BDD: user interface testing

F-bound over a generic type in Scala

Microservices vs Monolithic architecture

“Almost-infinit” scalability

iOS Objective-C app: sucessful case study

Mobile app development trends of the year

Banco Falabella wearable case study 

Mobile development projects

Viper architecture advantages for iOS apps

Why Kotlin ? 

Software architecture meetups

Pure MVP in Android

Author

  • Joaquin Caro

    Software developer with over 15 years of experience, the last 9 years focussed on JVM. He can play different roles like Software developer, Software Architect, or Tech Lead, always following the best practices and leading by example, delivering high-quality code. Passionate about software craftsmanship and DevOps culture.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Subscribe To Our Newsletter

Get updates from our latest tech findings

Have a challenging project?

We Can Work On It Together

apiumhub software development projects barcelona
Secured By miniOrange