March 21, 2019

Introduction to Functional Programming with Vavr

Pure functions, High Order functions, Immutability, Composition, Currying, Recursion, Lazy, Functors, Foldable, Applicative, Category, Semi Group, Monoid, Monad.

brain

Concepts

I won’t write something easier to follow than: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html

I encourage you to follow it, but for this article I will take some of its examples and just show you how you can do it in Java with Vavr.

All images are courtesy of Aditya Bhargava, the author of the article above.

Functors

When a value is wrapped in a box, you can’t apply a normal function to it

This is where map comes in. map knows how to apply functions to values that are wrapped in a box.

Option.of(2).map(i -> i + 3); // Some(5)
Option.<Integer>of(null).map(i -> i + 5); // None

A functor is any type that defines how map works.

Here’s what is happening behind the scenes when we write Option.of(2).map(i -> i + 3)

Here’s what is happening behind the scenes when we try to map a function on an empty box

Now, what happens when you apply a function to a list?

List.of(2, 4, 6).map(i -> i + 3); // (5, 7, 9)

Functions are also functors!

In Java with vavr map is called compose or andThen

Function1<Integer, Integer> add3 = Function1.of(i -> i + 3);
Function1<Integer, Integer> add2 = Function1.of(i -> i + 2);

List.of(2, 4, 6).map(add3.compose(add2)); // (7, 9, 11)

Applicatives

Applicatives are like functors, except that not only the value is being wrapped in a context, but the function to apply to it also.

Monads

Monads apply a function that returns a wrapped value to a wrapped value

Suppose a function named half which only works on even numbers:

Option<Double> half(Double x) {
    return x % 2 == 0 ? Option.of(x / 2) : Option.none();
}

But what if we feed it a wrapped value?

This is where bind also called flatMap or sometimes chain comes in!

Option.of(3d).flatMap(this::half); // None
Option.of(4d).flatMap(this::half); // Some(2)

If you pass in None, it’s even simpler:

You can chain calls to flatMap:

Option.of(20d)
    .flatMap(this::half)  // Some(10)
    .flatMap(this::half)  // Some(5)
    .flatMap(this::half); // None

So now we know that Option is a functor, an applicative and a monad.

Another example: user types a path, we load the file content and display it

Option<String> getLine() {
    System.out.print("File: ");
    return Option.of(new Scanner(System.in).next());
}

Option<String> readFile(String file) {
    return Try.of(() -> new String(Files.readAllBytes(Paths.get(file)))).toOption();
}

Option<Boolean> putStrLn(String str) {
    System.out.println("File content:\n");
    System.out.println(str);
    return Option.of(str.length() % 2 == 0);
}

And just call it like this:

getLine()
    .flatMap(this::readFile)
    .flatMap(this::putStrLn);

Some more code please!

How to FP in Java with Vavr

  • Functions
    • Composition
    • Lifting
    • Partial application
    • Currying
    • Memoization
  • Values
    • Option
    • Try
    • Either

Functions

Java provides only:

  • Function: accepts one parameter
  • BiFunction: accepts two parameters

Vavr goes up to 8 parameters:

  • Function0, Function1, Function2, …
  • Support checked functions: CheckedFunction1, CheckedFunction2, …
  • Support composition, lifting, currying and memoization
Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
sum.apply(40, 2); // 42
Composition

Functions can be composed:

  • f : X -> Y
  • g : Y -Z
  • h : X -> Z = g(f(x))

Use compose or andThen for mor natural (for humans) order.

Function1<String, String> upperCase = Function1.of(String::toUpperCase);
Function1<String, String> hi = s -> "Hi, " + s;
Function1<String, String> addMark = s -> s + " !";

Function1<String, String> welcome =
    addMark.compose(hi.compose(upperCase));
welcome.apply("tars"); // Hi, TARS !

Function1<String, String> welcome2 =
    upperCase.andThen(hi).andThen(addMark);
welcome2.apply("tars"); // Hi, TARS !
Lifting

Partial Functions are functions that are valid for a specific subset of values but may yield errors for some input

The lift function lifts a partial function into a total function

  • It can accept all input
  • Returns an Option instead of the value
  • Instead of throwing an exception it will return None
Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
Function2<Integer, Integer, Option<Integer>> safeDivide = Function2.lif(divide);

safeDivide.apply(15, 5); // Some(3)
safeDivide.apply(15, 0); // None
Partial application

Partial application allows you to create new function from an existing one by setting some arguments

It is not currying

Function2<Integer, Integer, Integer> multiply = (a, b) -> a * b;
Function1<Integer, Integer> twoTimes = multiply.apply(2);

multiply.apply(4, 3); // 12
twoTimes.apply(9); // 18

Function3<Long, Long, Long, Long> computation = (a, b, c) -> a + (b * c);
Function2<Long, Long, Long> incMultiply = computation.apply(1L);

incMultiply.apply(L2, 3L); // 1 + (2 * 3) = 7
Currying

Currying is the same as converting a function that takes n arguments into n functions taking a single argument each.

Function3<Long, Long, Long, Long> computation = (a, b, c) -> a + (b * c);
Function2<Long, Long, Long> incMultiply = computation.apply(1L);

incMultiply.apply(L2, 3L); // 1 + (2 * 3) = 7

Function1<Long, Function1<Long, Function1<Long, Long>>> curriedComputation =
    computation.curried();

curriedComputation.apply(1L).apply(2L).apply(3L); // 1 + (2 * 3) = 7
Memoization

Memoization acts like some kind of caching, if you memoize a function, it will be only executed once for a specific input

Function0<String> cachedUUID = Function0.of(UUID::randomUUID)
    .andThen(UUID::toString).memoized();

cachedUUID.apply(); // 88e93417-2adb-48f0-81f3-aa9bd41efad3
cachedUUID.apply(); // 88e93417-2adb-48f0-81f3-aa9bd41efad3

Function1<String, String> cachedUserUUID = 
    Function1.of((String user) -> user + ": " + UUID.randomUUID().toString())
    .memoized();
cachedUserUUID.apply("Tars"); // Tars: d01562a7-1a13-45c0-b567-3239f4d0abc1
cachedUserUUID.apply("Kipp"); // Kipp: 304b1e2d-4c76-4785-9acc-de04ecf87730   
cachedUserUUID.apply("Case"); // Case: 2e77448f-9a4d-4fc1-8b78-8805b035dd5b
cachedUserUUID.apply("Tars"); // Tars: d01562a7-1a13-45c0-b567-3239f4d0abc1

Values

Option

Option is a monadic container with additions:

  • map
  • flatMap
  • filter
  • peek

Represents an optional value: None / Some(value).

final String[] robots = new String[] {"Tars", "Kipp", "Case"};
Function0<String> randomRobot = () -> {
    Random r = new Random();
    boolean shouldFail = r.nextInt(10) > 5;
    return shouldFail ? null : robots[r.nextInt(3)];
};

Function1<String, String> upperCase = Function1.of(String::toUpperCase);
Option.of(randomRobot.apply()).map(upperCase); // None
Option.of(randomRobot.apply()).map(upperCase); // Some(KIPP)
Option.of(randomRobot.apply()).map(upperCase); // Some(TARS)
Option.of(randomRobot.apply()).map(upperCase); // Some(TARS)
Option.of(randomRobot.apply()).map(upperCase); // None
Option.of(randomRobot.apply()).map(upperCase); // Some(CASE)
Try

Try is a monadic container wich represents a computation that may either throw an exception of successfuly completes

Like Option it has a lot of additions:

  • map
  • flatMap
  • filter
  • mapTry
  • peek
  • recover
  • onFailure
  • onSuccess
final String[] robots = new String[] {"Tars", "Kipp", "Case"};
Function0<String> randomRobot = () -> {
    Random r = new Random();
    boolean shouldFail = r.nextInt(10) > 5;
    if (shouldFail)
        throw new RuntimeException("Plenty of slaves for my robot colony");
    else
        return robots[r.nextInt(3)];
};

Function1<String, String> upperCase = Function1.of(String::toUpperCase);
Try.of(randomRobot::apply).map(upperCase); // Failure(Plenty of slaves for my robot colony)
Try.of(randomRobot::apply).map(upperCase); // Some(CASE)
Try.of(randomRobot::apply).map(upperCase); // Some(KIPP)
Try.of(randomRobot::apply).map(upperCase); // Failure(Plenty of slaves for my robot colony) 
Try.of(randomRobot::apply).map(upperCase); // Failure(Plenty of slaves for my robot colony)
Try.of(randomRobot::apply).map(upperCase); // Some(CASE)
Try.of(randomRobot::apply).map(upperCase); // Some(TARS)
Either

Either represents a value of two types, it is either a Left or a Right.

By convention Left is the failure case, and Right the success case, like Option and Try it has a lot of additions:

  • right
  • left
  • map
  • flatMap
  • filter
Function0<Either<Throwable, String>> safeRandomRobot =
    () -> {
       Random r = new Random();
       boolean shouldFail = r.nextInt(10) > 5; 
       return shouldFail ? Either.left(new RuntimeException("Plenty of slaves for my robot colony"))
                : Either.right(robots[r.nextInt(3)]);
    };

// Plenty of slaves for my robot colony
safeRandomRobot.apply().map(upperCase).getOrElsGet(Throwable::getMessage);
// TARS
safeRandomRobot.apply().map(upperCase).getOrElsGet(Throwable::getMessage);
// TARS
safeRandomRobot.apply().map(upperCase).getOrElsGet(Throwable::getMessage);
// CASE
safeRandomRobot.apply().map(upperCase).getOrElsGet(Throwable::getMessage);
// Plenty of slaves for my robot colony
safeRandomRobot.apply().map(upperCase).getOrElsGet(Throwable::getMessage);
// KIPP
safeRandomRobot.apply().map(upperCase).getOrElsGet(Throwable::getMessage);

That’s all for now, I encourage you to try Vavr, it can make your code both cleaner and safer at the same time.

Edit: See also my other article about how to use Try efficiently in the context of a pipeline.

Cheers!

Alexandre Grison - //grison.me - @algrison