What is Vavr?

Vavr is a Functional Programming library in Java 8+. The first version, called Javaslang, was released on 19h march 2014.

It mainly contains three parts

  1. Immutable collections to avoid side-effects
  2. ADT(Algebraic data type) to eliminate side-effects
  3. Pattern matching to simplify the usage of ADT

I already created vavr-examples, welcome to clone and play the features.

How to install Vavr?

  • Maven

      <dependencies>
          <dependency>
              <groupId>io.vavr</groupId>
              <artifactId>vavr</artifactId>
              <version>0.10.4</version>
          </dependency>
      </dependencies>
    
  • Gradle

      dependencies {
          compile "io.vavr:vavr:0.10.4"
      }
    
  • Gradel 7 +

      dependencies {
          implementation "io.vavr:vavr:0.10.4"
      }
    

How to use Vavr?

Functional Programming means constructing program with pure functions.

A function is pure when

  • For all input, the same input produces the same output
  • No Side-Effects

For example, the following example breaks the first rule

public Integer generateRandomNumber() {
    Random random = new Random();
    return random.nextInt();
}

@Test
public void returnDifferentResultForEachCall() {
    assertThat(generateRandomNumber()).isNotEqualTo(generateRandomNumber());
}

If we call generateRandomNumber multiple times, it will generate a different Integer. We can not avoid this logic, because we need a random integer anyway. But We can move this logic to the boundary of the program to ensure most of the functions are pure. For example, we can pass a function to generate a random integer or generate the random integer in main and then pass it to business logic.

public Integer generateRandomNumber(Supplier<Integer> generator) {
    return generator.get();
}

@Test
public void returnSameResultForEachCall() {
    assertThat(generateRandomNumber(() -> 1)).isEqualTo(generateRandomNumber(() -> 1));
}

In the following sections, we will focus on how to use Vavr to avoid or eliminate side-effect.

Avoid Side-Effect

Is the append function pure?

public void append(List<Integer> intList1, List<Integer> intList2) {
    intList1.addAll(intList2);
    return;
}

@Test
public void modifyParameterIsSideEffect() {

    List<Integer> intList1 = new LinkedList<Integer>();
    intList1.add(1);
    intList1.add(2);

    List<Integer> intList2 = new LinkedList<Integer>();
    intList2.add(3);
    intList2.add(4);

    this.append(intList1, intList2);

    assertThat(intList1.size()).isEqualTo(4);
    assertThat(intList2.size()).isEqualTo(2);
}

The answer is no. it returns void, but actually, changes the intList1 which is outside of the function.

We can code like this because the List is mutable. To avoid the modification of the outside variable, we need to make the data type immutable.

Vavr redefines the common collection data type to make them immutable.

List

We can construct List by some util functions

java.util.List<Integer> javaList = new java.util.LinkedList<Integer>();
javaList.add(1);
javaList.add(2);
javaList.add(3);

List.of(1,2,3);
List.ofAll(javaList);

The List is immutable, any changes to the list instance will create a new instance

public List<Integer> append(List<Integer> intList1, List<Integer> intList2) {
    return intList1.appendAll(intList2);
}

@Test
public void modifyParameterWillGenerateNewList() {

    List<Integer> intList1 = List.of(1, 2);

    List<Integer> intList2 = List.of(1, 2);

    List<Integer> result = this.append(intList1, intList2);

    assertThat(result.size()).isEqualTo(4);
    assertThat(intList1.size()).isEqualTo(2);
    assertThat(intList2.size()).isEqualTo(2);
}

We can also compare the elements of List easily

@Test
public void listCanCompareElement() {
    assertThat(List.of(1, 2)).isEqualTo(List.of(1, 2));
    assertThat(List.of(1, 2)).isNotEqualTo(List.of(1, 2, 3));
}

Eliminate Side-Effect

Option

It’s very common to return null in Java, for example

public Integer getHead(java.util.List<Integer> intList){
  if(intList.size() == 0 ){
     return null;
  } else {
     return intList.get(0);
  }
}

@Test
public void consumerNeedToCheckNull() {
    Integer head = getHead(new LinkedList<Integer>());

    String result = head == null ? "No Element" : head.toString();

    assertThat(result).isEqualTo("No Element");
}

But if one function may return null, the consumer of the function need always to check if the return value is null, that’s why there are lots of ways to do nullable checking, such as @NonNull, assert a != null and Objects.requireNonNull(a).

The reason is a wrapper type always stands for two possible values, null or normal value, which is implicit and easy to forget. Even worse if we forget to check null, the compiler won’t remind us.

In Functional Programming, we’d like to eliminate null, then it’s very easy to know what’s the return value of the function, and don’t need to guess if it will return null.

So null is a side-effect in Functional Programming, we need to find a way to return it more obviously. Vavr defines Option to make the function returning null pure.

The above example can be changed to

public Option<Integer> getHead(java.util.List<Integer> intList) {
    if (intList.size() == 0) {
        return Option.none();
    } else {
        return Option.some(intList.get(0));
    }
}

@Test
public void consumerAlwaysKnowOptionMaybeSomeOrNone() {
    Option<Integer> head = getHead(new java.util.LinkedList<Integer>());

    String result = head.fold(() -> "No Element", (x) -> x.toString());

    assertThat(result).isEqualTo("No Element");
}

We can use Some to wrap normal value, None to replace null

Option.some(1);
Option.none();

We can compare its value easily

assertThat(Option.some(1)).isEqualTo(Option.some(1));
assertThat(Option.none()).isEqualTo(Option.none());

And unwrap it easily

assertThat(Option.some(1).get()).isEqualTo(1);
assertThat(Option.none().getOrNull()).isNull();

Either

Do you think this is a pure function?

public Integer divide(Integer x, Integer y) {
    if (y == 0) {
        throw new Error("The denominator can not be 0");
    } else {
        return x / y;
    }
}

The error is a side-effect because we don’t know if the function will throw an error just according to its return type, we have to add some comment or use the throws keyword to supply more information. But it’s still not convenient for the user to handle the error.

To make the function pure, we can use Either, then the function can be refactored like this

public Either<Error, Integer> divide(Integer x, Integer y) {
    if (y == 0) {
        return Either.left(new Error("The denominator can not be 0"));
    } else {
        return Either.right(x / y);
    }
}

Either can wrap data of two different types, take String and Integer for example

Either<String, Integer> errorOrInteger = Either.left("Error");
Either<String, Integer> errorOrInteger = Either.right(1);

It supports comparing values directly

assertThat(Either.right(1)).isEqualTo(Either.right(1));
assertThat(Either.left("Error")).isEqualTo(Either.left("Error"));

And can be unwrapped

@Test
public void shouldReturnWrappedValueForRight() {
    Either<String, Integer> intValue = Either.right(1);
    assertThat(intValue.get()).isEqualTo(1);

    Either<String, Integer> intValue2 = Either.left("Error");
    assertThatThrownBy(() -> intValue2.get()).isInstanceOf(NoSuchElementException.class)
            .hasMessageContaining("get() on Left");
}

@Test
public void shouldReturnWrappedValueForLeft() {
    Either<String, Integer> intValue = Either.left("Error");
    assertThat(intValue.getLeft()).isEqualTo("Error");

    Either<String, Integer> intValue2 = Either.right(1);
    assertThatThrownBy(() -> intValue2.getLeft()).isInstanceOf(NoSuchElementException.class)
            .hasMessageContaining("getLeft() on Right");
}

Try

Sometimes we need to invoke other libraries, which may throw an error, for example

public Integer add(String x, String y){
   Integer xInt = Integer.valueOf(x);
   Integer yInt = Integer.valueOf(y);
   return xInt + yInt;
}

@Test
public void stringToIntegerMayThrowError() {
    assertThatThrownBy(() -> {
        add("1", "a");
    }).isInstanceOf(IllegalArgumentException.class);
}

We can use regex expression to check if the parameters are integer string, but we also can use Try to make it easier

public Try<Integer> add(String x, String y) {
    Try<Integer> xInt = Try.of(() -> Integer.valueOf(x));
    Try<Integer> yInt = Try.of(() -> Integer.valueOf(y));
    return xInt.flatMap(xv -> yInt.map(yv -> xv + yv));
}
@Test
public void tryCanHandleError() {
    assertThat(add("1", "a").getCause()).isInstanceOf(IllegalArgumentException.class);
}

Try can accept a function, return Failure if the function throws an error, and return Success if the function return value successfully.

It can also accept a constant value

Try<Integer> = Try.success(1);
Try<Integer> = Try.failure(new Error("Error"));

And can be unwrapped

assertThat(Try.success(1).get()).isEqualTo(1);
assertThatThrownBy(() -> {
    Try.success(1).getCause();
}).isInstanceOf(UnsupportedOperationException.class);
assertThat(Try.failure(new Error("Error")).getCause()).isInstanceOf(Error.class);
assertThatThrownBy(() -> {
    Try.faiure(new Error("Error")).get();
}).isInstanceOf(Error.class);

How to code with functions?

In OO, there are lots of popular design patterns to organize the code.

In Functional Programing, there are lots of popular higher order functions to organize the code.

In this section, considering we use ADT as the return type to wrap effects, we will call ADT a container to make it easy to understand.

map

A map can apply a lambda function to a container, for example

assertThat(Option.some(1).map(x -> x + 1)).isEqualTo(2);
assertThat(Option.none().map(x -> x + 1)).isEqualTo(Option.none());

assertThat(Either.right(1).map(x -> x + 1)).isEqualTo(Either.right(2));
assertThat(Either.left("Error").map(x -> x + 1)).isEqualTo(Either.left("Error"));

assertThat(Try.success(1).map(x -> x + 1)).isEqualTo(Try.success(2));
assertThat(Try.failure(new Error("Error")).map(x -> x + 1).isFailure()).isTrue();

assertThat(List.of(1,2,3).map(x -> x + 1)).isEqualTo(List.of(2,3,4));

Vavr uses the traditional class method to implement map, we can abstract it to a higher-order function, the pseudocode is

public <M<_>, A, B> M<B> map(M<A> value, Function<A, B> f)

The code can’t compile in Java, M<_> means any type requiring one type parameter, such as Option<_>, Either<String, _>, Try<_>, List<_> etc.

flatMap

A flatMap can also apply a lambda function to a container, but not like a map, the lambda function will return the type of container. If M.map accept function A->B, then M.flatMap accept function A -> M<B>

for example

assertThat(Option.some(1).flatMap(x -> Option.some(x + 1))).isEqualTo(2);
assertThat(Option.some(1).flatMap(x -> Option.none())).isEqualTo(Option.none());
assertThat(Option.none().flatMap(x -> Option.some(x + 1))).isEqualTo(Option.none());
assertThat(Option.none().flatMap(x -> Option.none())).isEqualTo(Option.none());

assertThat(Either.right(1).flatMap(x -> Either.right(x + 1))).isEqualTo(Either.right(2));
assertThat(Either.right(1).flatMap(x -> Either.left("Error"))).isEqualTo(Either.left("Error"));
assertThat(Either.left("Error").flatMap(x -> Either.right(x + 1))).isEqualTo(Either.left("Error"));
assertThat(Either.left("Error").flatMap(x -> Either.left("Error2"))).isEqualTo(Either.left("Error"));

assertThat(Try.success(1).flatMap(x -> Try.success(x + 1))).isEqualTo(Try.success(2));
assertThat(Try.success(1).flatMap(x -> Try.failure(new Error("Error"))).isFailure()).isTrue();
assertThat(Try.failure(new Error("Error")).flatMap(x -> Try.sucess(x + 1)).isFailure()).isTrue();
assertThat(Try.failure(new Error("Error")).flatMap(x -> Try.failure(new Error("Error2"))).isFailure()).isTrue();

assertThat(List.of(1,2,3).flatMap(x -> List.of(x, x))).isEqualTo(List.of(1,1,2,2,3,3,4,4));

The pseudocode of flatMap is

public <M, A, B> M<B> flatMap(M<A> value, Function<A, M<B>> f)

filter

A filter is a shortcut usage of flatMap, take Option for example

public <A> Option<A> filter(Option<A> value, Function<A, boolean> f) {
	return value.flatMap(x -> {
		return f(x) ? Option.some(x) : Option.none();
	});
}

The different containers will have different behavior for filter, for example

assertThat(Option.some(1).filter(x -> x > 1)).isEqualTo(Option.none());
assertThat(Option.some(1).filter(x -> x < 2)).isEqualTo(Option.some(1));
assertThat(Option.none().filter(x -> x > 1)).isEqualTo(Option.none());
assertThat(Option.none().filter(x -> x < 2)).isEqualTo(Option.none());

assertThat(Either.right(1).filter(x -> x > 1)).isEqualTo(Option.some(Either.right(1)));
assertThat(Either.right(1).filter(x -> x < 2)).isEqualTo(Option.none());
assertThat(Either.left("Error").filter(x -> x > 1)).isEqualTo(Option.none());
assertThat(Either.left("Error").filter(x -> x < 2)).isEqualTo(Option.none());

assertThat(Try.success(1).filter(x -> x > 1).isFailure()).isTrue();
assertThat(Try.success(1).filter(x -> x < 2)).isEqualTo(Try.success(1));
assertThat(Try.failure(new Error("Error")).filter(x -> x > 1).isFailure()).isTrue();
assertThat(Try.failure(new Error("Error")).filter(x -> x < 2).isFailure()).isTrue();

assertThat(List.of(1,2,3).filter(x -> x > 1)).isEqualTo(List.of(2,3));

foldLeft

A foldLeft is used to fold all the possible effects of the container into one value, for example

assertThat(List.of(1,2,3).foldLeft(0, (acc, ele) -> acc + ele)).isEqualTo(6);

But Option, Either, and Try only have the fold function in Vavr

assertThat(Option.some(1).fold(() -> "none", x -> x.toString())).isEqualTo("1");
assertThat(Option.none().fold(() -> "none", x -> x.toString())).isEqualTo("none");

assertThat(Either.right(1).<String>fold((e) -> "left", x -> x.toString())).isEqualTo("1");
assertThat(Either.left("Error").<String>fold((e) -> "left", x -> x.toString())).isEqualTo("left");

assertThat(Try.success(1).<String>fold((e) -> "failure", x -> x.toString())).isEqualTo("1");
assertThat(Try.failure(new Error("Error")).<String>fold((e) -> "failure", x -> x.toString()))
                .isEqualTo("failure");

But they actually can implement foldLeft, take Option for example

public <A, B> B foldLeft(Option<A> option, B initial, Function2<B, A, B> f){
	return option.flatMap(x -> f(initial, x)).getOrElse(initial);
}

public <A, B> B fold(Option<A> option, B onNone, Function<A, B> onSome){
	return foldLeft(option, onNone, (acc, ele) -> onSome(ele));
}

the pseudocode of higher oder function foldLeft is

public <M<_>, A, B> B foldLeft(M<A> value, B initial, Function2<B, A, B> f)

How to unwrap a container easily?

To catch side-effects, we use an abstract interface as the parent type of all function effects, each effect is a child of the interface. For the consumer of the function, to know what’s the effect returned, we need to find a way to identify the child of the interface, then take corresponding actions.

For Option, we can use isSome and isNone

assertThat(Option.some(1).isSome()).isTrue();
assertThat(Option.some(1).isNone()).isFalse();

For Either, we can use isRight and isLeft

assertThat(Either.right(1).isRight()).isTrue();
assertThat(Either.right(1).isLeft()).isFalse();

For Try, we can use isSuccess and isFailue

assertThat(Try.success(1).isSuccess()).isTrue();
assertThat(Try.success(1).isFailure()).isFalse();

There will be lots of if-else in our code, like

if(v.isSome()){
   ...
} else {
   ...
}

Vavr supply pattern matching to help us remove these boilerplates.

Pattern Matching

Vavr borrows pattern matching from Scala Pattern Matching, which is a very powerful tool.

We can use it to unwrap container

@Test
public void canMatchOption() {
    Integer result =
            Match(Option.some(1)).of(Case($Some($()), x -> x + 10), Case($None(), 2));

    assertThat(result).isEqualTo(11);
}

@Test
public void canMatchEither() {

    Either<String, Integer> eitherValue = Either.right(1);

    String result = Match(eitherValue).of(Case($Right($()), x -> "right"),
            Case($Left($()), x -> "left"));

    assertThat(result).isEqualTo("right");

}

@Test
public void canMatchTry() {
    Try<Integer> tryValue = Try.success(1);

    String result = Match(tryValue).of(Case($Success($()), x -> "success"),
            Case($Failure($()), x -> "failure"));

    assertThat(result).isEqualTo("success");
}

@Test
public void canMatchList() {
    List<Integer> listValue = List.of(1,2,3);

    Integer result = Match(listValue).of(Case($Cons($(), $()), (head, tail) -> head));

    assertThat(result).isEqualTo(1);
}

We can check if the value meets the given condition

@Test
public void canCheckValue() {
    Integer intValue = 2;
    String result = Match(intValue).of(Case($(x -> x > 0), x -> "positive"),
            Case($(x -> x < 0), x -> "negative"),
            Case($(x -> x == 0), x -> "zero"));

    assertThat(result).isEqualTo("positive");
}

We can also use it like switch

@Test
public void canMatchLikeSwitch() {
    Integer intValue = 1;

    String result = Match(intValue).of(Case($(1), "1"), Case($(2), "2"), Case($(), "-1"));

    assertThat(result).isEqualTo("1");
}

Summary

Vavr borrow lots of concepts from Scala, if you are interested in Functional Programming, please try cats in Scala, it supplies more powerful features.

Tags: ,

Updated:

Comments